DATACMNS-669 - Added support to bind Querydsl Predicates to Spring MVC controllers.

Adding @QuerydslPredicate to a parameter within SpringMVC Controller when having registered the QueryDslPredicateArgumentResolver allows to generate a Querydsl Predicate based on the request parameters.

Parameters will be converted into their corresponding property types before using the as predicate value. Given more than one attribute will connect those using 'and'. Collections of values will force usage of 'in' while single values on collection like properties result in 'contains'.

The base type for building the Querydsl Path is extracted from the methods return type. In case of non domain type return values it can optionally be specified via @QueydslPredicate(root = …). Specific Predicate conversions can be registered via '@QuerydslSpecification'.

This allows to sneak in specific property path handlings more easily.

new QueryDslPredicateSpecification() {
  {
    define(new StringPath("address.city"), (path, value) -> path.like(value.toString()));
  }
};

Renamed resolver, accessor etc. to express operation of binding properties to a resolved path and introduced explicit binding context which allows reuse of predicate builder within the ArgumentResolver.

Added registration of QuerydslPredicateArgumentResolver directly in SpringDataWebConfiguration reusing the provided ConversionService.

Added Javadoc and changed visibility of non public API and types.

Original pull request: #132.
This commit is contained in:
Christoph Strobl
2015-07-06 15:13:20 +02:00
committed by Oliver Gierke
parent 30b4cf53fc
commit e645501174
14 changed files with 1521 additions and 1 deletions

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.querydsl;
import com.mysema.query.annotations.QueryEntity;
/**
* @author Christoph Strobl
*/
@QueryEntity
public class Address {
String street;
String city;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011-2014 the original author or authors.
* Copyright 2011-2015 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.
@@ -16,16 +16,23 @@
package org.springframework.data.querydsl;
import java.util.Date;
import java.util.List;
import com.mysema.query.annotations.QueryEntity;
/**
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
*/
@QueryEntity
public class User {
String firstname;
String lastname;
Date dateOfBirth;
Address address;
List<String> nickNames;
Long inceptionYear;
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.web.querydsl;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsNull.*;
import static org.junit.Assert.*;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.querydsl.Address;
import org.springframework.data.querydsl.QAddress;
import org.springframework.data.querydsl.User;
import org.springframework.data.util.ClassTypeInformation;
import com.mysema.query.types.Predicate;
import com.mysema.query.types.path.StringPath;
/**
* Unit tests for {@link QuerydslBindings}.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class QuerydslBindingsUnitTests {
private QuerydslPredicateBuilder builder;
private QuerydslBindings typeBasedBindings;
private QuerydslBindings pathBasedBindings;
@Before
public void setUp() {
builder = new QuerydslPredicateBuilder();
typeBasedBindings = new QuerydslBindings() {
{
bind(String.class, new QuerydslBinding<StringPath>() {
@Override
public Predicate bind(StringPath path, Object value) {
return path.contains(value.toString());
}
});
}
};
pathBasedBindings = new QuerydslBindings() {
{
bind("address.street", new QuerydslBinding<StringPath>() {
@Override
public Predicate bind(StringPath path, Object value) {
return path.contains(value.toString());
}
});
bind("firstname", new QuerydslBinding<StringPath>() {
@Override
public Predicate bind(StringPath path, Object value) {
return path.contains(value.toString());
}
});
}
};
}
/**
* @see DATACMNS-669
*/
@Test(expected = IllegalArgumentException.class)
public void getBindingForPathShouldThrowErrorWhenPathIsNull() {
pathBasedBindings.getBindingForPath(null);
}
/**
* @see DATACMNS-669
*/
@Test
public void getBindingForPathShouldReturnNullWhenNoSpecifcBindingAvailable() {
assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("lastname", User.class)), nullValue());
}
/**
* @see DATACMNS-669
*/
@Test
public void getBindingForPathShouldReturnSpeficicBindingWhenAvailable() {
assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("firstname", User.class)), notNullValue());
}
/**
* @see DATACMNS-669
*/
@Test
public void getBindingForPathShouldReturnSpeficicBindingForNestedElementsWhenAvailable() {
assertThat(pathBasedBindings.getBindingForPath(PropertyPath.from("address.street", User.class)), notNullValue());
}
/**
* @see DATACMNS-669
*/
@Test
public void getBindingForPathShouldReturnSpeficicBindingForTypes() {
assertThat(typeBasedBindings.getBindingForPath(PropertyPath.from("address.street", User.class)), notNullValue());
}
/**
* @see DATACMNS-669
*/
@Test
public void getBindingForPathShouldIgnoreSpeficicBindingForTypesWhenTypesDoNotMatch() {
assertThat(typeBasedBindings.getBindingForPath(PropertyPath.from("inceptionYear", User.class)), nullValue());
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnTrueWhenNoRestrictionDefined() {
assertThat(typeBasedBindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnTrueWhenPathContainedInIncluding() {
assertThat(
new QuerydslBindings().including("inceptionYear").isPathVisible(PropertyPath.from("inceptionYear", User.class)),
is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnFalseWhenPathNotContainedInIncluding() {
assertThat(
new QuerydslBindings().including("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)),
is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnFalseWhenPathContainedInExcluding() {
assertThat(
new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("inceptionYear", User.class)),
is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnTrueWhenPathNotContainedInExcluding() {
assertThat(
new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)),
is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnTrueWhenPathContainedInExcludingAndIncluding() {
assertThat(
new QuerydslBindings().excluding("inceptionYear").isPathVisible(PropertyPath.from("firstname", User.class)),
is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnFalseWhenPartialPathContainedInExcluding() {
assertThat(
new QuerydslBindings().excluding("address").isPathVisible(PropertyPath.from("address.city", User.class)),
is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnTrueWhenPartialPathContainedInExcludingButConcretePathIsIncluded() {
assertThat(
new QuerydslBindings().excluding("address").including("address.city")
.isPathVisible(PropertyPath.from("address.city", User.class)), is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void isPathVisibleShouldReturnFalseWhenPartialPathContainedInExcludingAndConcretePathToDifferentPropertyIsIncluded() {
assertThat(
new QuerydslBindings().excluding("address").including("address.city")
.isPathVisible(PropertyPath.from("address.street", User.class)), is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void usesTypeBasedBindingIfConfigured() {
MutablePropertyValues values = new MutablePropertyValues(Collections.singletonMap("city", "Dresden"));
QuerydslBindingContext context = new QuerydslBindingContext(ClassTypeInformation.from(Address.class),
this.typeBasedBindings, null);
assertThat(builder.getPredicate(values, context), is((Predicate) QAddress.address.city.contains("Dresden")));
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.web.querydsl;
import static org.hamcrest.core.Is.*;
import java.util.Arrays;
import org.hamcrest.Matcher;
import org.hamcrest.core.Is;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.querydsl.QUser;
import com.mysema.query.types.Expression;
import com.mysema.query.types.Predicate;
/**
* @author Christoph Strobl
*/
public class QuerydslDefaultBindingUnitTests {
QuerydslDefaultBinding builder;
@Before
public void setUp() {
builder = new QuerydslDefaultBinding();
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateCorrectlyWhenPropertyIsInRoot() {
Predicate predicate = builder.bind(QUser.user.firstname, "tam");
assertThat(predicate, is(QUser.user.firstname.eq("tam")));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateCorrectlyWhenPropertyIsInNestedElement() {
Predicate predicate = builder.bind(QUser.user.address.city, "two rivers");
Assert.assertThat(predicate.toString(), is(QUser.user.address.city.eq("two rivers").toString()));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateCorrectlyWhenValueIsNull() {
Predicate predicate = builder.bind(QUser.user.firstname, null);
assertThat(predicate, is(QUser.user.firstname.isNull()));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateWithContainingWhenPropertyIsCollectionLikeAndValueIsObject() {
Predicate predicate = builder.bind(QUser.user.nickNames, "dragon reborn");
assertThat(predicate, is(QUser.user.nickNames.contains("dragon reborn")));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateWithInWhenPropertyIsAnObjectAndValueIsACollection() {
Predicate predicate = builder.bind(QUser.user.firstname, Arrays.asList("dragon reborn", "shadowkiller"));
assertThat(predicate, is(QUser.user.firstname.in(Arrays.asList("dragon reborn", "shadowkiller"))));
}
/*
* just to satisfy generic type boundaries o_O
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private void assertThat(Predicate predicate, Matcher<? extends Expression> matcher) {
Assert.assertThat((Expression) predicate, Is.<Expression> is((Matcher<Expression>) matcher));
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.web.querydsl;
import static org.hamcrest.core.Is.*;
import static org.junit.Assert.*;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.querydsl.QUser;
import org.springframework.data.querydsl.User;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.ServletWebRequest;
import com.mysema.query.types.Predicate;
import com.mysema.query.types.expr.BooleanExpression;
import com.mysema.query.types.path.StringPath;
/**
* @author Christoph Strobl
*/
public class QuerydslPredicateArgumentResolverUnitTests {
QuerydslPredicateArgumentResolver resolver;
MockHttpServletRequest request;
@Before
public void setUp() {
resolver = new QuerydslPredicateArgumentResolver(null);
request = new MockHttpServletRequest();
}
/**
* @see DATACMNS-669
*/
@Test
public void supportsParameterReturnsTrueWhenMethodParameterIsPredicateAndAnnotatedCorrectly() {
assertThat(resolver.supportsParameter(getMethodParameterFor("simpleFind", Predicate.class)), is(true));
}
/**
* @see DATACMNS-669
*/
@Test
public void supportsParameterReturnsTrueWhenMethodParameterIsPredicateButNotAnnotatedAsSuch() {
assertThat(resolver.supportsParameter(getMethodParameterFor("predicateWithoutAnnotation", Predicate.class)),
is(true));
}
/**
* @see DATACMNS-669
*/
@Test(expected = IllegalArgumentException.class)
public void supportsParameterShouldThrowExceptionWhenMethodParameterIsNoPredicateButAnnotatedAsSuch() {
resolver.supportsParameter(getMethodParameterFor("nonPredicateWithAnnotation", String.class));
}
/**
* @see DATACMNS-669
*/
@Test
public void supportsParameterReturnsFalseWhenMethodParameterIsNoPredicate() {
assertThat(resolver.supportsParameter(getMethodParameterFor("nonPredicateWithoutAnnotation", String.class)),
is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldCreateSingleStringParameterPredicateCorrectly() throws Exception {
request.addParameter("firstname", "rand");
Object predicate = (BooleanExpression) resolver.resolveArgument(
getMethodParameterFor("simpleFind", Predicate.class), null, new ServletWebRequest(request), null);
assertThat(predicate, Is.<Object> is(QUser.user.firstname.eq("rand")));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldCreateMultipleParametersPredicateCorrectly() throws Exception {
request.addParameter("firstname", "rand");
request.addParameter("lastname", "al'thor");
Object predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null,
new ServletWebRequest(request), null);
assertThat(predicate, Is.<Object> is(QUser.user.firstname.eq("rand").and(QUser.user.lastname.eq("al'thor"))));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldCreateNestedObjectPredicateCorrectly() throws Exception {
request.addParameter("address.city", "two rivers");
Object predicate = resolver.resolveArgument(getMethodParameterFor("simpleFind", Predicate.class), null,
new ServletWebRequest(request), null);
assertThat(predicate.toString(), is(QUser.user.address.city.eq("two rivers").toString()));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldResolveTypePropertyFromPageCorrectly() throws Exception {
request.addParameter("address.city", "tar valon");
Object predicate = resolver.resolveArgument(getMethodParameterFor("pagedFind", Predicate.class, Pageable.class),
null, new ServletWebRequest(request), null);
assertThat(predicate.toString(), is(QUser.user.address.city.eq("tar valon").toString()));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldHonorCustomSpeficifcation() throws Exception {
request.addParameter("firstname", "egwene");
request.addParameter("lastname", "al'vere");
Object predicate = resolver.resolveArgument(getMethodParameterFor("specificFind", Predicate.class), null,
new ServletWebRequest(request), null);
assertThat(predicate.toString(),
is(QUser.user.firstname.eq("egwene".toUpperCase()).and(QUser.user.lastname.toLowerCase().eq("al'vere"))
.toString()));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateForNonStringPropertyCorrectly() throws Exception {
request.addParameter("inceptionYear", "978");
Object predicate = (BooleanExpression) resolver.resolveArgument(
getMethodParameterFor("specificFind", Predicate.class), null, new ServletWebRequest(request), null);
assertThat(predicate, Is.<Object> is(QUser.user.inceptionYear.eq(978L)));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldCreatePredicateForNonStringListPropertyCorrectly() throws Exception {
request.addParameter("inceptionYear", new String[] { "978", "998" });
Object predicate = (BooleanExpression) resolver.resolveArgument(
getMethodParameterFor("specificFind", Predicate.class), null, new ServletWebRequest(request), null);
assertThat(predicate, Is.<Object> is(QUser.user.inceptionYear.in(978L, 998L)));
}
/**
* @see DATACMNS-669
*/
@Test
public void shouldExcludePorpertiesCorrectly() throws Exception {
request.addParameter("address.street", "downhill");
request.addParameter("inceptionYear", "973");
Object predicate = resolver.resolveArgument(getMethodParameterFor("specificFind", Predicate.class), null,
new ServletWebRequest(request), null);
assertThat(predicate.toString(), is(QUser.user.inceptionYear.eq(973L).toString()));
}
private MethodParameter getMethodParameterFor(String methodName, Class<?>... args) throws RuntimeException {
try {
return new MethodParameter(Sample.class.getMethod(methodName, args), 0);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
static class SpecificBinding extends QuerydslBindings {
public SpecificBinding() {
bind("firstname", new QuerydslBinding<StringPath>() {
@Override
public Predicate bind(StringPath path, Object value) {
return path.eq(value.toString().toUpperCase());
}
});
bind(QUser.user.lastname, new QuerydslBinding<StringPath>() {
@Override
public Predicate bind(StringPath path, Object value) {
return path.toLowerCase().eq(value.toString());
}
});
excluding("address");
}
}
static interface Sample {
User predicateWithoutAnnotation(Predicate predicate);
User nonPredicateWithAnnotation(@QuerydslPredicate String predicate);
User nonPredicateWithoutAnnotation(String predicate);
User simpleFind(@QuerydslPredicate Predicate predicate);
Page<User> pagedFind(@QuerydslPredicate Predicate predicate, Pageable page);
User specificFind(@QuerydslPredicate(bindings = SpecificBinding.class) Predicate predicate);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.web.querydsl;
import static org.hamcrest.core.Is.*;
import static org.junit.Assert.*;
import java.util.Collections;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.data.querydsl.QUser;
import org.springframework.data.querydsl.User;
import org.springframework.data.util.ClassTypeInformation;
import com.mysema.query.BooleanBuilder;
import com.mysema.query.types.Predicate;
/**
* @author Christoph Strobl
*/
public class QuerydslPredicateBuilderUnitTests {
QuerydslPredicateBuilder builder;
@Before
public void setUp() {
builder = new QuerydslPredicateBuilder();
}
/**
* @see DATACMNS-669
*/
@Test(expected = IllegalArgumentException.class)
public void getPredicateShouldThrowErrorWhenBindingContextIsNull() {
builder.getPredicate(new MutablePropertyValues(), null);
}
/**
* @see DATACMNS-669
*/
@Test
public void getPredicateShouldReturnEmptyPredicateWhenPropertiesAreEmpty() {
assertThat(builder.getPredicate(new MutablePropertyValues(), new QuerydslBindingContext(
ClassTypeInformation.OBJECT, null, null)), is((Predicate) new BooleanBuilder()));
}
/**
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldCreateSingleStringParameterPredicateCorrectly() throws Exception {
Predicate predicate = builder.getPredicate(
new MutablePropertyValues(Collections.singletonMap("firstname", new String[] { "rand" })),
new QuerydslBindingContext(ClassTypeInformation.from(User.class), null, null));
assertThat(predicate, Is.<Object> is(QUser.user.firstname.eq("rand")));
}
}