DATACMNS-787 - QuerydsBindings now allows aliasing of paths.

If a single path is now handed to QuerydslBindings.bind(…), a specialized AliasingPathBinder is returned which allows defining an alias the binding will be exposed under.

By default that alias will cause the original binding being black-listed and thus become unavailable for binding. This can be overridden by explicitly white-listing the properties to be bindable.
This commit is contained in:
Oliver Gierke
2015-11-23 13:07:19 +01:00
parent 60794a8923
commit 3ed2f44e60
4 changed files with 279 additions and 79 deletions

View File

@@ -25,6 +25,9 @@ import java.util.Map;
import java.util.Set;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -33,29 +36,37 @@ import com.querydsl.core.types.PathMetadata;
import com.querydsl.core.types.Predicate;
/**
* {@link QuerydslBindings} allows definition of path specific {@link SingleValueBinding}.
* {@link QuerydslBindings} allows definition of path specific bindings.
*
* <pre>
* <code>
* new QuerydslBindings() {
* {
* bind(QUser.user.address.city).using((path, value) -> path.like(value.toString()));
* bind(String.class).using((path, value) -> path.like(value.toString()));
* bind(QUser.user.address.city).first((path, value) -> path.like(value.toString()));
* bind(String.class).first((path, value) -> path.like(value.toString()));
* }
* }
* </code>
* </pre>
*
* The bindings can either handle a single - see {@link PathBinder#first(SingleValueBinding)} - (the first in case
* multiple ones are supplied) or multiple - see {@link PathBinder#all(MultiValueBinding)} - value binding. If exactly
* one path is deployed, an {@link AliasingPathBinder} is returned which - as the name suggests - allows aliasing of
* paths, i.e. exposing the path under a different name.
* <p>
* {@link QuerydslBindings} are usually manipulated using a {@link QuerydslBinderCustomizer}, either implemented
* directly or using a default method on a Spring Data repository.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.11
* @see QuerydslBinderCustomizer
*/
public class QuerydslBindings {
private final Map<String, PathAndBinding<?, ?>> pathSpecs;
private final Map<Class<?>, PathAndBinding<?, ?>> typeSpecs;
private final Set<String> whiteList;
private final Set<String> blackList;
private final Set<String> whiteList, blackList, aliases;
private boolean excludeUnlistedProperties;
@@ -68,6 +79,18 @@ public class QuerydslBindings {
this.typeSpecs = new LinkedHashMap<Class<?>, PathAndBinding<?, ?>>();
this.whiteList = new HashSet<String>();
this.blackList = new HashSet<String>();
this.aliases = new HashSet<String>();
}
/**
* Returns an {@link AliasingPathBinder} for the given {@link Path} to define bindings for them.
*
* @param path must not be {@literal null}.
* @return
*/
public final <T extends Path<S>, S> AliasingPathBinder<T, S> bind(T path) {
return new AliasingPathBinder<T, S>(path);
}
/**
@@ -135,30 +158,8 @@ public class QuerydslBindings {
return this;
}
/**
* Checks if a given {@link PropertyPath} should be visible for binding values.
*
* @param path
* @return
*/
boolean isPathVisible(PropertyPath path) {
List<String> segments = Arrays.asList(path.toDotPath().split("\\."));
for (int i = 1; i <= segments.size(); i++) {
if (!isPathVisible(StringUtils.collectionToDelimitedString(segments.subList(0, i), "."))) {
// check if full path is on whitelist if though partial one is not
if (!whiteList.isEmpty()) {
return whiteList.contains(path.toDotPath());
}
return false;
}
}
return true;
public boolean isPathVisible(String path, Class<?> type) {
return getPropertyPath(path, ClassTypeInformation.from(type)) != null;
}
/**
@@ -166,17 +167,22 @@ public class QuerydslBindings {
* specific path but falls back to the builder registered for a given type.
*
* @param path must not be {@literal null}.
* @return
* @return can be {@literal null}.
*/
@SuppressWarnings("unchecked")
public <S extends Path<T>, T> MultiValueBinding<S, T> getBindingForPath(PropertyPath path) {
public <S extends Path<? extends T>, T> MultiValueBinding<S, T> getBindingForPath(PropertyPath path) {
Assert.notNull(path, "PropertyPath must not be null!");
PathAndBinding<S, T> pathAndBinding = (PathAndBinding<S, T>) pathSpecs.get(path.toDotPath());
if (pathAndBinding != null) {
return pathAndBinding.getBinding();
MultiValueBinding<S, T> binding = pathAndBinding.getBinding();
if (binding != null) {
return pathAndBinding.getBinding();
}
}
pathAndBinding = (PathAndBinding<S, T>) typeSpecs.get(path.getLeafProperty().getType());
@@ -197,24 +203,73 @@ public class QuerydslBindings {
}
/**
* Returns whether the given path is visible, which means either on the white list or not on the black list if no
* white list configured.
* @param path
* @param type
* @return
*/
PropertyPath getPropertyPath(String path, TypeInformation<?> type) {
if (!isPathVisible(path)) {
return null;
}
if (pathSpecs.containsKey(path)) {
return PropertyPath.from(toDotPath(pathSpecs.get(path).getPath()), type);
}
try {
PropertyPath propertyPath = PropertyPath.from(path, type);
return isPathVisible(propertyPath) ? propertyPath : null;
} catch (PropertyReferenceException o_O) {
return null;
}
}
/**
* Checks if a given {@link PropertyPath} should be visible for binding values.
*
* @param path
* @return
*/
private boolean isPathVisible(PropertyPath path) {
List<String> segments = Arrays.asList(path.toDotPath().split("\\."));
for (int i = 1; i <= segments.size(); i++) {
if (!isPathVisible(StringUtils.collectionToDelimitedString(segments.subList(0, i), "."))) {
// check if full path is on whitelist although the partial one is not
if (!whiteList.isEmpty()) {
return whiteList.contains(path.toDotPath());
}
return false;
}
}
return true;
}
/**
* Returns whether the given path is visible, which means either an alias and not explicitly blacklisted, explicitly
* white listed or not on the black list if no white list configured.
*
* @param path must not be {@literal null}.
* @return
*/
private boolean isPathVisible(String path) {
if (!whiteList.isEmpty()) {
if (whiteList.contains(path)) {
return true;
}
return false;
// Aliases are visible if not explicitly blacklisted
if (aliases.contains(path) && !blackList.contains(path)) {
return true;
}
return excludeUnlistedProperties ? false : !blackList.contains(path);
if (whiteList.isEmpty()) {
return excludeUnlistedProperties ? false : !blackList.contains(path);
}
return whiteList.contains(path);
}
/**
@@ -223,7 +278,7 @@ public class QuerydslBindings {
* @param path can be {@literal null}.
* @return
*/
private String toDotPath(Path<?> path) {
private static String toDotPath(Path<?> path) {
if (path == null) {
return "";
@@ -239,7 +294,7 @@ public class QuerydslBindings {
*
* @author Oliver Gierke
*/
public final class PathBinder<P extends Path<? extends T>, T> {
public class PathBinder<P extends Path<? extends T>, T> {
private final List<P> paths;
@@ -248,7 +303,7 @@ public class QuerydslBindings {
*
* @param paths must not be {@literal null} or empty.
*/
public PathBinder(P... paths) {
PathBinder(P... paths) {
Assert.notEmpty(paths, "At least one path has to be provided!");
this.paths = Arrays.asList(paths);
@@ -278,7 +333,86 @@ public class QuerydslBindings {
Assert.notNull(binding, "Binding must not be null!");
for (P path : paths) {
QuerydslBindings.this.pathSpecs.put(toDotPath(path), new PathAndBinding<P, T>(path, binding));
registerBinding(new PathAndBinding<P, T>(path, binding));
}
}
protected void registerBinding(PathAndBinding<P, T> binding) {
QuerydslBindings.this.pathSpecs.put(toDotPath(binding.getPath()), binding);
}
}
/**
* A special {@link PathBinder} that additionally registers the binding under a dedicated alias. The original path is
* still registered but blacklisted so that it becomes unavailable except it's explicitly whitelisted.
*
* @author Oliver Gierke
*/
public class AliasingPathBinder<P extends Path<? extends T>, T> extends PathBinder<P, T> {
private final String alias;
private final P path;
/**
* Creates a new {@link AliasingPathBinder} for the given {@link Path}.
*
* @param paths must not be {@literal null}.
*/
AliasingPathBinder(P path) {
this(null, path);
}
/**
* Creates a new {@link AliasingPathBinder} using the given alias and {@link Path}.
*
* @param alias can be {@literal null}.
* @param path must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private AliasingPathBinder(String alias, P path) {
super(path);
Assert.notNull(path, "Path must not be null!");
this.alias = alias;
this.path = path;
}
/**
* Aliases the current binding to be available under the given path. By default, the binding path will be
* blacklisted so that aliasing effectively hides the original path. If you want to keep the original path around,
* include it in an explicit whitelist.
*
* @param alias must not be {@literal null}.
* @return will never be {@literal null}.
*/
public AliasingPathBinder<P, T> as(String alias) {
Assert.hasText(alias, "Alias must not be null or empty!");
return new AliasingPathBinder<P, T>(alias, path);
}
/**
* Registers the current aliased binding to use the default binding.
*/
public void withDefaultBinding() {
registerBinding(new PathAndBinding<P, T>(path, null));
}
/*
* (non-Javadoc)
* @see org.springframework.data.querydsl.binding.QuerydslBindings.PathBinder#registerBinding(org.springframework.data.querydsl.binding.QuerydslBindings.PathAndBinding)
*/
@Override
protected void registerBinding(PathAndBinding<P, T> binding) {
super.registerBinding(binding);
if (alias != null) {
QuerydslBindings.this.pathSpecs.put(alias, binding);
QuerydslBindings.this.aliases.add(alias);
QuerydslBindings.this.blackList.add(toDotPath(binding.getPath()));
}
}
}

View File

@@ -31,7 +31,6 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.Property;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@@ -100,22 +99,23 @@ public class QuerydslPredicateBuilder {
continue;
}
try {
String path = entry.getKey();
PropertyPath propertyPath = PropertyPath.from(entry.getKey(), type);
if (!bindings.isPathVisible(path, type.getType())) {
continue;
}
if (bindings.isPathVisible(propertyPath)) {
PropertyPath propertyPath = bindings.getPropertyPath(path, type);
Collection<Object> value = convertToPropertyPathSpecificType(entry.getValue(), propertyPath);
if (propertyPath == null) {
continue;
}
Predicate predicate = invokeBinding(propertyPath, bindings, value);
Collection<Object> value = convertToPropertyPathSpecificType(entry.getValue(), propertyPath);
Predicate predicate = invokeBinding(propertyPath, bindings, value);
if (predicate != null) {
builder.and(predicate);
}
}
} catch (PropertyReferenceException o_O) {
// not a property of the domain object, continue
if (predicate != null) {
builder.and(predicate);
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.querydsl.QUser;
import org.springframework.data.querydsl.SimpleEntityPathResolver;
import org.springframework.data.querydsl.User;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.test.util.ReflectionTestUtils;
import com.querydsl.core.types.Path;
@@ -128,7 +129,7 @@ public class QuerydslBindingsUnitTests {
bindings.bind(String.class).first(CONTAINS_BINDING);
assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true));
assertThat(bindings.isPathVisible("inceptionYear", User.class), is(true));
}
/**
@@ -139,7 +140,7 @@ public class QuerydslBindingsUnitTests {
bindings.including(QUser.user.inceptionYear);
assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(true));
assertThat(bindings.isPathVisible("inceptionYear", User.class), is(true));
}
/**
@@ -150,7 +151,7 @@ public class QuerydslBindingsUnitTests {
bindings.including(QUser.user.inceptionYear);
assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(false));
assertThat(bindings.isPathVisible("firstname", User.class), is(false));
}
/**
@@ -161,7 +162,7 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.inceptionYear);
assertThat(bindings.isPathVisible(PropertyPath.from("inceptionYear", User.class)), is(false));
assertThat(bindings.isPathVisible("inceptionYear", User.class), is(false));
}
/**
@@ -172,7 +173,7 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.inceptionYear);
assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(true));
assertThat(bindings.isPathVisible("firstname", User.class), is(true));
}
/**
@@ -184,7 +185,7 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.firstname);
bindings.including(QUser.user.firstname);
assertThat(bindings.isPathVisible(PropertyPath.from("firstname", User.class)), is(true));
assertThat(bindings.isPathVisible("firstname", User.class), is(true));
}
/**
@@ -195,7 +196,7 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.address);
assertThat(bindings.isPathVisible(PropertyPath.from("address.city", User.class)), is(false));
assertThat(bindings.isPathVisible("address.city", User.class), is(false));
}
/**
@@ -207,7 +208,7 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.address);
bindings.including(QUser.user.address.city);
assertThat(bindings.isPathVisible(PropertyPath.from("address.city", User.class)), is(true));
assertThat(bindings.isPathVisible("address.city", User.class), is(true));
}
/**
@@ -219,29 +220,94 @@ public class QuerydslBindingsUnitTests {
bindings.excluding(QUser.user.address);
bindings.including(QUser.user.address.city);
assertThat(bindings.isPathVisible(PropertyPath.from("address.street", User.class)), is(false));
assertThat(bindings.isPathVisible("address.street", User.class), is(false));
}
/**
* @see DATACMNS-669
*/
@Test
public void testname() {
PropertyPath firstname = PropertyPath.from("firstname", User.class);
PropertyPath lastname = PropertyPath.from("lastname", User.class);
PropertyPath city = PropertyPath.from("address.city", User.class);
PropertyPath street = PropertyPath.from("address.street", User.class);
public void whitelistsPropertiesCorrectly() {
bindings.including(QUser.user.firstname, QUser.user.address.street);
assertThat(bindings.isPathVisible(firstname), is(true));
assertThat(bindings.isPathVisible(street), is(true));
assertThat(bindings.isPathVisible(lastname), is(false));
assertThat(bindings.isPathVisible(city), is(false));
assertThat(bindings.isPathVisible("firstname", User.class), is(true));
assertThat(bindings.isPathVisible("address.street", User.class), is(true));
assertThat(bindings.isPathVisible("lastname", User.class), is(false));
assertThat(bindings.isPathVisible("address.city", User.class), is(false));
}
private static <P extends Path<S>, S> void assertAdapterWithTargetBinding(MultiValueBinding<P, S> binding,
/**
* @see DATACMNS-787
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsNullAlias() {
bindings.bind(QUser.user.address).as(null);
}
/**
* @see DATACMNS-787
*/
@Test(expected = IllegalArgumentException.class)
public void rejectsEmptyAlias() {
bindings.bind(QUser.user.address).as("");
}
/**
* @see DATACMNS-787
*/
@Test
public void aliasesBinding() {
bindings.bind(QUser.user.address.city).as("city").first(CONTAINS_BINDING);
PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class));
assertThat(path, is(notNullValue()));
assertThat(bindings.isPathVisible("city", User.class), is(true));
// Aliasing implicitly blacklists original path
assertThat(bindings.isPathVisible("address.city", User.class), is(false));
}
/**
* @see DATACMNS-787
*/
@Test
public void explicitlyIncludesOriginalBindingDespiteAlias() {
bindings.including(QUser.user.address.city);
bindings.bind(QUser.user.address.city).as("city").first(CONTAINS_BINDING);
PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class));
assertThat(path, is(notNullValue()));
assertThat(bindings.isPathVisible("city", User.class), is(true));
assertThat(bindings.isPathVisible("address.city", User.class), is(true));
PropertyPath propertyPath = bindings.getPropertyPath("address.city", ClassTypeInformation.from(User.class));
assertThat(propertyPath, is(notNullValue()));
assertAdapterWithTargetBinding(bindings.getBindingForPath(propertyPath), CONTAINS_BINDING);
}
/**
* @see DATACMNS-787
*/
@Test
public void registedAliasWithNullBinding() {
bindings.bind(QUser.user.address.city).as("city").withDefaultBinding();
PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class));
assertThat(path, is(notNullValue()));
MultiValueBinding<Path<? extends Object>, Object> binding = bindings.getBindingForPath(path);
assertThat(binding, is(nullValue()));
}
private static <P extends Path<? extends S>, S> void assertAdapterWithTargetBinding(MultiValueBinding<P, S> binding,
SingleValueBinding<? extends Path<?>, ?> expected) {
assertThat(binding, is(instanceOf(QuerydslBindings.MultiValueBindingAdapter.class)));

View File

@@ -168,7 +168,7 @@ public class QuerydslPredicateArgumentResolverUnitTests {
* @see DATACMNS-669
*/
@Test
public void resolveArgumentShouldHonorCustomSpeficifcation() throws Exception {
public void resolveArgumentShouldHonorCustomSpecification() throws Exception {
request.addParameter("firstname", "egwene");
request.addParameter("lastname", "al'vere");