From 9e013d3c14ea27a4707e36979340e25d9fa03809 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 29 Nov 2017 17:25:06 +0100 Subject: [PATCH] DATACMNS-1206 - Add API to read methods in declaration order. We now provide MethodsMetadataReader to read method metadata from a class file. MethodMetadata is read for all user-declared methods except for constructors (which are technically methods, too). MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); MethodsMetadataReader metadataReader = factory.getMetadataReader("com.acme.Foo"); MethodsMetadata metadata = metadataReader.getMethodsMetadata(); This new API is now used by DefaultProjectionInformation to make sure the order of input properties is based on the declaration order in the projection interfaces. Previously that order could not be guaranteed to be stable. Original pull request: #263. --- .../DefaultProjectionInformation.java | 94 +++++++++++++- .../data/type/MethodsMetadata.java | 51 ++++++++ .../data/type/MethodsMetadataReader.java | 33 +++++ .../DefaultMethodsMetadataReader.java | 99 +++++++++++++++ .../MethodsMetadataReaderFactory.java | 76 ++++++++++++ .../MethodsMetadataReadingVisitor.java | 107 ++++++++++++++++ .../data/type/classreading/package-info.java | 6 + .../data/type/package-info.java | 5 + ...DefaultProjectionInformationUnitTests.java | 36 ++++++ ...MethodsMetadataReaderFactoryUnitTests.java | 70 +++++++++++ ...DefaultMethodsMetadataReaderUnitTests.java | 117 ++++++++++++++++++ 11 files changed, 688 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/type/MethodsMetadata.java create mode 100644 src/main/java/org/springframework/data/type/MethodsMetadataReader.java create mode 100644 src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java create mode 100644 src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java create mode 100644 src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java create mode 100644 src/main/java/org/springframework/data/type/classreading/package-info.java create mode 100644 src/main/java/org/springframework/data/type/package-info.java create mode 100644 src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java diff --git a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java index 66786f99a..17fe50f30 100644 --- a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java +++ b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java @@ -16,14 +16,26 @@ package org.springframework.data.projection; 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; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +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.MethodsMetadataReaderFactory; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input @@ -31,6 +43,7 @@ import org.springframework.util.Assert; * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch * @since 1.12 */ class DefaultProjectionInformation implements ProjectionInformation { @@ -43,7 +56,7 @@ class DefaultProjectionInformation implements ProjectionInformation { * * @param type must not be {@literal null}. */ - public DefaultProjectionInformation(Class type) { + DefaultProjectionInformation(Class type) { Assert.notNull(type, "Projection type must not be null!"); @@ -102,17 +115,86 @@ class DefaultProjectionInformation implements ProjectionInformation { private static List collectDescriptors(Class type) { List result = new ArrayList<>(); - result.addAll(Arrays.stream(BeanUtils.getPropertyDescriptors(type))// - .filter(it -> !hasDefaultGetter(it))// - .collect(Collectors.toList())); - for (Class interfaze : type.getInterfaces()) { - result.addAll(collectDescriptors(interfaze)); + 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. * diff --git a/src/main/java/org/springframework/data/type/MethodsMetadata.java b/src/main/java/org/springframework/data/type/MethodsMetadata.java new file mode 100644 index 000000000..74fc84988 --- /dev/null +++ b/src/main/java/org/springframework/data/type/MethodsMetadata.java @@ -0,0 +1,51 @@ +/* + * 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; + +import java.util.Set; + +import org.springframework.core.type.ClassMetadata; +import org.springframework.core.type.MethodMetadata; + +/** + * Interface that defines abstract metadata of a specific class, in a form that does not require that class to be loaded + * yet. + * + * @author Mark Paluch + * @since 2.1 + * @see MethodMetadata + * @see ClassMetadata + * @see MethodsMetadataReader#getMethodsMetadata() + */ +public interface MethodsMetadata extends ClassMetadata { + + /** + * Return all methods. + * + * @return the methods declared in the class ordered as found in the class file. Order does not necessarily reflect + * the declaration order in the source file. + */ + Set getMethods(); + + /** + * Return all methods matching method {@code name}. + * + * @param name name of the method, must not be {@literal null} or empty. + * @return the methods matching method {@code name } declared in the class ordered as found in the class file. Order + * does not necessarily reflect the declaration order in the source file. + */ + Set getMethods(String name); +} diff --git a/src/main/java/org/springframework/data/type/MethodsMetadataReader.java b/src/main/java/org/springframework/data/type/MethodsMetadataReader.java new file mode 100644 index 000000000..f21d64a99 --- /dev/null +++ b/src/main/java/org/springframework/data/type/MethodsMetadataReader.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.springframework.core.type.classreading.MetadataReader; + +/** + * Extension to {@link MetadataReader} for accessing class metadata and method metadata as read by an ASM + * {@link org.springframework.asm.ClassReader}. + * + * @author Mark Paluch + * @since 2.1 + */ +public interface MethodsMetadataReader extends MetadataReader { + + /** + * @return the metadata for methods in the class file. + */ + MethodsMetadata getMethodsMetadata(); +} diff --git a/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java b/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java new file mode 100644 index 000000000..d201637a8 --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java @@ -0,0 +1,99 @@ +/* + * 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.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.asm.ClassReader; +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.data.type.MethodsMetadata; +import org.springframework.data.type.MethodsMetadataReader; +import org.springframework.lang.Nullable; + +/** + * {@link MethodsMetadataReader} implementation based on an ASM {@link org.springframework.asm.ClassReader}. + * + * @author Mark Paluch + * @since 2.1 + */ +class DefaultMethodsMetadataReader implements MethodsMetadataReader { + + private final Resource resource; + private final ClassMetadata classMetadata; + private final AnnotationMetadata annotationMetadata; + private final MethodsMetadata methodsMetadata; + + DefaultMethodsMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + + this.resource = resource; + + ClassReader classReader; + + 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); + } + + 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() + */ + @Override + public Resource getResource() { + return resource; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.classreading.MetadataReader#getClassMetadata() + */ + @Override + public ClassMetadata getClassMetadata() { + return classMetadata; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.classreading.MetadataReader#getAnnotationMetadata() + */ + @Override + public AnnotationMetadata getAnnotationMetadata() { + return annotationMetadata; + } + + /* (non-Javadoc) + * @see org.springframework.data.util.ClassMetadataReader#getMethodsMetadata() + */ + @Override + public MethodsMetadata getMethodsMetadata() { + return methodsMetadata; + } +} diff --git a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java new file mode 100644 index 000000000..6f4d249e0 --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReaderFactory.java @@ -0,0 +1,76 @@ +/* + * 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.io.IOException; + +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; + +/** + * Extension of {@link SimpleMetadataReaderFactory} that reads {@link MethodsMetadata}, creating a new ASM + * {@link MethodsMetadataReader} for every request. + * + * @author Mark Paluch + * @since 2.1 + */ +public class MethodsMetadataReaderFactory extends SimpleMetadataReaderFactory { + + /** + * Create a new {@link MethodsMetadataReaderFactory} for the default class loader. + */ + public MethodsMetadataReaderFactory() {} + + /** + * Create a new {@link MethodsMetadataReaderFactory} for the given {@link ResourceLoader}. + * + * @param resourceLoader the Spring {@link ResourceLoader} to use (also determines the {@link ClassLoader} to use). + */ + public MethodsMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { + super(resourceLoader); + } + + /** + * Create a new {@link MethodsMetadataReaderFactory} for the given {@link ClassLoader}. + * + * @param classLoader the class loader to use. + */ + public MethodsMetadataReaderFactory(@Nullable ClassLoader classLoader) { + super(classLoader); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.classreading.SimpleMetadataReaderFactory#getMetadataReader(java.lang.String) + */ + @Override + public MethodsMetadataReader getMetadataReader(String className) throws IOException { + return (MethodsMetadataReader) super.getMetadataReader(className); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.type.classreading.SimpleMetadataReaderFactory#getMetadataReader(org.springframework.core.io.Resource) + */ + @Override + public MethodsMetadataReader getMetadataReader(Resource resource) throws IOException { + return new DefaultMethodsMetadataReader(resource, getResourceLoader().getClassLoader()); + } +} diff --git a/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java new file mode 100644 index 000000000..db3826c7c --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/MethodsMetadataReadingVisitor.java @@ -0,0 +1,107 @@ +/* + * 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/main/java/org/springframework/data/type/classreading/package-info.java b/src/main/java/org/springframework/data/type/classreading/package-info.java new file mode 100644 index 000000000..fe6d22de4 --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for reading annotation and class-level metadata. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.type.classreading; diff --git a/src/main/java/org/springframework/data/type/package-info.java b/src/main/java/org/springframework/data/type/package-info.java new file mode 100644 index 000000000..b887b66b2 --- /dev/null +++ b/src/main/java/org/springframework/data/type/package-info.java @@ -0,0 +1,5 @@ +/** + * Core support package for type introspection. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.type; diff --git a/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java b/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java index a00cecc44..36bafbcc0 100755 --- a/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java +++ b/src/test/java/org/springframework/data/projection/DefaultProjectionInformationUnitTests.java @@ -28,6 +28,7 @@ import org.junit.Test; * Unit tests for {@link DefaultProjectionInformation}. * * @author Oliver Gierke + * @author Mark Paluch */ public class DefaultProjectionInformationUnitTests { @@ -47,6 +48,23 @@ public class DefaultProjectionInformationUnitTests { assertThat(toNames(information.getInputProperties())).containsExactly("age", "firstname", "lastname"); } + @Test // DATACMNS-1206 + public void discoversInputPropertiesInOrder() { + + ProjectionInformation information = new DefaultProjectionInformation(SameMethodNamesInAlternateOrder.class); + + assertThat(toNames(information.getInputProperties())).containsExactly("firstname", "lastname"); + } + + @Test // DATACMNS-1206 + public void discoversAllInputPropertiesInOrder() { + + assertThat(toNames(new DefaultProjectionInformation(CompositeProjection.class).getInputProperties())) + .containsExactly("firstname", "lastname", "age"); + assertThat(toNames(new DefaultProjectionInformation(ReorderedCompositeProjection.class).getInputProperties())) + .containsExactly("age", "firstname", "lastname"); + } + @Test // DATACMNS-967 public void doesNotConsiderDefaultMethodInputProperties() throws Exception { @@ -76,6 +94,24 @@ public class DefaultProjectionInformationUnitTests { int getAge(); } + interface SameMethodNamesInAlternateOrder { + + String getFirstname(); + + String getLastname(); + + String getFirstname(String foo); + } + + interface CompositeProjection extends CustomerProjection, AgeProjection {} + + interface ReorderedCompositeProjection extends AgeProjection, CustomerProjection {} + + interface AgeProjection { + + int getAge(); + } + interface WithDefaultMethod { String getFirstname(); diff --git a/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java b/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java new file mode 100644 index 000000000..02d477294 --- /dev/null +++ b/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java @@ -0,0 +1,70 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.*; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.Test; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.data.type.classreading.MethodsMetadataReaderFactory; + +/** + * Unit tests for {@link MethodsMetadataReaderFactory}. + * + * @author Mark Paluch + */ +public class MethodsMetadataReaderFactoryUnitTests { + + @Test // DATACMNS-1206 + public void shouldReadFromDefaultClassLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader).isNotNull(); + } + + @Test // DATACMNS-1206 + public void shouldReadFromClassLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(getClass().getClassLoader()); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader).isNotNull(); + } + + @Test // DATACMNS-1206 + public void shouldNotFindClass() { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(new URLClassLoader(new URL[0], null)); + + assertThatThrownBy(() -> factory.getMetadataReader(getClass().getName())).isInstanceOf(FileNotFoundException.class); + } + + @Test // DATACMNS-1206 + public void shouldReadFromResourceLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(new DefaultResourceLoader()); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader).isNotNull(); + } +} diff --git a/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java b/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java new file mode 100644 index 000000000..7d0937084 --- /dev/null +++ b/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java @@ -0,0 +1,117 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +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}. + * + * @author Mark Paluch + */ +public class DefaultMethodsMetadataReaderUnitTests { + + @Test // DATACMNS-1206 + public void shouldReadClassMethods() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Foo.class); + + assertThat(metadata.getMethods()).hasSize(3); + + Iterator iterator = metadata.getMethods().iterator(); + + assertThat(iterator.next().getMethodName()).isEqualTo("one"); + assertThat(iterator.next().getMethodName()).isEqualTo("two"); + assertThat(iterator.next().getMethodName()).isEqualTo("three"); + } + + @Test // DATACMNS-1206 + public void shouldReadInterfaceMethods() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Baz.class); + + assertThat(metadata.getMethods()).hasSize(3); + + Iterator iterator = metadata.getMethods().iterator(); + + assertThat(iterator.next().getMethodName()).isEqualTo("one"); + assertThat(iterator.next().getMethodName()).isEqualTo("two"); + assertThat(iterator.next().getMethodName()).isEqualTo("three"); + } + + @Test // DATACMNS-1206 + public void shouldMetadata() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); + MethodsMetadataReader metadataReader = factory.getMetadataReader(getClass().getName()); + + assertThat(metadataReader.getClassMetadata()).isNotNull(); + assertThat(metadataReader.getAnnotationMetadata()).isNotNull(); + } + + @Test // DATACMNS-1206 + public void shouldReturnMethodMetadataByName() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Foo.class); + + assertThat(metadata.getMethods()).hasSize(3); + + assertThat(metadata.getMethods("one")).extracting(MethodMetadata::getMethodName).contains("one"); + assertThat(metadata.getMethods("foo")).isEmpty(); + } + + private static MethodsMetadata getMethodsMetadata(Class classToIntrospect) throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); + MethodsMetadataReader metadataReader = factory.getMetadataReader(classToIntrospect.getName()); + return metadataReader.getMethodsMetadata(); + } + + // Create a scenario with a cyclic dependency to mix up methods reported by class.getDeclaredMethods() + // That's not exactly deterministic because it depends on when the compiler sees the classes. + abstract class Foo { + + abstract void one(Foo b); + + abstract void two(Bar b); + + abstract void three(Foo b); + } + + interface Baz { + + void one(Foo b); + + void two(Bar b); + + void three(Baz b); + } + + abstract class Bar { + + abstract void dependOnFoo(Foo f); + + abstract void dependOnBaz(Baz f); + } +}