/*
* Copyright 2015-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.
* 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.binding;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
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.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.querydsl.core.types.Path;
/**
* {@link QuerydslBindings} allows definition of path specific bindings.
*
* <pre>
* <code>
* new QuerydslBindings() {
* {
* 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, blackList, aliases;
private boolean excludeUnlistedProperties;
/**
* Creates a new {@link QuerydslBindings} instance.
*/
public QuerydslBindings() {
this.pathSpecs = new LinkedHashMap<>();
this.typeSpecs = new LinkedHashMap<>();
this.whiteList = new HashSet<>();
this.blackList = new HashSet<>();
this.aliases = new HashSet<>();
}
/**
* 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<>(path);
}
/**
* Returns a new {@link PathBinder} for the given {@link Path}s to define bindings for them.
*
* @param paths must not be {@literal null} or empty.
* @return
*/
@SafeVarargs
public final <T extends Path<S>, S> PathBinder<T, S> bind(T... paths) {
return new PathBinder<>(paths);
}
/**
* Returns a new {@link TypeBinder} for the given type.
*
* @param type must not be {@literal null}.
* @return
*/
public final <T> TypeBinder<T> bind(Class<T> type) {
return new TypeBinder<>(type);
}
/**
* Exclude properties from binding. Exclusion of all properties of a nested type can be done by exclusion on a higher
* level. E.g. {@code address} would exclude both {@code address.city} and {@code address.street}.
*
* @param paths must not be {@literal null} or empty.
*/
public final void excluding(Path<?>... paths) {
Assert.notEmpty(paths, "At least one path has to be provided!");
for (Path<?> path : paths) {
this.blackList.add(toDotPath(Optional.of(path)));
}
}
/**
* Include properties for binding. Include the property considered a binding candidate.
*
* @param properties must not be {@literal null} or empty.
*/
public final void including(Path<?>... paths) {
Assert.notEmpty(paths, "At least one path has to be provided!");
for (Path<?> path : paths) {
this.whiteList.add(toDotPath(Optional.of(path)));
}
}
/**
* Returns whether to exclude all properties for which no explicit binding has been defined or it has been explicitly
* white-listed. This defaults to {@literal false} which means that for properties without an explicitly defined
* binding a type specific default binding will be applied.
*
* @param excludeUnlistedProperties
* @return
* @see #including(String...)
* @see #including(Path...)
*/
public final QuerydslBindings excludeUnlistedProperties(boolean excludeUnlistedProperties) {
this.excludeUnlistedProperties = excludeUnlistedProperties;
return this;
}
/**
* Returns whether the given path is available on the given type.
*
* @param path must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
*/
boolean isPathAvailable(String path, Class<?> type) {
Assert.notNull(path, "Path must not be null!");
Assert.notNull(type, "Type must not be null!");
return isPathAvailable(path, ClassTypeInformation.from(type));
}
/**
* Returns whether the given path is available on the given type.
*
* @param path must not be {@literal null}.
* @param type
* @return
*/
boolean isPathAvailable(String path, TypeInformation<?> type) {
Assert.notNull(path, "Path must not be null!");
Assert.notNull(type, "Type must not be null!");
return getPropertyPath(path, type) != null;
}
/**
* Returns the {@link SingleValueBinding} for the given {@link PropertyPath}. Prefers a path configured for the
* specific path but falls back to the builder registered for a given type.
*
* @param path must not be {@literal null}.
* @return can be {@literal null}.
*/
@SuppressWarnings("unchecked")
public <S extends Path<? extends T>, T> Optional<MultiValueBinding<S, T>> getBindingForPath(PathInformation path) {
Assert.notNull(path, "PropertyPath must not be null!");
PathAndBinding<S, T> pathAndBinding = (PathAndBinding<S, T>) pathSpecs.get(path.toDotPath());
if (pathAndBinding != null) {
Optional<MultiValueBinding<S, T>> binding = pathAndBinding.getBinding();
if (binding.isPresent()) {
return binding;
}
}
pathAndBinding = (PathAndBinding<S, T>) typeSpecs.get(path.getLeafType());
return pathAndBinding == null ? Optional.empty() : pathAndBinding.getBinding();
}
/**
* Returns a {@link Path} for the {@link PropertyPath} instance.
*
* @param path must not be {@literal null}.
* @return
*/
Optional<Path<?>> getExistingPath(PathInformation path) {
Assert.notNull(path, "PropertyPath must not be null!");
return Optional.ofNullable(pathSpecs.get(path.toDotPath())).flatMap(PathAndBinding::getPath);
}
/**
* Returns the {@link PathInformation} for the given path and {@link TypeInformation}.
*
* @param path must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
*/
PathInformation getPropertyPath(String path, TypeInformation<?> type) {
Assert.notNull(path, "Path must not be null!");
Assert.notNull(type, "Type information must not be null!");
if (!isPathVisible(path)) {
return null;
}
if (pathSpecs.containsKey(path)) {
return pathSpecs.get(path).getPath()//
.map(QuerydslPathInformation::of)//
.orElse(null);
}
try {
PathInformation propertyPath = PropertyPathInformation.of(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(PathInformation 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) {
// Aliases are visible if not explicitly blacklisted
if (aliases.contains(path) && !blackList.contains(path)) {
return true;
}
if (whiteList.isEmpty()) {
return excludeUnlistedProperties ? false : !blackList.contains(path);
}
return whiteList.contains(path);
}
/**
* Returns the property path for the given {@link Path}.
*
* @param path can be {@literal null}.
* @return
*/
private static String toDotPath(Optional<Path<?>> path) {
return path.map(it -> it.toString().substring(it.getMetadata().getRootPath().getMetadata().getName().length() + 1))
.orElse("");
}
/**
* A binder for {@link Path}s.
*
* @author Oliver Gierke
*/
public class PathBinder<P extends Path<? extends T>, T> {
private final List<P> paths;
/**
* Creates a new {@link PathBinder} for the given {@link Path}s.
*
* @param paths must not be {@literal null} or empty.
*/
@SafeVarargs
PathBinder(P... paths) {
Assert.notEmpty(paths, "At least one path has to be provided!");
this.paths = Arrays.asList(paths);
}
/**
* Defines the given {@link SingleValueBinding} to be used for the paths.
*
* @param binding must not be {@literal null}.
* @return
*/
public void firstOptional(OptionalValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
all((path, value) -> binding.bind(path, Optionals.next(value.iterator())));
}
public void first(SingleValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
all((path, value) -> Optionals.next(value.iterator()).map(t -> binding.bind(path, t)));
}
/**
* Defines the given {@link MultiValueBinding} to be used for the paths.
*
* @param binding must not be {@literal null}.
* @return
*/
public void all(MultiValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
paths.forEach(path -> registerBinding(PathAndBinding.withPath(path).with(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}.
*/
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<>(alias, path);
}
/**
* Registers the current aliased binding to use the default binding.
*/
public void withDefaultBinding() {
registerBinding(PathAndBinding.withPath(path));
}
/*
* (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()));
}
}
}
/**
* A binder for types.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
public final class TypeBinder<T> {
private final @NonNull Class<T> type;
/**
* Configures the given {@link SingleValueBinding} to be used for the current type.
*
* @param binding must not be {@literal null}.
*/
public <P extends Path<T>> void firstOptional(OptionalValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
all((MultiValueBinding<P, T>) (path, value) -> binding.bind(path, Optionals.next(value.iterator())));
}
public <P extends Path<T>> void first(SingleValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
all((MultiValueBinding<P, T>) (path, value) -> Optionals.next(value.iterator()).map(t -> binding.bind(path, t)));
}
/**
* Configures the given {@link MultiValueBinding} to be used for the current type.
*
* @param binding must not be {@literal null}.
*/
public <P extends Path<T>> void all(MultiValueBinding<P, T> binding) {
Assert.notNull(binding, "Binding must not be null!");
QuerydslBindings.this.typeSpecs.put(type, PathAndBinding.<T, P> withoutPath().with(binding));
}
}
/**
* A pair of a {@link Path} and the registered {@link MultiValueBinding}.
*
* @author Christoph Strobl
* @author Oliver Gierke
* @since 1.11
*/
@Value
private static class PathAndBinding<P extends Path<? extends T>, T> {
@NonNull Optional<Path<?>> path;
@NonNull Optional<MultiValueBinding<P, T>> binding;
public static <T, P extends Path<? extends T>> PathAndBinding<P, T> withPath(P path) {
return new PathAndBinding<>(Optional.of(path), Optional.empty());
}
public static <T, S extends Path<? extends T>> PathAndBinding<S, T> withoutPath() {
return new PathAndBinding<>(Optional.empty(), Optional.empty());
}
public PathAndBinding<P, T> with(MultiValueBinding<P, T> binding) {
return new PathAndBinding<>(path, Optional.of(binding));
}
}
}