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(); } /* @@ -106,95 +107,6 @@ class DefaultProjectionInformation implements ProjectionInformation { return true; } - /** - * Collects {@link PropertyDescriptor}s for all properties exposed by the given type and all its super interfaces. - * - * @param type must not be {@literal null}. - * @return - */ - private static List collectDescriptors(Class type) { - - List result = new ArrayList<>(); - - Optional metadata = getMetadata(type); - Stream stream = Arrays.stream(BeanUtils.getPropertyDescriptors(type))// - .filter(it -> !hasDefaultGetter(it)); - - 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); - - result.addAll(streamToUse.collect(Collectors.toList())); - - if (metadata.isPresent()) { - - Stream interfaceNames = metadata.map(ClassMetadata::getInterfaceNames) // - .map(Arrays::stream) // - .orElse(Stream.empty()); - - result.addAll(interfaceNames.map(it -> loadClass(it, type.getClassLoader())) // - .map(DefaultProjectionInformation::collectDescriptors) // - .flatMap(List::stream) // - .collect(Collectors.toList())); - } else { - - for (Class interfaze : type.getInterfaces()) { - result.addAll(collectDescriptors(interfaze)); - } - } - - return result.stream().distinct().collect(Collectors.toList()); - } - - private static Class loadClass(String className, ClassLoader classLoader) { - - try { - return ClassUtils.forName(className, classLoader); - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException(String.format("Cannot load class %s", className)); - } - } - - /** - * 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)); - } - - /** - * 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) { - - try { - - MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader()); - MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type)); - return Optional.of(metadataReader.getMethodsMetadata()); - } catch (IOException e) { - return Optional.empty(); - } - } - /** * Returns whether the given {@link PropertyDescriptor} has a getter that is a Java 8 default method. * @@ -204,6 +116,163 @@ class DefaultProjectionInformation implements ProjectionInformation { private static boolean hasDefaultGetter(PropertyDescriptor descriptor) { Method method = descriptor.getReadMethod(); + return method == null ? false : method.isDefault(); } + + /** + * 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 { + + private final Class type; + private final Optional metadata; + + /** + * Creates a new {@link PropertyDescriptorSource} for the given type. + * + * @param type must not be {@literal null}. + */ + public PropertyDescriptorSource(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + this.metadata = getMetadata(type); + } + + /** + * 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()); + } + + /** + * 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); + + Stream superTypeDescriptors = metadata.map(this::fromMetadata) // + .orElseGet(this::fromType) // + .flatMap(it -> new PropertyDescriptorSource(it).collectDescriptors()); + + return Stream.concat(ownDescriptors, superTypeDescriptors); + } + + /** + * 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; + } + + return source.filter(descriptor -> orderedMethods.containsKey(descriptor.getReadMethod().getName())) + .sorted(Comparator.comparingInt(left -> orderedMethods.get(left.getReadMethod().getName()))); + } + + /** + * 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())); + } + + /** + * Returns a Stream of interfaces using the given type as primary source for ordering. + * + * @return + */ + private Stream> fromType() { + return Arrays.stream(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) { + + 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(); + } + } + + /** + * 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))); + } + + /** + * 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); + } + } + + /** + * 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 + */ + private static 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); } - MethodsMetadataReadingVisitor visitor = new MethodsMetadataReadingVisitor(classLoader); - classReader.accept(visitor, ClassReader.SKIP_DEBUG); + /* + * (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) { - classMetadata = visitor; - annotationMetadata = visitor; - methodsMetadata = visitor; - } + // 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); + } - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getResource() - */ - @Override - public Resource getResource() { - return resource; - } + // Skip constructors + if (name.equals("")) { + return super.visitMethod(access, name, desc, signature, exceptions); + } - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata() - */ - @Override - public ClassMetadata getClassMetadata() { - return classMetadata; - } + MethodMetadataReadingVisitor visitor = new MethodMetadataReadingVisitor(name, access, getClassName(), + Type.getReturnType(desc).getClassName(), this.classLoader, this.methodMetadataSet); - /* - * (non-Javadoc) - * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() - */ - @Override - public AnnotationMetadata getAnnotationMetadata() { - return annotationMetadata; - } + this.methodMetadataSet.add(visitor); + return visitor; + } - /* (non-Javadoc) - * @see org.springframework.data.util.ClassMetadataReader#getMethodsMetadata() - */ - @Override - public MethodsMetadata getMethodsMetadata() { - return methodsMetadata; + /* + * (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; /**