diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/BeanWrapper.java b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/BeanWrapper.java new file mode 100644 index 000000000..582a7f52f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/BeanWrapper.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2011 by the original author(s). + * + * 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.mapping; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.data.mapping.model.MappingInstantiationException; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.PersistentEntity; +import org.springframework.data.mapping.model.PersistentProperty; +import org.springframework.data.mapping.model.PreferredConstructor; +import org.springframework.data.mapping.model.PreferredConstructor.Parameter; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Value object to allow creation of objects using the metamodel, setting and getting properties. + * + * @author Oliver Gierke + */ +public class BeanWrapper, T> { + + private final T bean; + private final ConversionService conversionService; + + /** + * Creates a new {@link BeanWrapper} for the given bean instance and {@link ConversionService}. If + * {@link ConversionService} is {@literal null} no property type conversion will take place. + * + * @param + * @param + * @param bean must not be {@literal null} + * @param conversionService + * @return + */ + public static , T> BeanWrapper create(T bean, + ConversionService conversionService) { + return new BeanWrapper(bean, conversionService); + } + + /** + * Creates a new {@link BeanWrapper} using the given {@link PersistentEntity} and {@link ParameterValueProvider}. Will + * instantly create a bean instance using the {@link PreferredConstructor} of the {@link PersistentEntity}. + * + * @param + * @param + * @param entity + * @param provider + * @param conversionService + * @return + */ + public static , T> BeanWrapper create(E entity, + ParameterValueProvider provider, ConversionService conversionService) { + return new BeanWrapper(entity, provider, conversionService); + } + + private BeanWrapper(T bean, ConversionService conversionService) { + Assert.notNull(bean); + this.bean = bean; + this.conversionService = conversionService; + } + + @SuppressWarnings("unchecked") + private BeanWrapper(E entity, ParameterValueProvider provider, ConversionService conversionService) { + + this.conversionService = conversionService; + + T bean = null; + + PreferredConstructor constructor = entity.getPreferredConstructor(); + if (null == constructor) { + try { + Class clazz = entity.getType(); + if (clazz.isArray()) { + Class ctype = clazz; + int dims = 0; + while (ctype.isArray()) { + ctype = ctype.getComponentType(); + dims++; + } + bean = (T) Array.newInstance(clazz, dims); + } else { + bean = BeanUtils.instantiateClass(entity.getType()); + } + } catch (BeanInstantiationException e) { + throw new MappingInstantiationException(e.getMessage(), e); + } + } + + List params = new LinkedList(); + if (null != provider && constructor.hasParameters()) { + for (Parameter parameter : constructor.getParameters()) { + params.add(provider.getParameterValue(parameter)); + } + } + + try { + bean = BeanUtils.instantiateClass(constructor.getConstructor(), params.toArray()); + } catch (BeanInstantiationException e) { + throw new MappingInstantiationException(e.getMessage(), e); + } + + this.bean = bean; + } + + /** + * Sets the given {@link PersistentProperty} to the given value. Will do type conversion if a + * {@link ConversionService} is configured. Will use the accessor method of the given {@link PersistentProperty} if it + * has one or field access otherwise. + * + * @param property + * @param value + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public void setProperty(PersistentProperty property, Object value) throws IllegalAccessException, + InvocationTargetException { + setProperty(property, value, false); + } + + /** + * Sets the given {@link PersistentProperty} to the given value. Will do type conversion if a + * {@link ConversionService} is configured. + * + * @param property + * @param value + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public void setProperty(PersistentProperty property, Object value, boolean fieldAccessOnly) + throws IllegalAccessException, InvocationTargetException { + + Method setter = property.getPropertyDescriptor() != null ? property.getPropertyDescriptor().getWriteMethod() : null; + + if (fieldAccessOnly || null == setter) { + Object valueToSet = getPotentiallyConvertedValue(value, property.getType()); + ReflectionUtils.makeAccessible(property.getField()); + ReflectionUtils.setField(property.getField(), bean, valueToSet); + return; + } + + Class[] paramTypes = setter.getParameterTypes(); + Object valueToSet = getPotentiallyConvertedValue(value, paramTypes[0]); + ReflectionUtils.makeAccessible(setter); + ReflectionUtils.invokeMethod(setter, bean, valueToSet); + } + + /** + * Returns the value of the given {@link PersistentProperty} of the underlying bean instance. + * + * @param + * @param property + * @return + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public Object getProperty(PersistentProperty property) + throws IllegalAccessException, InvocationTargetException { + return getProperty(property, property.getType(), false); + } + + /** + * Returns the value of the given {@link PersistentProperty} potentially converted to the given type. + * + * @param + * @param property + * @param type + * @param fieldAccessOnly + * @return + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + public S getProperty(PersistentProperty property, Class type, boolean fieldAccessOnly) + throws IllegalAccessException, InvocationTargetException { + Object obj; + Field field = property.getField(); + Method getter = (null != property.getPropertyDescriptor() ? property.getPropertyDescriptor().getReadMethod() : null); + if (fieldAccessOnly || null == getter) { + ReflectionUtils.makeAccessible(field); + obj = ReflectionUtils.getField(field, bean); + } else { + ReflectionUtils.makeAccessible(getter); + obj = ReflectionUtils.invokeMethod(getter, bean); + } + + return getPotentiallyConvertedValue(obj, type); + } + + /** + * Converts the given source value if it is not assignable to the given target type. + * + * @param source + * @param targetType + * @return + */ + @SuppressWarnings("unchecked") + private S getPotentiallyConvertedValue(Object source, Class targetType) { + + boolean conversionServiceAvailable = conversionService != null; + boolean conversionNeeded = source == null || !source.getClass().isAssignableFrom(targetType); + + if (conversionServiceAvailable && conversionNeeded) { + return conversionService.convert(source, targetType); + } + + return (S) source; + } + + /** + * Returns the underlying bean instance. + * + * @return + */ + public T getBean() { + return bean; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/MappingBeanHelper.java b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/MappingBeanHelper.java index 24e4d1cfb..8c21897b3 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/MappingBeanHelper.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/MappingBeanHelper.java @@ -16,44 +16,21 @@ package org.springframework.data.mapping; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.Date; import java.util.Locale; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.BeanInstantiationException; -import org.springframework.beans.BeanUtils; -import org.springframework.core.convert.support.ConversionServiceFactory; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.data.mapping.model.MappingInstantiationException; -import org.springframework.data.mapping.model.PersistentEntity; -import org.springframework.data.mapping.model.PersistentProperty; -import org.springframework.data.mapping.model.PreferredConstructor; -import org.springframework.data.mapping.model.PreferredConstructor.Parameter; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.Expression; -import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.ReflectionUtils; - /** + * Helper class to set and retrieve bean values. + * * @author Jon Brisbin * @author Oliver Gierke */ public abstract class MappingBeanHelper { - protected static GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); - protected static SpelExpressionParser parser = new SpelExpressionParser(); - protected static Set> simpleTypes = Collections.newSetFromMap(new ConcurrentHashMap, Boolean>()); + private static final Set> simpleTypes = Collections.newSetFromMap(new ConcurrentHashMap, Boolean>()); static { simpleTypes.add(boolean.class); @@ -81,23 +58,26 @@ public abstract class MappingBeanHelper { simpleTypes.add(Double.class); simpleTypes.add(Character.class); simpleTypes.add(String.class); - simpleTypes.add(java.util.Date.class); + simpleTypes.add(Date.class); simpleTypes.add(Locale.class); simpleTypes.add(Class.class); } - public static GenericConversionService getConversionService() { - return conversionService; - } - - public static void setConversionService(GenericConversionService conversionService) { - MappingBeanHelper.conversionService = conversionService; - } - + /** + * Returns the set of types considered to be simple. + * + * @return + */ public static Set> getSimpleTypes() { return simpleTypes; } + /** + * Returns whether the given type is considered a simple one. + * + * @param type + * @return + */ public static boolean isSimpleType(Class type) { for (Class clazz : simpleTypes) { if (type == clazz || type.isAssignableFrom(clazz)) { @@ -106,122 +86,4 @@ public abstract class MappingBeanHelper { } return type.isEnum(); } - - public static > T constructInstance(PersistentEntity entity, PreferredConstructor.ParameterValueProvider provider) { - return constructInstance(entity, provider, new StandardEvaluationContext()); - } - - @SuppressWarnings({"unchecked"}) - public static > T constructInstance(PersistentEntity entity, PreferredConstructor.ParameterValueProvider provider, EvaluationContext spelCtx) { - - PreferredConstructor constructor = entity.getPreferredConstructor(); - if (null == constructor) { - try { - Class clazz = entity.getType(); - if (clazz.isArray()) { - Class ctype = clazz; - int dims = 0; - while (ctype.isArray()) { - ctype = ctype.getComponentType(); - dims++; - } - return (T) Array.newInstance(clazz, dims); - } else { - return BeanUtils.instantiateClass(entity.getType()); - } - } catch (BeanInstantiationException e) { - throw new MappingInstantiationException(e.getMessage(), e); - } - } - - List params = new LinkedList(); - if (null != provider && constructor.getParameters().size() > 0) { - for (Parameter parameter : constructor.getParameters()) { - String key = parameter.getKey(); - Object obj; - if (null != key) { - Expression x = parser.parseExpression(key); - obj = x.getValue(spelCtx); - } else { - obj = provider.getParameterValue(parameter); - } - params.add(obj); - } - } - - T obj = null; - try { - obj = BeanUtils.instantiateClass(constructor.getConstructor(), params.toArray()); - } catch (BeanInstantiationException e) { - throw new MappingInstantiationException(e.getMessage(), e); - } - - return obj; - } - - public static void setProperty(Object on, - PersistentProperty property, - Object value) - throws IllegalAccessException, InvocationTargetException { - setProperty(on, property, value, false); - } - - public static void setProperty(Object on, - PersistentProperty property, - Object value, - boolean fieldAccessOnly) - throws IllegalAccessException, InvocationTargetException { - - Method setter = property.getPropertyDescriptor() != null ? property.getPropertyDescriptor().getWriteMethod() : null; - - if (fieldAccessOnly || null == setter) { - Object valueToSet = getPotentiallyConvertedValue(value, property.getType()); - ReflectionUtils.makeAccessible(property.getField()); - ReflectionUtils.setField(property.getField(), on, valueToSet); - return; - } - - Class[] paramTypes = setter.getParameterTypes(); - Object valueToSet = getPotentiallyConvertedValue(value, paramTypes[0]); - ReflectionUtils.makeAccessible(setter); - ReflectionUtils.invokeMethod(setter, on, valueToSet); - } - - /** - * Converts the given source value if it is not assignable to the given target type. - * - * @param source - * @param targetType - * @return - */ - private static Object getPotentiallyConvertedValue(Object source, Class targetType) { - if (source != null && source.getClass().isAssignableFrom(targetType)) { - return source; - } - - return conversionService.convert(source, targetType); - } - - @SuppressWarnings({"unchecked"}) - public static T getProperty(Object from, - PersistentProperty property, - Class type, - boolean fieldAccessOnly) - throws IllegalAccessException, InvocationTargetException { - Object obj; - Field field = property.getField(); - Method getter = (null != property.getPropertyDescriptor() ? property.getPropertyDescriptor().getReadMethod() : null); - if (fieldAccessOnly || null == getter) { - ReflectionUtils.makeAccessible(field); - obj = ReflectionUtils.getField(field, from); - } else { - ReflectionUtils.makeAccessible(getter); - obj = ReflectionUtils.invokeMethod(getter, from); - } - if (null != obj && !type.isAssignableFrom(obj.getClass())) { - return conversionService.convert(obj, type); - } else { - return (T) obj; - } - } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java new file mode 100644 index 000000000..f6e167696 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/ParameterValueProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2011 by the original author(s). + * + * 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.mapping.model; + +import org.springframework.data.mapping.model.PreferredConstructor.Parameter; + +/** + * Callback interface to lookup values for a given {@link Parameter}. + * + * @author Oliver Gierke + */ +public interface ParameterValueProvider { + T getParameterValue(PreferredConstructor.Parameter parameter); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/PreferredConstructor.java b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/PreferredConstructor.java index be98a1b01..9a4b6a431 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/PreferredConstructor.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/PreferredConstructor.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.mapping.model; import java.lang.annotation.Annotation; @@ -29,7 +28,7 @@ import org.springframework.util.ReflectionUtils; /** * Value object to encapsulate the constructor to be used when mapping persistent data to objects. - * + * * @author Jon Brisbin * @author Oliver Gierke */ @@ -38,23 +37,54 @@ public class PreferredConstructor { private final Constructor constructor; private final List> parameters; + /** + * Creates a new {@link PreferredConstructor} from the given {@link Constructor} and {@link Parameter}s. + * + * @param constructor + * @param parameters + */ public PreferredConstructor(Constructor constructor, Parameter... parameters) { + + Assert.notNull(constructor); + Assert.notNull(parameters); + ReflectionUtils.makeAccessible(constructor); this.constructor = constructor; this.parameters = Arrays.asList(parameters); } + /** + * Returns the underlying {@link Constructor}. + * + * @return + */ public Constructor getConstructor() { return constructor; } - public List> getParameters() { + /** + * Returns the {@link Parameter}s of the constructor. + * + * @return + */ + public Iterable> getParameters() { return parameters; } + + /** + * Returns whether the constructor has {@link Parameter}s. + * + * @see #isNoArgConstructor() + * @return + */ + public boolean hasParameters() { + return !parameters.isEmpty(); + } /** * Returns whether the constructor does not have any arguments. - * + * + * @see #hasParameters() * @return */ public boolean isNoArgConstructor() { @@ -63,18 +93,35 @@ public class PreferredConstructor { /** * Returns whether the constructor was explicitly selected (by {@link PersistenceConstructor}). - * + * * @return */ public boolean isExplicitlyAnnotated() { return constructor.isAnnotationPresent(PersistenceConstructor.class); } + /** + * Value object to represent constructor parameters. + * + * @param + * the type of the paramter + * @author Oliver Gierke + */ public static class Parameter { + private final String name; private final TypeInformation type; private final String key; + /** + * Creates a new {@link Parameter} with the given name, {@link TypeInformation} as well as an array of + * {@link Annotation}s. Will insprect the annotations for an {@link Value} annotation to lookup a key or an SpEL + * expression to be evaluated. + * + * @param name the name of the parameter, can be {@literal null} + * @param type must not be {@literal null} + * @param annotations must not be {@literal null} but can be empty + */ public Parameter(String name, TypeInformation type, Annotation[] annotations) { Assert.notNull(type); @@ -94,25 +141,40 @@ public class PreferredConstructor { return null; } + /** + * Returns the name of the parameter or {@literal null} if none was given. + * + * @return + */ public String getName() { return name; } + /** + * Returns the {@link TypeInformation} of the parameter. + * + * @return + */ public TypeInformation getType() { return type; } + /** + * Returns the raw resolved type of the parameter. + * + * @return + */ public Class getRawType() { return type.getType(); } + /** + * Returns the key to be used when looking up a source data structure to populate the actual parameter value. + * + * @return + */ public String getKey() { return key; } } - - public static interface ParameterValueProvider { - T getParameterValue(Parameter parameter); - } - } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/SpELAwareParameterValueProvider.java b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/SpELAwareParameterValueProvider.java new file mode 100644 index 000000000..919f46225 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/mapping/model/SpELAwareParameterValueProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2011 by the original author(s). + * + * 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.mapping.model; + +import org.springframework.data.mapping.model.PreferredConstructor.Parameter; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * {@link ParameterValueProvider} implementation that evaluates the {@link Parameter}s key against + * {@link SpelExpressionParser} and {@link EvaluationContext}. + * + * @author Oliver Gierke + */ +public class SpELAwareParameterValueProvider implements ParameterValueProvider { + + private final SpelExpressionParser parser; + private final EvaluationContext context; + + /** + * Creates a new {@link SpELAwareParameterValueProvider} from the given {@link SpelExpressionParser} and {@link EvaluationContext}. + * + * @param parser must not be {@literal null} + * @param context must not be {@literal null} + */ + public SpELAwareParameterValueProvider(SpelExpressionParser parser, EvaluationContext context) { + Assert.notNull(parser); + Assert.notNull(context); + this.parser = parser; + this.context = context; + } + + /* (non-Javadoc) + * @see org.springframework.data.mapping.model.PreferredConstructor.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.model.PreferredConstructor.Parameter) + */ + @SuppressWarnings("unchecked") + public T getParameterValue(Parameter parameter) { + Expression expression = parser.parseExpression(parameter.getKey()); + return (T) expression.getValue(context); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java index 3e5d24791..ff6d81d7b 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/mapping/PreferredConstructorDiscovererUnitTests.java @@ -18,6 +18,7 @@ package org.springframework.data.mapping; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import java.util.Iterator; import java.util.List; import org.junit.Test; @@ -83,10 +84,12 @@ public class PreferredConstructorDiscovererUnitTests { assertThat(constructor.isNoArgConstructor(), is(false)); assertThat(constructor.isExplicitlyAnnotated(), is(true)); - List> parameters = constructor.getParameters(); + assertThat(constructor.hasParameters(), is(true)); + Iterator> parameters = constructor.getParameters().iterator(); - assertThat(parameters.size(), is(1)); - assertThat(parameters.get(0).getType().getType(), typeCompatibleWith(Long.class)); + Parameter parameter = parameters.next(); + assertThat(parameter.getType().getType(), typeCompatibleWith(Long.class)); + assertThat(parameters.hasNext(), is(false)); } static class EntityWithoutConstructor {