/*
* Copyright 2002-2011 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.flex.core.io;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import flex.messaging.io.BeanProxy;
import flex.messaging.io.PropertyProxy;
import flex.messaging.io.amf.ASObject;
/**
* Spring {@link ConversionService}-aware {@link PropertyProxy} that seeks to find an appropriate converter for
* a given bean property during AMF serialization and deserialization.
*
* <p>
* Uses Spring's {@link PropertyAccessor} interface for all property access, allowing for optional direct field access
* on the objects being serialized/deserialized.
*
* @author Jeremy Grelle
* @author Jose Barragan
*/
public class SpringPropertyProxy extends BeanProxy {
private static final Log log = LogFactory.getLog(SpringPropertyProxy.class);
private static final long serialVersionUID = 5374027421774405789L;
private List<String> propertyNames;
protected final ConversionService conversionService;
protected final Class<?> beanType;
protected final boolean useDirectFieldAccess;
/**
* Factory method for creating correctly configured Spring property proxy instances.
* @param beanType the type being introspected
* @param useDirectFieldAccess whether to access fields directly
* @param conversionService the conversion service to use for property type conversion
* @return a properly configured property proxy
*/
public static SpringPropertyProxy proxyFor(Class<?> beanType, boolean useDirectFieldAccess, ConversionService conversionService) {
if(PropertyProxyUtils.hasAmfCreator(beanType)) {
SpringPropertyProxy proxy = new DelayedWriteSpringPropertyProxy(beanType, useDirectFieldAccess, conversionService);
return proxy;
} else {
Assert.isTrue(beanType.isEnum() || ClassUtils.hasConstructor(beanType), "Failed to create SpringPropertyProxy for "+beanType.getName()+" - Classes mapped " +
"for deserialization from AMF must have either a no-arg default constructor, " +
"or a constructor annotated with "+AmfCreator.class.getName());
SpringPropertyProxy proxy = new SpringPropertyProxy(beanType, useDirectFieldAccess, conversionService);
try {
//If possible, create an instance to introspect and cache the property names
Object instance = BeanUtils.instantiate(beanType);
proxy.setPropertyNames(PropertyProxyUtils.findPropertyNames(conversionService, useDirectFieldAccess, instance));
} catch(BeanInstantiationException ex) {
//Property names can't be cached, but this is ok
}
return proxy;
}
}
private SpringPropertyProxy(Class<?> beanType, boolean useDirectFieldAccess, ConversionService conversionService){
super(null);
this.beanType = beanType;
this.useDirectFieldAccess = useDirectFieldAccess;
this.conversionService = conversionService;
}
/**
* The type for which this {@link PropertyProxy} is registered.
* @return the bean type
*/
public Class<?> getBeanType() {
return beanType;
}
/**
* {@inheritDoc}
*
* Delegates to the configured {@link ConversionService} to potentially convert the instance to the registered bean type.
*/
@Override
public Object getInstanceToSerialize(Object instance) {
if (this.conversionService.canConvert(instance.getClass(), this.beanType)) {
return this.conversionService.convert(instance, this.beanType);
} else {
return instance;
}
}
/**
* {@inheritDoc}
*/
@Override
public List<String> getPropertyNames(Object instance) {
if (!CollectionUtils.isEmpty(propertyNames) && instance.getClass().equals(this.beanType)) {
return this.propertyNames;
} else {
return PropertyProxyUtils.findPropertyNames(this.conversionService, this.useDirectFieldAccess, instance);
}
}
/**
* {@inheritDoc}
*/
@Override
public Class<?> getType(Object instance, String propertyName) {
return PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance).getPropertyType(propertyName);
}
/**
* {@inheritDoc}
*
* Delegates to the configured {@link ConversionService} to potentially convert the current value to the actual type of the property.
*/
@Override
public Object getValue(Object instance, String propertyName) {
PropertyAccessor accessor = PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance);
Object value = accessor.getPropertyValue(propertyName);
if(log.isDebugEnabled()) {
getType(instance, propertyName);
log.debug("Actual type of value for property '"+propertyName+"' on instance "+instance+" is "+(value != null ? value.getClass() : null));
}
TypeDescriptor targetType = accessor.getPropertyTypeDescriptor(propertyName);
TypeDescriptor sourceType = value == null ? targetType : TypeDescriptor.valueOf(value.getClass());
if (this.conversionService.canConvert(sourceType, targetType)) {
value = this.conversionService.convert(value, sourceType, targetType);
}
return value;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWriteOnly(Object instance, String propertyName) {
PropertyAccessor accessor = PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance);
return isReadIgnored(instance, propertyName) || (!accessor.isReadableProperty(propertyName) && accessor.isWritableProperty(propertyName));
}
/**
* {@inheritDoc}
*
* Delegates to the configured {@link ConversionService} to potentially convert the value to the actual type of the property.
*/
@Override
public void setValue(Object instance, String propertyName, Object value) {
if (!isWriteIgnored(instance, propertyName)) {
PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance).setPropertyValue(propertyName, value);
}
}
private void setPropertyNames(List<String> propertyNames) {
this.propertyNames = propertyNames;
}
private boolean isReadIgnored(Object instance, String propertyName) {
PropertyAccessor accessor = PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance);
if (!accessor.isReadableProperty(propertyName)) {
return true;
}
if (this.useDirectFieldAccess) {
AmfIgnoreField ignoreField = accessor.getPropertyTypeDescriptor(propertyName).getAnnotation(AmfIgnoreField.class);
return ignoreField != null && ignoreField.onSerialization();
} else {
PropertyDescriptor pd = ((BeanWrapper)accessor).getPropertyDescriptor(propertyName);
return pd.getReadMethod().getAnnotation(AmfIgnore.class) != null;
}
}
private boolean isWriteIgnored(Object instance, String propertyName) {
PropertyAccessor accessor = PropertyProxyUtils.getPropertyAccessor(this.conversionService, this.useDirectFieldAccess, instance);
if (!accessor.isWritableProperty(propertyName)) {
return true;
}
if (this.useDirectFieldAccess) {
AmfIgnoreField ignoreField = accessor.getPropertyTypeDescriptor(propertyName).getAnnotation(AmfIgnoreField.class);
return ignoreField != null && ignoreField.onDeserialization();
} else {
PropertyDescriptor pd = ((BeanWrapper)accessor).getPropertyDescriptor(propertyName);
return pd.getWriteMethod().getAnnotation(AmfIgnore.class) != null;
}
}
/**
* Extension to {@link SpringPropertyProxy} that allow for use of classes that lack default no-arg constructors and instead have
* a constructor annotated with {@link AmfCreator}.
*
* @author Jeremy Grelle
*/
public static final class DelayedWriteSpringPropertyProxy extends SpringPropertyProxy {
private static final long serialVersionUID = -5330475591068260312L;
private final Constructor<?> amfConstructor;
private final List<String> paramNames = new ArrayList<String>();
private DelayedWriteSpringPropertyProxy(Class<?> beanType, boolean useDirectFieldAccess, ConversionService conversionService) {
super(beanType, useDirectFieldAccess, conversionService);
this.amfConstructor = findAmfConstructor();
}
/**
* {@inheritDoc}
*/
@Override
public Object createInstance(String className) {
Assert.isTrue(this.beanType.getName().equals(className), "Asked to create instance of an unknown type.");
return new ASObject(className);
}
/**
* {@inheritDoc}
*/
@Override
public Object instanceComplete(Object instance) {
Assert.isInstanceOf(ASObject.class, instance, "Expected an instance of "+ASObject.class.getName());
ASObject sourceInstance = (ASObject) instance;
Assert.notNull(sourceInstance.getType(), "Expected an explicit type to be set on the ASObject instance passed to this PropertyProxy.");
Object targetInstance = createTargetInstance(sourceInstance);
applyPropertyValues(sourceInstance, targetInstance);
return targetInstance;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public void setValue(Object instance, String propertyName, Object value) {
if (instance instanceof ASObject) {
((ASObject) instance).put(propertyName, value);
} else {
super.setValue(instance, propertyName, value);
}
}
private Object createTargetInstance(ASObject sourceInstance) {
Object[] params = new Object[this.paramNames.size()];
for (int i=0; i<params.length; i++) {
Object value = sourceInstance.remove(this.paramNames.get(i));
TypeDescriptor targetType = TypeDescriptor.valueOf(this.amfConstructor.getParameterTypes()[i]);
TypeDescriptor sourceType = value == null ? targetType : TypeDescriptor.valueOf(value.getClass());
if (this.conversionService.canConvert(sourceType, targetType)) {
value = this.conversionService.convert(value, sourceType, targetType);
}
params[i] = value;
}
try {
return this.amfConstructor.newInstance(params);
} catch (Exception ex) {
throw new IllegalArgumentException("Failed to invoke constructor marked with "+AmfCreator.class.getName()+" for type "+this.beanType, ex);
}
}
private Object applyPropertyValues(ASObject sourceInstance, Object targetInstance) {
for (Object property : sourceInstance.keySet()) {
setValue(targetInstance, property.toString(), sourceInstance.get(property));
}
return targetInstance;
}
private Constructor<?> findAmfConstructor() {
for (Constructor<?> c : this.beanType.getConstructors()) {
if (c.isAnnotationPresent(AmfCreator.class)) {
Assert.isTrue(c.getParameterAnnotations().length == c.getParameterTypes().length, "Found a constructor marked with "+AmfCreator.class.getName()+" but not all of its parameters are marked with "+AmfProperty.class.getName());
for (Annotation[] paramAnnotations : c.getParameterAnnotations()) {
boolean hasAmfProperty = false;
for (Annotation paramAnnotation : paramAnnotations) {
if (paramAnnotation.annotationType().equals(AmfProperty.class)) {
hasAmfProperty = true;
this.paramNames.add(((AmfProperty)paramAnnotation).value());
break;
}
}
Assert.isTrue(hasAmfProperty, "Found a constructor marked with "+AmfCreator.class.getName()+" but not all of its parameters are marked with "+AmfProperty.class.getName());
}
return c;
}
}
throw new IllegalStateException("An instance of "+this.beanType+" could note be created. Must either have a public no-arg constructor, or a constructor annotated with "+AmfCreator.class.getName()+".");
}
}
}