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); + } +}