/*
 * Copyright 2015-2020 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
 *
 *      https://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 java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.PropertyValues;
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.querydsl.EntityPathResolver;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;

Builder assembling Predicate out of PropertyValues.
Author:Christoph Strobl, Oliver Gierke, Mark Paluch
Since:1.11
/** * Builder assembling {@link Predicate} out of {@link PropertyValues}. * * @author Christoph Strobl * @author Oliver Gierke * @author Mark Paluch * @since 1.11 */
public class QuerydslPredicateBuilder { private final ConversionService conversionService; private final MultiValueBinding<Path<? extends Object>, Object> defaultBinding; private final Map<PathInformation, Path<?>> paths; private final EntityPathResolver resolver;
Params:
  • conversionService – must not be null.
  • resolver – can be null.
/** * Creates a new {@link QuerydslPredicateBuilder} for the given {@link ConversionService} and * {@link EntityPathResolver}. * * @param conversionService must not be {@literal null}. * @param resolver can be {@literal null}. */
public QuerydslPredicateBuilder(ConversionService conversionService, EntityPathResolver resolver) { Assert.notNull(conversionService, "ConversionService must not be null!"); this.defaultBinding = new QuerydslDefaultBinding(); this.conversionService = conversionService; this.paths = new ConcurrentHashMap<>(); this.resolver = resolver; }
Creates a Querydsl Predicate for the given values, QuerydslBindings on the given TypeInformation.
Params:
  • type – the type to create a predicate for.
  • values – the values to bind.
  • bindings – the QuerydslBindings for the predicate.
Returns:
/** * Creates a Querydsl {@link Predicate} for the given values, {@link QuerydslBindings} on the given * {@link TypeInformation}. * * @param type the type to create a predicate for. * @param values the values to bind. * @param bindings the {@link QuerydslBindings} for the predicate. * @return */
@Nullable public Predicate getPredicate(TypeInformation<?> type, MultiValueMap<String, String> values, QuerydslBindings bindings) { Assert.notNull(bindings, "Context must not be null!"); BooleanBuilder builder = new BooleanBuilder(); if (values.isEmpty()) { return builder.getValue(); } for (Entry<String, List<String>> entry : values.entrySet()) { if (isSingleElementCollectionWithoutText(entry.getValue())) { continue; } String path = entry.getKey(); if (!bindings.isPathAvailable(path, type)) { continue; } PathInformation propertyPath = bindings.getPropertyPath(path, type); if (propertyPath == null) { continue; } Collection<Object> value = convertToPropertyPathSpecificType(entry.getValue(), propertyPath); Optional<Predicate> predicate = invokeBinding(propertyPath, bindings, value); predicate.ifPresent(builder::and); } return builder.getValue(); }
Invokes the binding of the given values, for the given PropertyPath and QuerydslBindings.
Params:
  • dotPath – must not be null.
  • bindings – must not be null.
  • values – must not be null.
Returns:
/** * Invokes the binding of the given values, for the given {@link PropertyPath} and {@link QuerydslBindings}. * * @param dotPath must not be {@literal null}. * @param bindings must not be {@literal null}. * @param values must not be {@literal null}. * @return */
private Optional<Predicate> invokeBinding(PathInformation dotPath, QuerydslBindings bindings, Collection<Object> values) { Path<?> path = getPath(dotPath, bindings); return bindings.getBindingForPath(dotPath).orElse(defaultBinding).bind(path, values); }
Returns the Path for the given PropertyPath and QuerydslBindings. Will try to obtain the Path from the bindings first but fall back to reifying it from the PropertyPath in case no specific binding has been configured.
Params:
  • path – must not be null.
  • bindings – must not be null.
Returns:
/** * Returns the {@link Path} for the given {@link PropertyPath} and {@link QuerydslBindings}. Will try to obtain the * {@link Path} from the bindings first but fall back to reifying it from the PropertyPath in case no specific binding * has been configured. * * @param path must not be {@literal null}. * @param bindings must not be {@literal null}. * @return */
private Path<?> getPath(PathInformation path, QuerydslBindings bindings) { Optional<Path<?>> resolvedPath = bindings.getExistingPath(path); return resolvedPath.orElseGet(() -> paths.computeIfAbsent(path, it -> it.reifyPath(resolver))); }
Converts the given source values into a collection of elements that are of the given PropertyPath's type. Considers a single element list with an empty String an empty collection because this basically indicates the property having been submitted but no value provided.
Params:
  • source – must not be null.
  • path – must not be null.
Returns:
/** * Converts the given source values into a collection of elements that are of the given {@link PropertyPath}'s type. * Considers a single element list with an empty {@link String} an empty collection because this basically indicates * the property having been submitted but no value provided. * * @param source must not be {@literal null}. * @param path must not be {@literal null}. * @return */
private Collection<Object> convertToPropertyPathSpecificType(List<String> source, PathInformation path) { Class<?> targetType = path.getLeafType(); if (source.isEmpty() || isSingleElementCollectionWithoutText(source)) { return Collections.emptyList(); } Collection<Object> target = new ArrayList<>(source.size()); for (String value : source) { target.add(conversionService.canConvert(String.class, targetType) ? conversionService.convert(value, TypeDescriptor.forObject(value), getTargetTypeDescriptor(path)) : value); } return target; }
Returns the target TypeDescriptor for the given PathInformation by either inspecting the field or property (the latter preferred) to pick up annotations potentially defined for formatting purposes.
Params:
  • path – must not be null.
Returns:
/** * Returns the target {@link TypeDescriptor} for the given {@link PathInformation} by either inspecting the field or * property (the latter preferred) to pick up annotations potentially defined for formatting purposes. * * @param path must not be {@literal null}. * @return */
private static TypeDescriptor getTargetTypeDescriptor(PathInformation path) { PropertyDescriptor descriptor = path.getLeafPropertyDescriptor(); Class<?> owningType = path.getLeafParentType(); String leafProperty = path.getLeafProperty(); TypeDescriptor result = descriptor == null // ? TypeDescriptor .nested(org.springframework.data.util.ReflectionUtils.findRequiredField(owningType, leafProperty), 0) : TypeDescriptor .nested(new Property(owningType, descriptor.getReadMethod(), descriptor.getWriteMethod(), leafProperty), 0); if (result == null) { throw new IllegalStateException(String.format("Could not obtain TypeDesciptor for PathInformation %s!", path)); } return result; }
Returns whether the given collection has exactly one element that doesn't contain any text. This is basically an indicator that a request parameter has been submitted but no value for it.
Params:
  • source – must not be null.
Returns:
/** * Returns whether the given collection has exactly one element that doesn't contain any text. This is basically an * indicator that a request parameter has been submitted but no value for it. * * @param source must not be {@literal null}. * @return */
private static boolean isSingleElementCollectionWithoutText(List<String> source) { return source.size() == 1 && !StringUtils.hasLength(source.get(0)); } }