diff --git a/pom.xml b/pom.xml index 75fa63795e..8aa9317b84 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index dc5a04a7ad..dddca18c0e 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 9876e11ee3..92dd13937a 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index c0436bac90..5d90e8c7a7 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ea1b594b99..e171a2ef31 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.11.0.BUILD-SNAPSHOT + 1.11.0.DATAMONGO-1603-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java index 154ecefdf8..83489e61b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ExpressionEvaluatingParameterBinder.java @@ -45,7 +45,7 @@ /** * {@link ExpressionEvaluatingParameterBinder} allows to evaluate, convert and bind parameters to placeholders within a * {@link String}. - * + * * @author Christoph Strobl * @author Thomas Darimont * @author Oliver Gierke @@ -59,7 +59,7 @@ class ExpressionEvaluatingParameterBinder { /** * Creates new {@link ExpressionEvaluatingParameterBinder} - * + * * @param expressionParser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. */ @@ -76,7 +76,7 @@ public ExpressionEvaluatingParameterBinder(SpelExpressionParser expressionParser /** * Bind values provided by {@link MongoParameterAccessor} to placeholders in {@literal raw} while considering * potential conversions and parameter types. - * + * * @param raw can be {@literal null} or empty. * @param accessor must not be {@literal null}. * @param bindingContext must not be {@literal null}. @@ -93,7 +93,7 @@ public String bind(String raw, MongoParameterAccessor accessor, BindingContext b /** * Replaced the parameter placeholders with the actual parameter values from the given {@link ParameterBinding}s. - * + * * @param input must not be {@literal null} or empty. * @param accessor must not be {@literal null}. * @param bindingContext must not be {@literal null}. @@ -126,7 +126,7 @@ private String replacePlaceholders(String input, MongoParameterAccessor accessor buffer.append(placeholder.getSuffix()); } - if (binding.isQuoted() || placeholder.isQuoted()) { + if (placeholder.isQuoted()) { postProcessQuotedBinding(buffer, valueForBinding, !binding.isExpression() ? accessor.getBindableValue(binding.getParameterIndex()) : null, binding.isExpression()); @@ -247,7 +247,8 @@ private Pattern createReplacementPattern(List bindings) { regex.append("|"); regex.append("(" + Pattern.quote(binding.getParameter()) + ")"); - regex.append("(\\W?['\"])?"); // potential quotation char (as in { foo : '?0' }). + regex.append("([\\w.]*"); + regex.append("(\\W?['\"]|\\w*')?)"); } return Pattern.compile(regex.substring(1)); @@ -263,30 +264,34 @@ private Pattern createReplacementPattern(List bindings) { */ private Placeholder extractPlaceholder(int parameterIndex, Matcher matcher) { - if (matcher.groupCount() > 1) { - - String rawPlaceholder = matcher.group(parameterIndex * 2 + 1); - String suffix = matcher.group(parameterIndex * 2 + 2); + String rawPlaceholder = matcher.group(parameterIndex * 3 + 1); + String suffix = matcher.group(parameterIndex * 3 + 2); - if (!StringUtils.hasText(rawPlaceholder)) { + if (!StringUtils.hasText(rawPlaceholder)) { - rawPlaceholder = matcher.group(); - suffix = "" + rawPlaceholder.charAt(rawPlaceholder.length() - 1); - if (QuotedString.endsWithQuote(rawPlaceholder)) { - rawPlaceholder = QuotedString.unquoteSuffix(rawPlaceholder); + rawPlaceholder = matcher.group(); + if (rawPlaceholder.matches(".*\\d$")) { + suffix = ""; + } else { + int index = rawPlaceholder.replaceAll("[^\\?0-9]*$", "").length() - 1; + if (index > 0 && rawPlaceholder.length() > index) { + suffix = rawPlaceholder.substring(index + 1); } } + if (QuotedString.endsWithQuote(rawPlaceholder)) { + rawPlaceholder = rawPlaceholder.substring(0, + rawPlaceholder.length() - (StringUtils.hasText(suffix) ? suffix.length() : 1)); + } + } - if (StringUtils.hasText(suffix)) { + if (StringUtils.hasText(suffix)) { - boolean quoted = QuotedString.endsWithQuote(suffix); + boolean quoted = QuotedString.endsWithQuote(suffix); - return Placeholder.of(parameterIndex, rawPlaceholder, quoted, - quoted ? QuotedString.unquoteSuffix(suffix) : suffix); - } + return Placeholder.of(parameterIndex, rawPlaceholder, quoted, + quoted ? QuotedString.unquoteSuffix(suffix) : suffix); } - - return Placeholder.of(parameterIndex, matcher.group(), false, null); + return Placeholder.of(parameterIndex, rawPlaceholder, false, null); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index c79f872ad1..2ea13d1a08 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ /** * Query to use a plain JSON String to create the {@link Query} to actually execute. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont @@ -61,7 +61,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { /** * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod} and {@link MongoOperations}. - * + * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. * @param expressionParser must not be {@literal null}. @@ -99,7 +99,6 @@ public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperati this.parameterBinder = new ExpressionEvaluatingParameterBinder(expressionParser, evaluationContextProvider); - if (method.hasAnnotatedQuery()) { org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); @@ -127,10 +126,10 @@ public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperati @Override protected Query createQuery(ConvertingParameterAccessor accessor) { - String queryString = parameterBinder.bind(this.query, accessor, new BindingContext(getQueryMethod() - .getParameters(), queryParameterBindings)); - String fieldsString = parameterBinder.bind(this.fieldSpec, accessor, new BindingContext(getQueryMethod() - .getParameters(), fieldSpecParameterBindings)); + String queryString = parameterBinder.bind(this.query, accessor, + new BindingContext(getQueryMethod().getParameters(), queryParameterBindings)); + String fieldsString = parameterBinder.bind(this.fieldSpec, accessor, + new BindingContext(getQueryMethod().getParameters(), fieldSpecParameterBindings)); Query query = new BasicQuery(queryString, fieldsString).with(accessor.getSort()); @@ -141,7 +140,7 @@ protected Query createQuery(ConvertingParameterAccessor accessor) { return query; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery() */ @@ -168,7 +167,8 @@ protected boolean isDeleteQuery() { return this.isDeleteQuery; } - private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, boolean isDeleteQuery) { + private static boolean hasAmbiguousProjectionFlags(boolean isCountQuery, boolean isExistsQuery, + boolean isDeleteQuery) { return countBooleanValues(isCountQuery, isExistsQuery, isDeleteQuery) > 1; } @@ -188,7 +188,7 @@ private static int countBooleanValues(boolean... values) { /** * A parser that extracts the parameter bindings from a given query string. - * + * * @author Thomas Darimont */ private enum ParameterBindingParser { @@ -211,7 +211,7 @@ private enum ParameterBindingParser { /** * Returns a list of {@link ParameterBinding}s found in the given {@code input} or an * {@link Collections#emptyList()}. - * + * * @param input can be {@literal null} or empty. * @param bindings must not be {@literal null}. * @return @@ -306,7 +306,7 @@ private static void collectParameterReferencesIntoBindings(List while (valueMatcher.find()) { int paramIndex = Integer.parseInt(valueMatcher.group(PARAMETER_INDEX_GROUP)); - boolean quoted = (source.startsWith("'") && source.endsWith("'")) - || (source.startsWith("\"") && source.endsWith("\"")); + boolean quoted = source.startsWith("'") || source.startsWith("\""); bindings.add(new ParameterBinding(paramIndex, quoted)); } @@ -357,7 +356,7 @@ private static int getIndexOfExpressionParameter(String input, int position) { /** * A generic parameter binding with name or position information. - * + * * @author Thomas Darimont */ static class ParameterBinding { @@ -368,7 +367,7 @@ static class ParameterBinding { /** * Creates a new {@link ParameterBinding} with the given {@code parameterIndex} and {@code quoted} information. - * + * * @param parameterIndex * @param quoted whether or not the parameter is already quoted. */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java index 8d9237bcbe..919c584750 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQueryUnitTests.java @@ -60,7 +60,7 @@ /** * Unit tests for {@link StringBasedMongoQuery}. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont @@ -437,6 +437,78 @@ public void shouldReplaceParametersInInQuotedExpressionOfNestedQueryOperator() t assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject("lastname", Pattern.compile("^(calamity)")))); } + @Test // DATAMONGO-1603 + public void shouldAllowReuseOfPlaceholderWithinQuery() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimes", String.class, + String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamity"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowReuseOfQuotedPlaceholderWithinQuery() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByReusingPlaceholdersMultipleTimesWhenQuoted", + String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamity"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowReuseOfQuotedPlaceholderWithinQueryAndIncludeSuffixCorrectly() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod( + "findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended", String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), is((DBObject) new BasicDBObject().append("arg0", "calamity") + .append("arg1", "regalia").append("arg2", "calamitys"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowQuotedParameterWithSuffixAppended() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByWhenQuotedAndSomeStuffAppended", String.class, + String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + assertThat(query.getQueryObject(), + is((DBObject) new BasicDBObject().append("arg0", "calamity").append("arg1", "regalias"))); + } + + @Test // DATAMONGO-1603 + public void shouldCaptureReplacementWithComplexSuffixCorrectly() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByMultiRegex", String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + + assertThat(query.getQueryObject(), is((DBObject) JSON.parse( + "{ \"$or\" : [ { \"firstname\" : { \"$regex\" : \".*calamity.*\" , \"$options\" : \"i\"}} , { \"lastname\" : { \"$regex\" : \".*calamityxyz.*\" , \"$options\" : \"i\"}}]}"))); + } + + @Test // DATAMONGO-1603 + public void shouldAllowPlaceholderReuseInQuotedValue() throws Exception { + + StringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameRegex", String.class, String.class); + ConvertingParameterAccessor accessor = StubParameterAccessor.getAccessor(converter, "calamity", "regalia"); + + org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accessor); + + assertThat(query.getQueryObject(), + is((DBObject) JSON.parse("{ 'lastname' : { '$regex' : '^(calamity|John regalia|regalia)'} }"))); + } + private StringBasedMongoQuery createQueryForMethod(String name, Class... parameters) throws Exception { Method method = SampleRepository.class.getMethod(name, parameters); @@ -460,6 +532,9 @@ private interface SampleRepository extends Repository { @Query("{ 'lastname' : { '$regex' : '^(?0)'} }") Person findByLastnameRegex(String lastname); + @Query("{'$or' : [{'firstname': {'$regex': '.*?0.*', '$options': 'i'}}, {'lastname' : {'$regex': '.*?0xyz.*', '$options': 'i'}} ]}") + Person findByMultiRegex(String arg0); + @Query("{ 'address' : ?0 }") Person findByAddress(Address address); @@ -510,5 +585,20 @@ private interface SampleRepository extends Repository { @Query("{ 'arg0' : ?0 }") List findByWithBsonArgument(DBObject arg0); + + @Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : ?0 }") + List findByReusingPlaceholdersMultipleTimes(String arg0, String arg1); + + @Query("{ 'arg0' : ?0, 'arg1' : ?1, 'arg2' : '?0' }") + List findByReusingPlaceholdersMultipleTimesWhenQuoted(String arg0, String arg1); + + @Query("{ 'arg0' : '?0', 'arg1' : ?1, 'arg2' : '?0s' }") + List findByReusingPlaceholdersMultipleTimesWhenQuotedAndSomeStuffAppended(String arg0, String arg1); + + @Query("{ 'arg0' : '?0', 'arg1' : '?1s' }") + List findByWhenQuotedAndSomeStuffAppended(String arg0, String arg1); + + @Query("{ 'lastname' : { '$regex' : '^(?0|John ?1|?1)'} }") // use spel or some regex string this is fucking bad + Person findByLastnameRegex(String lastname, String alternative); } }