From 6408e16faf8fa3d4fbcd65e5cd088f126704ede1 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 26 Feb 2014 12:52:38 +0100 Subject: [PATCH] DATACMNS-452 - Added some helper types to simplify the implementations in store modules. Added AnnotatedTypeScanner to easily scan for annotated types within a set of base packages. Picks up interfaces by default which had to be customized when using ClassPathScanningCanidateComponentProvider. Added DirectFieldAccessFallbackBeanWrapper that will use direct field access for beans in case no accessor method can be found. This allows to easily implement bean property values through accessors first but using the field directly if the accessors aren't available. Introduced AnnotationDetectionMethodCallback to easily find methods equipped with a given annotation. --- .../data/util/AnnotatedTypeScanner.java | 113 ++++++++++++++++++ .../AnnotationDetectionMethodCallback.java | 110 +++++++++++++++++ .../DirectFieldAccessFallbackBeanWrapper.java | 83 +++++++++++++ .../util/AnnotatedTypeScannerUnitTests.java | 50 ++++++++ ...ationDetectionMethodCallbackUnitTests.java | 80 +++++++++++++ ...eldAccessFallbackBeanWrapperUnitTests.java | 85 +++++++++++++ 6 files changed, 521 insertions(+) create mode 100644 src/main/java/org/springframework/data/util/AnnotatedTypeScanner.java create mode 100644 src/main/java/org/springframework/data/util/AnnotationDetectionMethodCallback.java create mode 100644 src/main/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapper.java create mode 100644 src/test/java/org/springframework/data/util/AnnotatedTypeScannerUnitTests.java create mode 100644 src/test/java/org/springframework/data/util/AnnotationDetectionMethodCallbackUnitTests.java create mode 100644 src/test/java/org/springframework/data/util/DirectFieldAccessFallbackBeanWrapperUnitTests.java 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; + } +}