diff --git a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java index 80c345301..3492aa421 100644 --- a/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java +++ b/src/main/java/org/springframework/data/projection/DefaultProjectionInformation.java @@ -16,19 +16,32 @@ 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.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.springframework.beans.BeanUtils; +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.data.util.ReflectionUtils; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * Default implementation of {@link ProjectionInformation}. Exposes all properties of the type as required input * properties. * * @author Oliver Gierke + * @author Mark Paluch * @since 1.12 */ class DefaultProjectionInformation implements ProjectionInformation { @@ -41,7 +54,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!"); @@ -105,15 +118,96 @@ class DefaultProjectionInformation implements ProjectionInformation { private static List collectDescriptors(Class type) { List result = new ArrayList(); - result.addAll(filterDefaultMethods(BeanUtils.getPropertyDescriptors(type))); + MethodsMetadata metadata = getMetadata(type); + final Map orders = getMethodOrder(metadata); - for (Class interfaze : type.getInterfaces()) { - result.addAll(collectDescriptors(interfaze)); + for (PropertyDescriptor descriptor : filterDefaultMethods(BeanUtils.getPropertyDescriptors(type))) { + if (metadata == null || orders.containsKey(descriptor.getReadMethod().getName())) { + result.add(descriptor); + } + } + + if (metadata == null) { + return result; + } + + Collections.sort(result, new Comparator() { + + /* + * (non-Javadoc) + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + @Override + public int compare(PropertyDescriptor left, PropertyDescriptor right) { + return orders.get(left.getReadMethod().getName()) - orders.get(right.getReadMethod().getName()); + } + }); + + for (String name : metadata.getInterfaceNames()) { + result.addAll(collectDescriptors(loadClass(name, type.getClassLoader()))); } return result; } + 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) { + + if (metadata == null) { + return Collections.emptyMap(); + } + + Set methods = metadata.getMethods(); + Map result = new HashMap(methods.size()); + int i = 0; + + for (MethodMetadata methodMetadata : methods) { + + String name = methodMetadata.getMethodName(); + + if (!result.containsKey(name)) { + result.put(name, i++); + } + } + + return result; + } + + /** + * 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 MethodsMetadata getMetadata(Class type) { + + try { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(type.getClassLoader()); + MethodsMetadataReader metadataReader = factory.getMetadataReader(ClassUtils.getQualifiedName(type)); + + return metadataReader.getMethodsMetadata(); + + } catch (IOException e) { + return null; + } + } + /** * Returns all {@link PropertyDescriptor}s that don't have a Java 8 default method as getter. * 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..49f73e993 --- /dev/null +++ b/src/main/java/org/springframework/data/type/MethodsMetadata.java @@ -0,0 +1,52 @@ +/* + * 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 + * @since 1.11.11 + * @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..7e9ce7644 --- /dev/null +++ b/src/main/java/org/springframework/data/type/MethodsMetadataReader.java @@ -0,0 +1,34 @@ +/* + * 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 + * @since 1.11.11 + */ +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..509a6c8c5 --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReader.java @@ -0,0 +1,111 @@ +/* + * 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; + +/** + * {@link MethodsMetadataReader} implementation based on an ASM {@link org.springframework.asm.ClassReader}. + * + * @author Mark Paluch + * @auhtor Oliver Gierke + * @since 2.1 + * @since 1.11.11 + */ +class DefaultMethodsMetadataReader implements MethodsMetadataReader { + + private final Resource resource; + private final ClassMetadata classMetadata; + private final AnnotationMetadata annotationMetadata; + private final MethodsMetadata methodsMetadata; + + DefaultMethodsMetadataReader(Resource resource, ClassLoader classLoader) throws IOException { + + this.resource = resource; + + ClassReader classReader = null; + InputStream is = null; + + try { + + 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); + + } finally { + + if (is != null) { + is.close(); + } + } + + 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..963dbf490 --- /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; + +/** + * Extension of {@link SimpleMetadataReaderFactory} that reads {@link MethodsMetadata}, creating a new ASM + * {@link MethodsMetadataReader} for every request. + * + * @author Mark Paluch + * @since 2.1 + * @since 1.11.11 + */ +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(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + /** + * Create a new {@link MethodsMetadataReaderFactory} for the given {@link ClassLoader}. + * + * @param classLoader the class loader to use. + */ + public MethodsMetadataReaderFactory(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..ea8f0a8db --- /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.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 + * @since 1.11.11 + * @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(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..0b6b2dd86 --- /dev/null +++ b/src/main/java/org/springframework/data/type/classreading/package-info.java @@ -0,0 +1,4 @@ +/** + * Support classes for reading annotation and class-level metadata. + */ +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..ec4147ac8 --- /dev/null +++ b/src/main/java/org/springframework/data/type/package-info.java @@ -0,0 +1,4 @@ +/** + * Core support package for type introspection. + */ +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 0cfd79260..3fc1f2944 100644 --- 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 { @@ -44,7 +45,24 @@ public class DefaultProjectionInformationUnitTests { ProjectionInformation information = new DefaultProjectionInformation(ExtendedProjection.class); - assertThat(toNames(information.getInputProperties()), hasItems("age", "firstname", "lastname")); + assertThat(toNames(information.getInputProperties()), contains("age", "firstname", "lastname")); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void discoversInputPropertiesInOrder() { + + ProjectionInformation information = new DefaultProjectionInformation(SameMethodNamesInAlternateOrder.class); + + assertThat(toNames(information.getInputProperties()), contains("firstname", "lastname")); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void discoversAllInputPropertiesInOrder() { + + assertThat(toNames(new DefaultProjectionInformation(CompositeProjection.class).getInputProperties()), + contains("firstname", "lastname", "age")); + assertThat(toNames(new DefaultProjectionInformation(ReorderedCompositeProjection.class).getInputProperties()), + contains("age", "firstname", "lastname")); } private static List toNames(List descriptors) { @@ -69,4 +87,22 @@ 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(); + } } 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..28707747b --- /dev/null +++ b/src/test/java/org/springframework/data/type/MethodsMetadataReaderFactoryUnitTests.java @@ -0,0 +1,78 @@ +/* + * 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.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.data.type.classreading.MethodsMetadataReaderFactory; + +/** + * Unit tests for {@link MethodsMetadataReaderFactory}. + * + * @author Mark Paluch + * @author Oliver Gierke + */ +public class MethodsMetadataReaderFactoryUnitTests { + + public @Rule ExpectedException exception = ExpectedException.none(); + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldReadFromDefaultClassLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader, is(notNullValue())); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldReadFromClassLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(getClass().getClassLoader()); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader, is(notNullValue())); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldNotFindClass() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(new URLClassLoader(new URL[0], null)); + + exception.expect(FileNotFoundException.class); + + factory.getMetadataReader(getClass().getName()); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldReadFromResourceLoader() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(new DefaultResourceLoader()); + MethodsMetadataReader reader = factory.getMetadataReader(getClass().getName()); + + assertThat(reader, is(notNullValue())); + } +} 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..84d708f55 --- /dev/null +++ b/src/test/java/org/springframework/data/type/classreading/DefaultMethodsMetadataReaderUnitTests.java @@ -0,0 +1,123 @@ +/* + * 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.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Set; + +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, DATACMNS-1284 + public void shouldReadClassMethods() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Foo.class); + + assertThat(metadata.getMethods(), hasSize(3)); + + Iterator iterator = metadata.getMethods().iterator(); + + assertThat(iterator.next().getMethodName(), is("one")); + assertThat(iterator.next().getMethodName(), is("two")); + assertThat(iterator.next().getMethodName(), is("three")); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldReadInterfaceMethods() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Baz.class); + + assertThat(metadata.getMethods(), hasSize(3)); + + Iterator iterator = metadata.getMethods().iterator(); + + assertThat(iterator.next().getMethodName(), is("one")); + assertThat(iterator.next().getMethodName(), is("two")); + assertThat(iterator.next().getMethodName(), is("three")); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldMetadata() throws IOException { + + MethodsMetadataReaderFactory factory = new MethodsMetadataReaderFactory(); + MethodsMetadataReader metadataReader = factory.getMetadataReader(getClass().getName()); + + assertThat(metadataReader.getClassMetadata(), is(notNullValue())); + assertThat(metadataReader.getAnnotationMetadata(), is(notNullValue())); + } + + @Test // DATACMNS-1206, DATACMNS-1284 + public void shouldReturnMethodMetadataByName() throws IOException { + + MethodsMetadata metadata = getMethodsMetadata(Foo.class); + + assertThat(metadata.getMethods(), hasSize(3)); + + Set oneMethods = metadata.getMethods("one"); + + assertThat(oneMethods, hasSize(1)); + assertThat(oneMethods.iterator().next().getMethodName(), is("one")); + + assertThat(metadata.getMethods("foo"), hasSize(0)); + } + + 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); + } +}