diff --git a/src/main/java/org/springframework/data/util/AnnotatedTypeScanner.java b/src/main/java/org/springframework/data/util/AnnotatedTypeScanner.java new file mode 100644 index 000000000..de5f33a43 --- /dev/null +++ b/src/main/java/org/springframework/data/util/AnnotatedTypeScanner.java @@ -0,0 +1,113 @@ +/* + * Copyright 2014 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.util; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.ClassUtils; + +/** + * Scanner to find types with annotations on the classpath. + * + * @author Oliver Gierke + */ +public class AnnotatedTypeScanner { + + private final Iterable> annotationTypess; + private final boolean considerInterfaces; + + /** + * Creates a new {@link AnnotatedTypeScanner} for the given annotation types. + * + * @param annotationTypes the annotation types to scan for. + */ + public AnnotatedTypeScanner(Class... annotationTypes) { + this(true, annotationTypes); + } + + /** + * Creates a new {@link AnnotatedTypeScanner} for the given annotation types. + * + * @param considerInterfaces whether to consider interfaces as well. + * @param annotationTypes the annotations to scan for. + */ + public AnnotatedTypeScanner(boolean considerInterfaces, Class... annotationTypes) { + + this.annotationTypess = Arrays.asList(annotationTypes); + this.considerInterfaces = considerInterfaces; + } + + public Set> findTypes(String... basePackages) { + return findTypes(Arrays.asList(basePackages)); + } + + public Set> findTypes(Iterable basePackages) { + + ClassPathScanningCandidateComponentProvider provider = new InterfaceAwareScanner(considerInterfaces); + + for (Class annotationType : annotationTypess) { + provider.addIncludeFilter(new AnnotationTypeFilter(annotationType, true, considerInterfaces)); + } + + Set> types = new HashSet>(); + + for (String basePackage : basePackages) { + + for (BeanDefinition definition : provider.findCandidateComponents(basePackage)) { + try { + types.add(ClassUtils.forName(definition.getBeanClassName(), getClass().getClassLoader())); + } catch (ClassNotFoundException o_O) { + throw new IllegalStateException(o_O); + } + } + } + + return types; + } + + /** + * Custom extension of {@link ClassPathScanningCandidateComponentProvider} to make sure interfaces to not get dropped + * from scanning results. + * + * @author Oliver Gierke + */ + private static class InterfaceAwareScanner extends ClassPathScanningCandidateComponentProvider { + + private final boolean considerInterfaces; + + public InterfaceAwareScanner(boolean considerInterfaces) { + super(false); + this.considerInterfaces = considerInterfaces; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(org.springframework.beans.factory.annotation.AnnotatedBeanDefinition) + */ + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + return super.isCandidateComponent(beanDefinition) || considerInterfaces + && beanDefinition.getMetadata().isInterface(); + } + } +} diff --git a/src/main/java/org/springframework/data/util/AnnotationDetectionMethodCallback.java b/src/main/java/org/springframework/data/util/AnnotationDetectionMethodCallback.java new file mode 100644 index 000000000..2c5b46d38 --- /dev/null +++ b/src/main/java/org/springframework/data/util/AnnotationDetectionMethodCallback.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014 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.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils.MethodCallback; + +/** + * {@link MethodCallback} to find annotations of a given type. + * + * @author Oliver Gierke + */ +public class AnnotationDetectionMethodCallback implements MethodCallback { + + private static final String MULTIPLE_FOUND = "Found annotation %s both on %s and %s! Make sure only one of them is annotated with it!"; + + private final boolean enforceUniqueness; + private final Class annotationType; + + private Method foundMethod; + private A annotation; + + /** + * Creates a new {@link AnnotationDetectionMethodCallback} for the given annotation type. + * + * @param annotationType must not be {@literal null}. + */ + public AnnotationDetectionMethodCallback(Class annotationType) { + this(annotationType, false); + } + + /** + * Creates a new {@link AnnotationDetectionMethodCallback} for the given annotation type. + * + * @param annotationType must not be {@literal null}. + * @param enforceUniqueness whether to fail if multiple methods with the annotation are found. + */ + public AnnotationDetectionMethodCallback(Class annotationType, boolean enforceUniqueness) { + + Assert.notNull(annotationType, "Annotation type must not be null!"); + + this.annotationType = annotationType; + this.enforceUniqueness = enforceUniqueness; + } + + /** + * @return the method + */ + public Method getMethod() { + return foundMethod; + } + + /** + * @return the annotation + */ + public A getAnnotation() { + return annotation; + } + + /** + * Returns whether an annotation was found. + * + * @return + */ + public boolean hasFoundAnnotation() { + return annotation != null; + } + + /* + * (non-Javadoc) + * @see org.springframework.util.ReflectionUtils.MethodCallback#doWith(java.lang.reflect.Method) + */ + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + + if (foundMethod != null && !enforceUniqueness) { + return; + } + + A annotation = AnnotationUtils.findAnnotation(method, annotationType); + + if (annotation != null) { + + if (foundMethod != null && enforceUniqueness) { + throw new IllegalStateException(String.format(MULTIPLE_FOUND, annotation.getClass().getName(), foundMethod, + method)); + } + + this.annotation = annotation; + this.foundMethod = method; + } + } +} diff --git a/src/main/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapper.java b/src/main/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapper.java new file mode 100644 index 000000000..adf961037 --- /dev/null +++ b/src/main/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2014 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.util; + +import static org.springframework.util.ReflectionUtils.*; + +import java.lang.reflect.Field; + +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.NotWritablePropertyException; + +/** + * Custom extension of {@link BeanWrapperImpl} that falls back to direct field access in case the object or type being + * wrapped does not use accessor methods. + * + * @author Oliver Gierke + */ +public class DirectFieldAccessFallbackBeanWrapper extends BeanWrapperImpl { + + public DirectFieldAccessFallbackBeanWrapper(Object entity) { + super(entity); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.BeanWrapperImpl#getPropertyValue(java.lang.String) + */ + @Override + public Object getPropertyValue(String propertyName) { + + try { + return super.getPropertyValue(propertyName); + } catch (NotReadablePropertyException e) { + + Field field = findField(getWrappedClass(), propertyName); + + if (field == null) { + throw new NotReadablePropertyException(getWrappedClass(), propertyName, + "Could not find field for property during fallback access!"); + } + + makeAccessible(field); + return getField(field, getWrappedInstance()); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.BeanWrapperImpl#setPropertyValue(java.lang.String, java.lang.Object) + */ + @Override + public void setPropertyValue(String propertyName, Object value) { + + try { + super.setPropertyValue(propertyName, value); + } catch (NotWritablePropertyException e) { + + Field field = findField(getWrappedClass(), propertyName); + + if (field == null) { + throw new NotWritablePropertyException(getWrappedClass(), propertyName, + "Could not find field for property during fallback access!"); + } + + makeAccessible(field); + setField(field, getWrappedInstance(), value); + } + } +} diff --git a/src/test/java/org/springframework/data/util/AnnotatedTypeScannerUnitTests.java b/src/test/java/org/springframework/data/util/AnnotatedTypeScannerUnitTests.java new file mode 100644 index 000000000..a7d81d098 --- /dev/null +++ b/src/test/java/org/springframework/data/util/AnnotatedTypeScannerUnitTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.util; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Set; + +import org.junit.Test; +import org.springframework.data.annotation.Persistent; + +/** + * Unit tests for {@link AnnotatedTypeScannerUnitTests}. + * + * @author Oliver Gierke + */ +public class AnnotatedTypeScannerUnitTests { + + /** + * @see DATACMNS-452 + */ + @Test + @SuppressWarnings("unchecked") + public void findsAnnotatedTypes() { + + AnnotatedTypeScanner scanner = new AnnotatedTypeScanner(Persistent.class); + Set> types = scanner.findTypes(AnnotatedTypeScanner.class.getPackage().getName()); + + assertThat(types, hasItem(Type.class)); + } + + @Persistent + static class Type { + + } +} diff --git a/src/test/java/org/springframework/data/util/AnnotationDetectionMethodCallbackUnitTests.java b/src/test/java/org/springframework/data/util/AnnotationDetectionMethodCallbackUnitTests.java new file mode 100644 index 000000000..eb8e72ebc --- /dev/null +++ b/src/test/java/org/springframework/data/util/AnnotationDetectionMethodCallbackUnitTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2014 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.util; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for {@link AnnotationDetectionMethodCallback}. + * + * @author Oliver Gierke + */ +public class AnnotationDetectionMethodCallbackUnitTests { + + public @Rule ExpectedException exception = ExpectedException.none(); + + /** + * @see DATACMNS-452 + */ + @Test + public void findsMethodWithAnnotation() throws Exception { + + AnnotationDetectionMethodCallback callback = new AnnotationDetectionMethodCallback(Value.class); + ReflectionUtils.doWithMethods(Sample.class, callback); + + assertThat(callback.hasFoundAnnotation(), is(true)); + assertThat(callback.getMethod(), is(Sample.class.getMethod("getValue"))); + assertThat(callback.getAnnotation(), is(notNullValue())); + assertThat(callback.getAnnotation().value(), is("#{null}")); + } + + /** + * @see DATACMNS-452 + */ + @Test + public void detectsAmbiguousAnnotations() { + + exception.expect(IllegalStateException.class); + exception.expectMessage("Value"); + exception.expectMessage("getValue"); + exception.expectMessage("getOtherValue"); + + AnnotationDetectionMethodCallback callback = new AnnotationDetectionMethodCallback(Value.class, true); + ReflectionUtils.doWithMethods(Multiple.class, callback); + } + + interface Sample { + + @Value("#{null}") + Object getValue(); + } + + interface Multiple { + + @Value("#{null}") + Object getValue(); + + @Value("#{null}") + Object getOtherValue(); + } +} diff --git a/src/test/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapperUnitTests.java b/src/test/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapperUnitTests.java new file mode 100644 index 000000000..74b165f3d --- /dev/null +++ b/src/test/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapperUnitTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2014 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.util; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.NotReadablePropertyException; +import org.springframework.beans.NotWritablePropertyException; + +/** + * Unit tests for {@link DirectFieldAccessFallbackBeanWrapper}. + * + * @author Oliver Gierke + */ +public class DirectFieldAccessFallbackBeanWrapperUnitTests { + + /** + * @see DATACMNS-452 + */ + @Test + public void usesFieldAccessForReadIfNoAccessorCanBeFound() { + + Sample sample = new Sample(); + sample.firstname = "Dave"; + + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(sample); + + assertThat(wrapper.getPropertyValue("firstname"), is((Object) "Dave")); + } + + /** + * @see DATACMNS-452 + */ + @Test + public void usesFieldAccessForWriteIfNoAccessorCanBeFound() { + + Sample sample = new Sample(); + + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(sample); + wrapper.setPropertyValue("firstname", "Dave"); + + assertThat(sample.firstname, is("Dave")); + } + + /** + * @see DATACMNS-452 + */ + @Test(expected = NotReadablePropertyException.class) + public void throwsAppropriateExceptionIfNoFieldFoundForRead() { + + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(new Sample()); + wrapper.getPropertyValue("lastname"); + } + + /** + * @see DATACMNS-452 + */ + @Test(expected = NotWritablePropertyException.class) + public void throwsAppropriateExceptionIfNoFieldFoundForWrite() { + + BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(new Sample()); + wrapper.setPropertyValue("lastname", "Matthews"); + } + + static class Sample { + + String firstname; + } +}