diff --git a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java index 17fe50f30..294717dfc 100644 --- a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java +++ b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java @@ -15,10 +15,11 @@ */ package org.springframework.data.projection; +import lombok.extern.slf4j.Slf4j; + import java.beans.PropertyDescriptor; import java.io.IOException; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -29,11 +30,11 @@ import java.util.stream.IntStream; import java.util.stream.Stream; import org.springframework.beans.BeanUtils; -import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.data.type.MethodsMetadata; -import org.springframework.data.type.MethodsMetadataReader; +import org.springframework.data.type.classreading.MethodsMetadataReader; import org.springframework.data.type.classreading.MethodsMetadataReaderFactory; +import org.springframework.data.util.StreamUtils; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -61,7 +62,7 @@ class DefaultProjectionInformation implements ProjectionInformation { Assert.notNull(type, "Projection type must not be null!"); this.projectionType = type; - this.properties = collectDescriptors(type); + this.properties = new PropertyDescriptorSource(type).getDescriptors(); } /* @@ -107,103 +108,171 @@ class DefaultProjectionInformation implements ProjectionInformation { } /** - * Collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces. + * Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method. * - * @param type must not be {@literal null}. + * @param descriptor must not be {@literal null}. * @return */ - private static List collectDescriptors(Class type) { + private static boolean hasDefaultGetter(PropertyDescriptor descriptor) { - List result = new ArrayList<>(); + Method method = descriptor.getReadMethod(); - Optional metadata = getMetadata(type); - Stream stream = Arrays.stream(BeanUtils.getPropertyDescriptors(type))// - .filter(it -> !hasDefaultGetter(it)); + return method == null ? false : method.isDefault(); + } - Stream streamToUse = metadata.map(DefaultProjectionInformation::getMethodOrder) - .filter(it -> !it.isEmpty()) // - .map(it -> stream.filter(descriptor -> it.containsKey(descriptor.getReadMethod().getName())) - .sorted(Comparator.comparingInt(left -> it.get(left.getReadMethod().getName())))) // - .orElse(stream); + /** + * Internal helper to detect {@link PropertyDescriptor} instances for a given type. + * + * @author Mark Paluch + * @author Oliver Gierke + * @since 2.1 + * @soundtrack The Meters - Cissy Strut (Here Comes The Meter Man) + */ + @Slf4j + private static class PropertyDescriptorSource { - result.addAll(streamToUse.collect(Collectors.toList())); + private final Class type; + private final Optional metadata; - if (metadata.isPresent()) { + /** + * Creates a new {@link PropertyDescriptorSource} for the given type. + * + * @param type must not be {@literal null}. + */ + public PropertyDescriptorSource(Class type) { - Stream interfaceNames = metadata.map(ClassMetadata::getInterfaceNames) // - .map(Arrays::stream) // - .orElse(Stream.empty()); + Assert.notNull(type, "Type must not be null!"); - result.addAll(interfaceNames.map(it -> loadClass(it, type.getClassLoader())) // - .map(DefaultProjectionInformation::collectDescriptors) // - .flatMap(List::stream) // - .collect(Collectors.toList())); - } else { + this.type = type; + this.metadata = getMetadata(type); + } - for (Class interfaze : type.getInterfaces()) { - result.addAll(collectDescriptors(interfaze)); - } + /** + * Returns {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces. + * + * @return + */ + public List getDescriptors() { + return collectDescriptors().distinct().collect(StreamUtils.toUnmodifiableList()); } - return result.stream().distinct().collect(Collectors.toList()); - } + /** + * Recursively collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super + * interfaces. + * + * @return + */ + private Stream collectDescriptors() { + + Stream allButDefaultGetters = Arrays.stream(BeanUtils.getPropertyDescriptors(type)) // + .filter(it -> !hasDefaultGetter(it)); + + Stream ownDescriptors = metadata.map(it -> filterAndOrder(allButDefaultGetters, it)) + .orElse(allButDefaultGetters); - private static Class loadClass(String className, ClassLoader classLoader) { + Stream superTypeDescriptors = metadata.map(this::fromMetadata) // + .orElseGet(this::fromType) // + .flatMap(it -> new PropertyDescriptorSource(it).collectDescriptors()); - try { - return ClassUtils.forName(className, classLoader); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException(String.format("Cannot load class %s", className)); + return Stream.concat(ownDescriptors, superTypeDescriptors); } - } - /** - * Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}. - * - * @param metadata - * @return - */ - private static Map getMethodOrder(MethodsMetadata metadata) { + /** + * Returns a Stream of {@link PropertyDescriptor} ordered following the given {@link MethodsMetadata} only returning + * methods seen by the given {@link MethodsMetadata}. + * + * @param source must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @return + */ + private static Stream filterAndOrder(Stream source, + MethodsMetadata metadata) { + + Map orderedMethods = getMethodOrder(metadata); + + if (orderedMethods.isEmpty()) { + return source; + } - List methods = metadata.getMethods() // - .stream() // - .map(MethodMetadata::getMethodName) // - .distinct() // - .collect(Collectors.toList()); + return source.filter(descriptor -> orderedMethods.containsKey(descriptor.getReadMethod().getName())) + .sorted(Comparator.comparingInt(left -> orderedMethods.get(left.getReadMethod().getName()))); + } - return IntStream.range(0, methods.size()) // - .boxed() // - .collect(Collectors.toMap(methods::get, i -> i)); - } + /** + * Returns a {@link Stream} of interfaces using the given {@link MethodsMetadata} as primary source for ordering. + * + * @param metadata must not be {@literal null}. + * @return + */ + private Stream> fromMetadata(MethodsMetadata metadata) { + return Arrays.stream(metadata.getInterfaceNames()).map(it -> findType(it, type.getInterfaces())); + } - /** - * Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing - * {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise. - * - * @param type must not be {@literal null}. - * @return the optional {@link MethodsMetadata}. - */ - private static Optional getMetadata(Class type) { + /** + * Returns a Stream of interfaces using the given type as primary source for ordering. + * + * @return + */ + private Stream> fromType() { + return Arrays.stream(type.getInterfaces()); + } - try { + /** + * Attempts to obtain {@link MethodsMetadata} from {@link Class}. Returns {@link Optional} containing + * {@link MethodsMetadata} if metadata was read successfully, {@link Optional#empty()} otherwise. + * + * @param type must not be {@literal null}. + * @return the optional {@link MethodsMetadata}. + */ + private static Optional getMetadata(Class type) { - MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader()); - MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type)); - return Optional.of(metadataReader.getMethodsMetadata()); - } catch (IOException e) { - return Optional.empty(); + try { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader()); + MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type)); + + return Optional.of(metadataReader.getMethodsMetadata()); + + } catch (IOException e) { + + LOG.info("Couldn't read class metadata for {}. Input property calculation might fail!", type); + + return Optional.empty(); + } } - } - /** - * Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method. - * - * @param descriptor must not be {@literal null}. - * @return - */ - private static boolean hasDefaultGetter(PropertyDescriptor descriptor) { + /** + * Find the type with the given name in the given array of {@link Class}. + * + * @param name must not be {@literal null} or empty. + * @param types must not be {@literal null}. + * @return + */ + private static Class findType(String name, Class[] types) { + + return Arrays.stream(types) // + .filter(it -> name.equals(it.getName())) // + .findFirst() + .orElseThrow(() -> new IllegalStateException(String.format("Did not find type %s in %s!", name, types))); + } - Method method = descriptor.getReadMethod(); - return method == null ? false : method.isDefault(); + /** + * Returns a {@link Map} containing method name to its positional index according to {@link MethodsMetadata}. + * + * @param metadata + * @return + */ + private static Map getMethodOrder(MethodsMetadata metadata) { + + List methods = metadata.getMethods() // + .stream() // + .map(MethodMetadata::getMethodName) // + .distinct() // + .collect(Collectors.toList()); + + return IntStream.range(0, methods.size()) // + .boxed() // + .collect(Collectors.toMap(methods::get, i -> i)); + } } } diff --git a/src/main/java/org/springframework/data/type/MethodsMetadata.java b/src/main/java/org/springframework/data/type/MethodsMetadata.java index 74fc84988..d9bc3d601 100644 --- a/src/main/java/org/springframework/data/type/MethodsMetadata.java +++ b/src/main/java/org/springframework/data/type/MethodsMetadata.java @@ -19,6 +19,7 @@ import java.util.Set; import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.MethodMetadata; +import org.springframework.data.type.classreading.MethodsMetadataReader; /** * Interface that defines abstract metadata of a specific class, in a form that does not require that class to be loaded diff --git a/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java b/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java index d201637a8..39e249693 100644 --- a/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java +++ b/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java @@ -15,18 +15,29 @@ */ package org.springframework.data.type.classreading; +import lombok.Getter; + import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Collections; +import java.util.Set; import org.springframework.asm.ClassReader; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.asm.Type; import org.springframework.core.NestedIOException; import org.springframework.core.io.Resource; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor; +import org.springframework.core.type.classreading.MethodMetadataReadingVisitor; import org.springframework.data.type.MethodsMetadata; -import org.springframework.data.type.MethodsMetadataReader; +import org.springframework.data.util.StreamUtils; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * {@link MethodsMetadataReader} implementation based on an ASM {@link org.springframework.asm.ClassReader}. @@ -34,6 +45,7 @@ import org.springframework.lang.Nullable; * @author Mark Paluch * @since 2.1 */ +@Getter class DefaultMethodsMetadataReader implements MethodsMetadataReader { private final Resource resource; @@ -43,57 +55,96 @@ class DefaultMethodsMetadataReader implements MethodsMetadataReader { DefaultMethodsMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + MethodsMetadataReadingVisitor visitor = new MethodsMetadataReadingVisitor(classLoader); + createClassReader(resource).accept(visitor, ClassReader.SKIP_DEBUG); + this.resource = resource; + this.classMetadata = visitor; + this.annotationMetadata = visitor; + this.methodsMetadata = visitor; + } - ClassReader classReader; + private static ClassReader createClassReader(Resource resource) throws IOException { + + try (InputStream is = new BufferedInputStream(resource.getInputStream())) { + + return new ClassReader(is); - try (InputStream is = new BufferedInputStream(getResource().getInputStream())) { - classReader = new ClassReader(is); } catch (IllegalArgumentException ex) { throw new NestedIOException("ASM ClassReader failed to parse class file - " - + "probably due to a new Java class file version that isn't supported yet: " + getResource(), ex); + + "probably due to a new Java class file version that isn't supported yet: " + resource, ex); } - - MethodsMetadataReadingVisitor visitor = new MethodsMetadataReadingVisitor(classLoader); - classReader.accept(visitor, ClassReader.SKIP_DEBUG); - - classMetadata = visitor; - annotationMetadata = visitor; - methodsMetadata = visitor; } - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getResource() + /** + * ASM class visitor which looks for the class name and implemented types as well as for the methods defined in the + * class, exposing them through the {@link MethodsMetadata} interface. + * + * @author Mark Paluch + * @since 2.1 + * @see ClassMetadata + * @see MethodMetadata + * @see MethodMetadataReadingVisitor */ - @Override - public Resource getResource() { - return resource; - } + private static class MethodsMetadataReadingVisitor extends AnnotationMetadataReadingVisitor + implements MethodsMetadata { - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata() - */ - @Override - public ClassMetadata getClassMetadata() { - return classMetadata; - } + /** + * Construct a new {@link MethodsMetadataReadingVisitor} given {@link ClassLoader}. + * + * @param classLoader may be {@literal null}. + */ + MethodsMetadataReadingVisitor(@Nullable ClassLoader classLoader) { + super(classLoader); + } - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() - */ - @Override - public AnnotationMetadata getAnnotationMetadata() { - return annotationMetadata; - } + /* + * (non-Javadoc) + * @see org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) + */ + @Override + @SuppressWarnings("null") + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - /* (non-Javadoc) - * @see org.springframework.data.util.ClassMetadataReader#getMethodsMetadata() - */ - @Override - public MethodsMetadata getMethodsMetadata() { - return methodsMetadata; + // Skip bridge methods - we're only interested in original user methods. + // On JDK 8, we'd otherwise run into double detection of the same method... + if ((access & Opcodes.ACC_BRIDGE) != 0) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + + // Skip constructors + if (name.equals("")) { + return super.visitMethod(access, name, desc, signature, exceptions); + } + + MethodMetadataReadingVisitor visitor = new MethodMetadataReadingVisitor(name, access, getClassName(), + Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet); + + this.methodMetadataSet.add(visitor); + return visitor; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.MethodsMetadata#getMethods() + */ + @Override + public Set getMethods() { + return Collections.unmodifiableSet(methodMetadataSet); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.util.MethodsMetadata#getMethods(String) + */ + @Override + public Set getMethods(String name) { + + Assert.hasText(name, "Method name must not be null or empty"); + + return methodMetadataSet.stream() // + .filter(it -> it.getMethodName().equals(name)) // + .collect(StreamUtils.toUnmodifiableSet()); + } } } diff --git a/src/main/java/org/springframework/data/type/MethodsMetadataReader.java b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReader.java similarity index 84% rename from src/main/java/org/springframework/data/type/MethodsMetadataReader.java rename to src/main/java/org/springframework/data/type/classreading/MethodsMetadataReader.java index f21d64a99..f5797e473 100644 --- a/src/main/java/org/springframework/data/type/MethodsMetadataReader.java +++ b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReader.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.type; +package org.springframework.data.type.classreading; import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.data.type.MethodsMetadata; /** * Extension to {@link MetadataReader} for accessing class metadata and method metadata as read by an ASM @@ -27,7 +28,7 @@ import org.springframework.core.type.classreading.MetadataReader; public interface MethodsMetadataReader extends MetadataReader { /** - * @return the metadata for methods in the class file. + * @return the {@link MethodsMetadata} for methods in the class file. */ MethodsMetadata getMethodsMetadata(); } diff --git a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java index 6f4d249e0..d8a4f7921 100644 --- a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java +++ b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java @@ -21,7 +21,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.data.type.MethodsMetadata; -import org.springframework.data.type.MethodsMetadataReader; import org.springframework.lang.Nullable; /** diff --git a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java deleted file mode 100644 index db3826c7c..000000000 --- a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2018 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.type.classreading; - -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.springframework.asm.MethodVisitor; -import org.springframework.asm.Opcodes; -import org.springframework.asm.Type; -import org.springframework.core.type.ClassMetadata; -import org.springframework.core.type.MethodMetadata; -import org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor; -import org.springframework.core.type.classreading.MethodMetadataReadingVisitor; -import org.springframework.data.type.MethodsMetadata; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * ASM class visitor which looks for the class name and implemented types as well as for the methods defined in the - * class, exposing them through the {@link MethodsMetadata} interface. - * - * @author Mark Paluch - * @since 2.1 - * @see ClassMetadata - * @see MethodMetadata - * @see MethodMetadataReadingVisitor - */ -class MethodsMetadataReadingVisitor extends AnnotationMetadataReadingVisitor implements MethodsMetadata { - - /** - * Construct a new {@link MethodsMetadataReadingVisitor} given {@link ClassLoader}. - * - * @param classLoader may be {@literal null}. - */ - MethodsMetadataReadingVisitor(@Nullable ClassLoader classLoader) { - super(classLoader); - } - - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor#visitMethod(int, java.lang.String, java.lang.String, java.lang.String, java.lang.String[]) - */ - @Override - public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - - // Skip bridge methods - we're only interested in original user methods. - // On JDK 8, we'd otherwise run into double detection of the same method... - if ((access & Opcodes.ACC_BRIDGE) != 0) { - return super.visitMethod(access, name, desc, signature, exceptions); - } - - // Skip constructors - if (name.equals("")) { - return super.visitMethod(access, name, desc, signature, exceptions); - } - - MethodMetadataReadingVisitor visitor = new MethodMetadataReadingVisitor(name, access, getClassName(), - Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet); - - this.methodMetadataSet.add(visitor); - return visitor; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.util.MethodsMetadata#getMethods() - */ - @Override - public Set getMethods() { - return Collections.unmodifiableSet(methodMetadataSet); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.util.MethodsMetadata#getMethods(String) - */ - @Override - public Set getMethods(String name) { - - Assert.hasText(name, "Method name must not be null or empty"); - - Set result = new LinkedHashSet<>(4); - - for (MethodMetadata metadata : methodMetadataSet) { - if (metadata.getMethodName().equals(name)) { - result.add(metadata); - } - } - - return Collections.unmodifiableSet(result); - } -} diff --git a/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java b/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java index 7d0937084..a81ebea6d 100644 --- a/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java +++ b/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java @@ -23,7 +23,6 @@ import java.util.Iterator; import org.junit.Test; import org.springframework.core.type.MethodMetadata; import org.springframework.data.type.MethodsMetadata; -import org.springframework.data.type.MethodsMetadataReader; /** * Unit tests for {@link DefaultMethodsMetadataReader}. diff --git a/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java b/src/test/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactoryUnitTests.java similarity index 94% rename from src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java rename to src/test/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactoryUnitTests.java index 02d477294..dae9853c7 100644 --- a/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactoryUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.type; +package org.springframework.data.type.classreading; import static org.assertj.core.api.Assertions.*; @@ -24,6 +24,7 @@ import java.net.URLClassLoader; import org.junit.Test; import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.data.type.classreading.MethodsMetadataReader; import org.springframework.data.type.classreading.MethodsMetadataReaderFactory; /**