6
\$\begingroup\$

I ran the following code through Sonar, a static analysis tool.

Original file:

public interface LegRepository extends CustomRepository<Leg, Long> {

    final String FIND_START_LEG_BY_TRANSPORTMISSION_ID = "SELECT ll.* "
            + "FROM T_Movement_Leg ll "
            + "INNER JOIN "
            + "(SELECT l.TransportMission_SID tptId, min(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' "
            + "GROUP BY l.TransportMission_SID) groupedl "
            + "ON ll.TransportMission_SID = groupedl.tptId "
            + "AND ll.legorder = groupedl.legorder";

    final String FIND_LAST_LEG_BY_TRANSPORTMISSION_ID = "SELECT ll.* "
            + "FROM T_Movement_Leg ll "
            + "INNER JOIN "
            + "(SELECT l.TransportMission_SID tptId, max(l.LegEffective_EndDate) as datetime "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' "
            + "GROUP BY l.TransportMission_SID) groupedl "
            + "ON ll.TransportMission_SID = groupedl.tptId "
            + "AND ll.LegEffective_EndDate = groupedl.datetime";

    final String FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = "SELECT ll.* "
            + "FROM T_Movement_Leg ll "
            + "INNER JOIN "
            + "(SELECT l.TransportMission_SID tptId, max(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' and l.legorder < ?2 "
            + "GROUP BY l.TransportMission_SID) groupedl "
            + "ON ll.TransportMission_SID = groupedl.tptId "
            + "AND ll.legorder = groupedl.legorder";

    final String FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = "SELECT l.* "
            + "FROM T_Movement_Leg l "
            + "WHERE l.TransportMission_SID = ?1 AND l.legorder=?2";

    final String FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = "SELECT l.* "
            + "FROM T_Movement_Leg l "
            + "WHERE l.TransportMission_SID = ?1 AND l.legorder>=?2";


    final String FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = "SELECT ll.* "
            + "FROM T_Movement_Leg ll "
            + "INNER JOIN "
            + "(SELECT l.TransportMission_SID tptId, min(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' and l.legorder > ?2 "
            + "GROUP BY l.TransportMission_SID) groupedl "
            + "ON ll.TransportMission_SID = groupedl.tptId "
            + "AND ll.legorder = groupedl.legorder";

    final String FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID = "SELECT max(l.legorder) "
            + "FROM T_Movement_Leg l "
            + "WHERE l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true'";

    @Query(value = FIND_START_LEG_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Leg findStartLeg(Long tptMissionId);

    @Query(value = FIND_LAST_LEG_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Leg findEndLeg(Long tptMissionId);

    @Query(value = FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findPreviousLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findNextLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Integer findLastOrderNumberByTransportMission(Long tptMissionId);

    @Query(value = FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findCurrentLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    List<Leg> findCurrentAndFollowingLegs(long tptMissionId, Integer legOrder);

    List<Leg> findByTransportMissionOrderByLegorderAsc(TransportMission transportMission);

    List<Leg> findByTransportMissionAndCanceledOrderByLegorderAsc(TransportMission transportMission, Boolean canceled);
}

In my opinion, it is perfectly readable. However, Sonar complains:

  • The final is absolute cause it's an interface.
  • Some literal is used more then 2 times.

So after refactoring I get this class with 100% compliance of Sonar:

public interface LegRepository extends CustomRepository<Leg, Long> {
    String FROM_MOVEMENT_LEG = "FROM T_Movement_Leg ll ";
    String INNER_JOIN = "INNER JOIN ";
    String GROUP_BY = "GROUP BY l.TransportMission_SID) groupedl ";
    String ON_TRANSPORTMISSION_ID_AND_LEGORDER = "ON ll.TransportMission_SID = groupedl.tptId AND ll.legorder = groupedl.legorder";
    String SELECT_ALL_LL = "SELECT ll.* ";

    String FIND_START_LEG_BY_TRANSPORTMISSION_ID = SELECT_ALL_LL
            + FROM_MOVEMENT_LEG
            + INNER_JOIN
            + "(SELECT l.TransportMission_SID tptId, min(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' "
            + GROUP_BY
            + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

    String FIND_LAST_LEG_BY_TRANSPORTMISSION_ID = SELECT_ALL_LL
            + FROM_MOVEMENT_LEG
            + INNER_JOIN
            + "(SELECT l.TransportMission_SID tptId, max(l.LegEffective_EndDate) as datetime "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' "
            + GROUP_BY
            + "ON ll.TransportMission_SID = groupedl.tptId "
            + "AND ll.LegEffective_EndDate = groupedl.datetime";

    String FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = SELECT_ALL_LL
            + FROM_MOVEMENT_LEG
            + INNER_JOIN
            + "(SELECT l.TransportMission_SID tptId, max(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' and l.legorder < ?2 "
            + GROUP_BY
            + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

    String FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = SELECT_ALL_LL
            + FROM_MOVEMENT_LEG
            + "WHERE ll.TransportMission_SID = ?1 AND ll.legorder=?2";

    String FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = SELECT_ALL_LL
            + "FROM T_Movement_Leg ll "
            + "WHERE ll.TransportMission_SID = ?1 AND ll.legorder>=?2";


    String FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = SELECT_ALL_LL
            + FROM_MOVEMENT_LEG
            + INNER_JOIN
            + "(SELECT l.TransportMission_SID tptId, min(l.legorder) as legorder "
            + "FROM T_Movement_Leg l where l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true' and l.legorder > ?2 "
            + GROUP_BY
            + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

    String FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID = "SELECT max(ll.legorder) "
            + "FROM T_Movement_Leg ll "
            + "WHERE ll.TransportMission_SID = ?1 and ll.is_Canceled = 'false' and ll.IN_USE = 'true'";

    @Query(value = FIND_START_LEG_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Leg findStartLeg(Long tptMissionId);

    @Query(value = FIND_LAST_LEG_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Leg findEndLeg(Long tptMissionId);

    @Query(value = FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findPreviousLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findNextLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID, nativeQuery = true)
    Integer findLastOrderNumberByTransportMission(Long tptMissionId);

    @Query(value = FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    Leg findCurrentLeg(Long tptMissionId, Integer legOrder);

    @Query(value = FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER, nativeQuery = true)
    List<Leg> findCurrentAndFollowingLegs(long tptMissionId, Integer legOrder);

    List<Leg> findByTransportMissionOrderByLegorderAsc(TransportMission transportMission);

    List<Leg> findByTransportMissionAndCanceledOrderByLegorderAsc(TransportMission transportMission, Boolean canceled);
}

The final tag I can understand, however I find it better to read static final then final alone, just to make clear to someone who read's the code. It's indeed obsolete but when you don't see it's an interface you are confused.
I can live with this, because it's actually normal.

What I have a problem with is actually the second part.
I have to make more fields because I repeat some parts of the query's.
Yes that's true, but how can you read now the query easy?
It's because I want the query readable, I split it in parts.
By splitting it, I need now to insert other fields where you can't read directly what's behind it.

What do you think about this?

\$\endgroup\$
  • \$\begingroup\$ I might expand this comment to a proper answer later, but have you considered using a builder-like pattern to actually construct your plain SQL query Strings? \$\endgroup\$ – h.j.k. Mar 4 '15 at 9:17
  • \$\begingroup\$ Yes, I considered that but then I would have the same remarks because the splitting I leave at the same location. This only saves me some time. (Stringbuilder is faster then "x " + "y ") \$\endgroup\$ – chillworld Mar 4 '15 at 10:35
13
\$\begingroup\$

First things first, I assume you are already convinced that using literal SQL query strings is the way to go in this project/class, and that there are no comparable solutions from using an established database access library.

If you prioritize readability of literal SQL query strings, then @200_success's advice is solid. However, looking at the content of your query strings, I think you may want to consider approaching refactoring not from a Sonar remarks or even a 'literacy' perspective, but a pattern-identification perspective.

Your refactored code is not fully refactored actually, since you were still using a case mixture of keywords e.g. and/AND. Taking things to the extreme (I like living on the edge, sometimes), I managed to 'condense' your literal Strings into the following fragments:

// extra line breaks added for CodeReview.SE presentation
String SELECT_ALL_LL = "SELECT ll.* ";
String FROM = "FROM T_Movement_Leg @@ WHERE @@.TransportMission_SID = ?1 ";
String SELECT_INNER_JOIN = SELECT_ALL_LL + "FROM T_Movement_Leg ll " 
                + "INNER JOIN (SELECT l.TransportMission_SID tptId, %s %s ";
String BASE_CONDITION = FROM.replaceAll("@@", "l") 
                + "AND l.is_Canceled = 'false' AND l.IN_USE = 'true' ";
String ON_TRANSPORTMISSION_ID = "GROUP BY l.TransportMission_SID) groupedl ON " 
                + "ll.TransportMission_SID = groupedl.tptId AND ll.%s = groupedl.%s";
String ON_TRANSPORTMISSION_ID_AND_LEGORDER = 
                String.format(ON_TRANSPORTMISSION_ID, "legorder", "legorder");
String CURRENT_LEG = SELECT_ALL_LL + FROM.replaceAll("@@", "ll") 
                + "AND ll.legorder %s ?2";

And the final definitions of your query strings:

// extra line breaks added for CodeReview.SE presentation
String FIND_START_LEG_BY_TRANSPORTMISSION_ID = 
        String.format(SELECT_INNER_JOIN, "min(l.legorder)", "legorder")
        + BASE_CONDITION
        + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

String FIND_LAST_LEG_BY_TRANSPORTMISSION_ID = 
        String.format(SELECT_INNER_JOIN, "max(l.LegEffective_EndDate)", "datetime")
        + BASE_CONDITION
        + String.format(ON_TRANSPORTMISSION_ID, "LegEffective_EndDate", "datetime");

String FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        String.format(SELECT_INNER_JOIN, "max(l.legorder)", "legorder")
        + BASE_CONDITION + "AND l.legorder < ?2 "
        + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

String FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        String.format(SELECT_INNER_JOIN, "min(l.legorder)", "legorder")
        + BASE_CONDITION + "AND l.legorder > ?2 "
        + ON_TRANSPORTMISSION_ID_AND_LEGORDER;

String FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        String.format(CURRENT_LEG, "=");

String FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        String.format(CURRENT_LEG, ">=");

String FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID = 
        "SELECT max(l.legorder) " + BASE_CONDITION;

The patterns I identified are:

  • Finding the start/last/previous/next legs require an inner join on a table that has a fixed BASE_CONDITION with an optional supplementary condition ("AND l.legorder < ?2 "/"AND l.legorder > ?2 "), and both tables are joined on very similar clauses.
  • Finding the following legs in addition to the current leg only differs from the latter by a conditional operator, namely >= vs. =.
  • Finding the last order number is pretty much a standalone query, though it shares the same BASE_CONDITION.

These patterns hint that you may want to consider using a builder-like pattern to construct your query strings, which also helps to solve one point mentioned in @200_success's answer:

Furthermore, it would be a tragedy to expose these SQL fragments in your interface.

A 'query builder' can be further extended to validate parts of the query during its construction, in addition to hiding away the query fragments while exposing (arguably) readable method names for interfacing. Here is a sample, simple implementation for your queries, based on the extensive refactoring done above:

public static final class QueryBuilder {
    // extra line breaks added for CodeReview.SE presentation
    private static final String SELECT_ALL_LL = "SELECT ll.* ";
    private static final String FROM = "FROM T_Movement_Leg @@ WHERE " 
                                    + "@@.TransportMission_SID = ?1 ";
    private static final String SELECT_INNER_JOIN = SELECT_ALL_LL 
                                    + "FROM T_Movement_Leg ll INNER JOIN (SELECT " 
                                    + "l.TransportMission_SID tptId, %s %s ";
    private static final String BASE_CONDITION = FROM.replaceAll("@@", "l") + " AND "
                                    + "l.is_Canceled = 'false' AND l.IN_USE = 'true' ";
    private static final String ON_TPT_ID = "GROUP BY l.TransportMission_SID) groupedl "
                                    + "ON ll.TransportMission_SID = groupedl.tptId "
                                    + "AND ll.%s = groupedl.%s";
    private static final String ON_TRANSPORTMISSION_ID_AND_LEGORDER = 
                                    String.format(ON_TPT_ID, "legorder", "legorder");
    private static final String CURR_LEG = SELECT_ALL_LL + FROM.replaceAll("@@", "ll")
                                    + "AND ll.legorder %s ?2";

    private final StringBuilder builder = new StringBuilder();

    private QueryBuilder() {
        // empty
    }

    public static QueryBuilder selectWithInnerJoin(final String supplementaryColumn,
            final String columnAlias) {
        final QueryBuilder qBuilder = new QueryBuilder();
        qBuilder.builder
                .append(String.format(SELECT_INNER_JOIN, supplementaryColumn, 
                                        columnAlias))
                .append(BASE_CONDITION);
        return qBuilder;
    }

    public QueryBuilder andLegOrderCondition(final String condition) {
        builder.append(" AND l.legorder ").append(condition).append(" ?2 ");
        return this;
    }

    public String onDefaultColumns() {
        return builder.append(ON_TRANSPORTMISSION_ID_AND_LEGORDER).toString();
    }

    public String onDefaultColumnAnd(final String leftColumn, 
            final String rightColumn) {
        return builder.append(String.format(ON_TPT_ID, 
                    "LegEffective_EndDate", "datetime")).toString();
    }

    public static String selectWithLegOrderCondition(final String condition) {
        return String.format(CURR_LEG, condition);
    }

    public static String selectLastLegOrder() {
        return "SELECT max(l.legorder) " + BASE_CONDITION;
    }

    @Override
    public String toString() {
        return builder.toString();
    }
}

Generating the final query strings then becomes:

// extra line breaks added for CodeReview.SE presentation
String FIND_START_LEG_BY_TRANSPORTMISSION_ID = QueryBuilder
        .selectWithInnerJoin("min(l.legorder)", "legorder")
        .onDefaultColumns();

String FIND_LAST_LEG_BY_TRANSPORTMISSION_ID = QueryBuilder
        .selectWithInnerJoin("max(l.LegEffective_EndDate)", "datetime")
        .onDefaultColumnAnd("LegEffective_EndDate", "datetime");

String FIND_PREVIOUS_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = QueryBuilder
        .selectWithInnerJoin("max(l.legorder)", "legorder")
        .andLegOrderCondition("<").onDefaultColumns();

String FIND_NEXT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = QueryBuilder
        .selectWithInnerJoin("min(l.legorder)", "legorder")
        .andLegOrderCondition(">").onDefaultColumns();

String FIND_CURRENT_LEG_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        QueryBuilder.selectWithLegOrderCondition("=");

String FIND_CURRENT_AND_FOLLOWING_LEGS_BY_TRANSPORTMISSION_ID_AND_LEG_ORDER = 
        QueryBuilder.selectWithLegOrderCondition(">=");

String FIND_LAST_ORDER_NUMBER_BY_TRANSPORTMISSION_ID = 
        QueryBuilder.selectLastLegOrder();

In Summary

It is sometimes OK to hard-code literal query strings, especially when prioritizing readability of the raw queries in code, when the queries themselves are simple enough, or when there are no distinctive patterns.

However, when these literal strings become too boilerplate-like, it might be worthwhile to either invest time in using a database access library, or constructing the queries with the aid of the builder approach so that common parts of the queries remain standardized.

Is the sample QueryBuilder really a good way to keep reusability higher? I don't think there's a definitive answer to that... Always evaluate the pros and cons.

\$\endgroup\$
  • \$\begingroup\$ I really like the QueryBuilder- it's what I was thinking of when I suggested doing the SQL programmatically. There is an added benefit that, if you change your underlying DB and there are minor variations to the SQL dialect, the abstraction layer should guard against that. \$\endgroup\$ – Danikov Mar 5 '15 at 10:31
  • 1
    \$\begingroup\$ @h.j.k. this is why I love CR, get the unexpected answer and see that it can improve your code \$\endgroup\$ – chillworld Mar 5 '15 at 12:49
  • \$\begingroup\$ @chillworld, glad to help! :) \$\endgroup\$ – h.j.k. Mar 5 '15 at 14:11
  • 1
    \$\begingroup\$ @Danikov, yeah and that's why we have database access libraries for that. :) My sample implementation is admittedly a poor abstraction still, it can and will be made quickly redundant if the queries' structure changes. It's just a start... \$\endgroup\$ – h.j.k. Mar 5 '15 at 14:12
8
\$\begingroup\$

Embedding long strings like this into your source code is not ideal with Java and you might find putting them into files, loading them, and extracting the Strings from them to be much tidier. It allows your text to be reproduced in the source file verbatim without quotes, escaping, appending, and newline issues and it cleans up your class too. As a side benefit, you may be able to give the external files an .sql extension and they will be executable/testable in their own right.

Having the SQL on your interface is a bit of a code-smell as interfaces should be concerned with signatures, not implementation.

Sonar isn't necessarily right and will detect false positives. Repetition inside strings shouldn't always be extracted out as there may coincidental or minor, genuine repetition (the repeated usage of 'SELECT' for example) that could change independently going forward. You can instruct Sonar to treat them as false positives and it will stop warning you about them.

Building SQL in your code is another code smell and, if done badly, can be subject to injection attacks. If you go down this path, you will want to do it programmatically and rely on proper escaping of parameters.

\$\endgroup\$
  • 2
    \$\begingroup\$ Injection is no problem here cause this is spring data => the variable are already protected against sql injection, cause spring data generate the actual implementation. \$\endgroup\$ – chillworld Mar 4 '15 at 10:31
  • \$\begingroup\$ Yeah, I suspected spring magic would be involved :) \$\endgroup\$ – Danikov Mar 5 '15 at 10:26
7
\$\begingroup\$

The original code is definitely better in every way.

Despite the fact that the source code is written as a "sum" of strings, the Java compiler should embed the concatenated version into the bytecode. Therefore, the commonality that Sonar sees has no basis in reality.

Furthermore, it would be a tragedy to expose these SQL fragments in your interface.

If you must do something to silence the Sonar alerts, I suggest inconsistently adding superfluous whitespace in a way that the fragments are no longer seen as common.

\$\endgroup\$
  • \$\begingroup\$ Modifying your source (with whitespace, of all things) to defeat Sonar alerts is not the way to go- there are specific annotations if you want to stop Sonar from checking code (but you'll turn it off entirely) or you can tell it to treat individual cases as false positives. Celebrating that the strings are optimised away as single strings feels like a bad case of pre-optimisation as well; prioritise maintainability over performance first. \$\endgroup\$ – Danikov Mar 5 '15 at 10:29
7
\$\begingroup\$

It's worth noting that if you ever need to debug one of these queries and wanted to print it out (say, you made a typo and can't spot it in the source code), this wouldn't be very readable.

Using the first query as an example, I recommend this:

final String FIND_START_LEG_BY_TRANSPORTMISSION_ID = 
    "SELECT ll.*\n" +
    "FROM T_Movement_Leg ll\n" +
    "    INNER JOIN (SELECT l.TransportMission_SID tptId, min(l.legorder) as legorder=n" +
    "FROM T_Movement_Leg l\n" +
    "WHERE l.TransportMission_SID = ?1 and l.is_Canceled = 'false' and l.IN_USE = 'true'\n" +
    "GROUP BY l.TransportMission_SID) groupedl\n" +
    "    ON ll.TransportMission_SID = groupedl.tptId\n" +
    "    AND ll.legorder = groupedl.legorder\n";

By adding the new line character to the end of each of our source code string breaks, and fixing up this white space, we make sure both our souce code, and our debugging println version of this query look more like what we'd expect to see if we were directly writing a script to use directly against the database.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.