/* * Copyright 2014-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.repository.query; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import org.springframework.beans.BeanUtils; import org.springframework.data.repository.query.EvaluationContextExtensionInformation.ExtensionTypeInformation.PublicMethodAndFieldFilter; import org.springframework.data.repository.query.spi.EvaluationContextExtension; import org.springframework.data.repository.query.spi.Function; import org.springframework.data.util.Streamable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.FieldFilter; import org.springframework.util.ReflectionUtils.MethodFilter; /** * Inspects the configured {@link EvaluationContextExtension} type for static methods and fields to avoid repeated * reflection lookups. Also inspects the return type of the {@link EvaluationContextExtension#getRootObject()} method * and captures the methods declared on it as {@link Function}s. * <p> * The type basically allows us to cache the type based information within * {@link ExtensionAwareEvaluationContextProvider} to avoid repeated reflection lookups for every creation of an * {@link org.springframework.expression.EvaluationContext}. * * @author Oliver Gierke * @author Christoph Strobl * @since 1.9 */ class EvaluationContextExtensionInformation { private final ExtensionTypeInformation extensionTypeInformation; private final Optional<RootObjectInformation> rootObjectInformation; /** * Creates a new {@link EvaluationContextExtension} for the given extension type. * * @param type must not be {@literal null}. */ public EvaluationContextExtensionInformation(Class<? extends EvaluationContextExtension> type) { Assert.notNull(type, "Extension type must not be null!"); Class<?> rootObjectType = getRootObjectMethod(type).getReturnType(); this.rootObjectInformation = Optional .ofNullable(Object.class.equals(rootObjectType) ? null : new RootObjectInformation(rootObjectType)); this.extensionTypeInformation = new ExtensionTypeInformation(type); } /** * Returns the {@link ExtensionTypeInformation} for the extension. * * @return */ public ExtensionTypeInformation getExtensionTypeInformation() { return this.extensionTypeInformation; } /** * Returns the {@link RootObjectInformation} for the given target object. If the information has been pre-computed * earlier, the existing one will be used. * * @param target * @return */ public RootObjectInformation getRootObjectInformation(Optional<Object> target) { return target.map(it -> rootObjectInformation.orElseGet(() -> new RootObjectInformation(it.getClass()))) .orElse(RootObjectInformation.NONE); } private static Method getRootObjectMethod(Class<?> type) { try { return type.getMethod("getRootObject"); } catch (Exception e) { return null; } } /** * Static information about the given {@link EvaluationContextExtension} type. Discovers public static methods and * fields. The fields' values are obtained directly, the methods are exposed {@link Function} invocations. * * @author Oliver Gierke */ @Getter public static class ExtensionTypeInformation { /** * The statically defined properties of the extension type. * * @return the properties will never be {@literal null}. */ private final Map<String, Object> properties; /** * The statically exposed functions of the extension type. * * @return the functions will never be {@literal null}. */ private final Map<String, Function> functions; /** * Creates a new {@link ExtensionTypeInformation} fir the given type. * * @param type must not be {@literal null}. */ public ExtensionTypeInformation(Class<? extends EvaluationContextExtension> type) { Assert.notNull(type, "Extension type must not be null!"); this.functions = discoverDeclaredFunctions(type); this.properties = discoverDeclaredProperties(type); } private static Map<String, Function> discoverDeclaredFunctions(Class<?> type) { Map<String, Function> map = new HashMap<>(); ReflectionUtils.doWithMethods(type, // method -> map.put(method.getName(), new Function(method, null)), // PublicMethodAndFieldFilter.STATIC); return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map); } @RequiredArgsConstructor static class PublicMethodAndFieldFilter implements MethodFilter, FieldFilter { public static final PublicMethodAndFieldFilter STATIC = new PublicMethodAndFieldFilter(true); public static final PublicMethodAndFieldFilter NON_STATIC = new PublicMethodAndFieldFilter(false); private final boolean staticOnly; /* * (non-Javadoc) * @see org.springframework.util.ReflectionUtils.MethodFilter#matches(java.lang.reflect.Method) */ @Override public boolean matches(Method method) { if (ReflectionUtils.isObjectMethod(method)) { return false; } boolean methodStatic = Modifier.isStatic(method.getModifiers()); boolean staticMatch = staticOnly ? methodStatic : !methodStatic; return Modifier.isPublic(method.getModifiers()) && staticMatch; } /* * (non-Javadoc) * @see org.springframework.util.ReflectionUtils.FieldFilter#matches(java.lang.reflect.Field) */ @Override public boolean matches(Field field) { boolean fieldStatic = Modifier.isStatic(field.getModifiers()); boolean staticMatch = staticOnly ? fieldStatic : !fieldStatic; return Modifier.isPublic(field.getModifiers()) && staticMatch; } } } /** * Information about the root object of an extension. * * @author Oliver Gierke */ static class RootObjectInformation { private static final RootObjectInformation NONE = new RootObjectInformation(Object.class); private final Map<String, Method> accessors; private final Collection<Method> methods; private final Collection<Field> fields; /** * Creates a new {@link RootObjectInformation} for the given type. Inspects public methods and fields to register * them as {@link Function}s and properties. * * @param type must not be {@literal null}. */ public RootObjectInformation(Class<?> type) { Assert.notNull(type, "Type must not be null!"); this.accessors = new HashMap<>(); this.methods = new HashSet<>(); this.fields = new ArrayList<>(); if (Object.class.equals(type)) { return; } Streamable<PropertyDescriptor> descriptors = Streamable.of(BeanUtils.getPropertyDescriptors(type)); ReflectionUtils.doWithMethods(type, method -> { RootObjectInformation.this.methods.add(method); descriptors.stream()// .filter(it -> method.equals(it.getReadMethod()))// .forEach(it -> RootObjectInformation.this.accessors.put(it.getName(), method)); }, PublicMethodAndFieldFilter.NON_STATIC); ReflectionUtils.doWithFields(type, RootObjectInformation.this.fields::add, PublicMethodAndFieldFilter.NON_STATIC); } /** * Returns {@link Function} instances that wrap method invocations on the given target object. * * @param target can be {@literal null}. * @return the methods */ public Map<String, Function> getFunctions(Optional<Object> target) { return target.map(it -> methods.stream()// .collect(Collectors.toMap(// Method::getName, // method -> new Function(method, it), // (left, right) -> right))) .orElseGet(Collections::emptyMap); } /** * Returns the properties of the target object. This will also include {@link Function} instances for all properties * with accessor methods that need to be resolved downstream. * * @return the properties */ public Map<String, Object> getProperties(Optional<Object> target) { return target.map(it -> { Map<String, Object> properties = new HashMap<>(); accessors.entrySet().stream() .forEach(method -> properties.put(method.getKey(), new Function(method.getValue(), it))); fields.stream().forEach(field -> properties.put(field.getName(), ReflectionUtils.getField(field, it))); return Collections.unmodifiableMap(properties); }).orElseGet(Collections::emptyMap); } } private static Map<String, Object> discoverDeclaredProperties(Class<?> type) { Map<String, Object> map = new HashMap<>(); ReflectionUtils.doWithFields(type, field -> map.put(field.getName(), field.get(null)), PublicMethodAndFieldFilter.STATIC); return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map); } }