/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 com.googlecode.openbeans;
import com.googlecode.openbeans.BeanInfo;
import com.googlecode.openbeans.Encoder;
import com.googlecode.openbeans.Expression;
import com.googlecode.openbeans.IntrospectionException;
import com.googlecode.openbeans.Introspector;
import com.googlecode.openbeans.PersistenceDelegate;
import com.googlecode.openbeans.PropertyDescriptor;
import com.googlecode.openbeans.Statement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import org.apache.harmony.beans.BeansUtils;
/**
* Default PersistenceDelegate for normal classes. The instances of this class
* are used when other customized PersistenceDelegate is not set in the encoders
* for a particular type.
* <p>
* This PersistenceDelegate assumes that the bean to be made persistent has a
* default constructor that takes no parameters or a constructor that takes some
* properties as its parameters. Only the properties that can be got or set
* based on the knowledge gained through an introspection will be made
* persistent. In the case that a bean is constructed with some properties, the
* value of these properties should be available via the conventional getter
* method.
* </p>
*
* @see Encoder
*/
public class DefaultPersistenceDelegate extends PersistenceDelegate {
// shared empty property name array
private static String[] EMPTY_PROPERTIES = new String[0];
// names of the properties accepted by the bean's constructor
private String[] propertyNames = EMPTY_PROPERTIES;
/**
* Constructs a <code>DefaultPersistenceDelegate</code> instance that
* supports the persistence of a bean which has a default constructor.
*
*/
public DefaultPersistenceDelegate() {
// empty
}
/**
* Constructs a <code>DefaultPersistenceDelegate</code> instance that
* supports the persistence of a bean which is constructed with some
* properties.
*
* @param propertyNames
* the name of the properties that are taken as parameters by the
* bean's constructor
*/
public DefaultPersistenceDelegate(String[] propertyNames) {
if (null != propertyNames) {
this.propertyNames = propertyNames;
}
}
/**
* Initializes the new instance in the new environment so that it becomes
* equivalent with the old one, meanwhile recording this process in the
* encoder.
* <p>
* This is done by inspecting each property of the bean. The property value
* from the old bean instance and the value from the new bean instance are
* both retrieved and examined to see whether the latter mutates to the
* former, and if not, issue a call to the write method to set the
* equivalent value for the new instance. Exceptions occured during this
* process are reported to the exception listener of the encoder.
* </p>
*
* @param type
* the type of the bean
* @param oldInstance
* the original bean object to be recorded
* @param newInstance
* the simmulating new bean object to be initialized
* @param enc
* the encoder to write the outputs to
*/
@Override
protected void initialize(Class<?> type, Object oldInstance,
Object newInstance, Encoder enc) {
// Call the initialization of the super type
super.initialize(type, oldInstance, newInstance, enc);
// Continue only if initializing the "current" type
if (type != oldInstance.getClass()) {
return;
}
// Get all bean properties
BeanInfo info = null;
try {
info = Introspector.getBeanInfo(type);
} catch (IntrospectionException ex) {
enc.getExceptionListener().exceptionThrown(ex);
return;
}
PropertyDescriptor[] pds = info.getPropertyDescriptors();
Method getter, setter;
// Initialize each found non-transient property
for (int i = 0; i < pds.length; i++) {
// Skip a property whose transient attribute is true
if (Boolean.TRUE.equals(pds[i].getValue("transient"))) { //$NON-NLS-1$
continue;
}
getter = pds[i].getReadMethod();
setter = pds[i].getWriteMethod();
// Skip a property having no setter or getter
if (getter == null || setter == null) {
continue;
}
// Get the value of the property in the old instance
Expression getterExp = new Expression(oldInstance,
getter.getName(), null);
try {
// Calculate the old value of the property
Object oldVal = getterExp.getValue();
// Write the getter expression to the encoder
enc.writeExpression(getterExp);
// Get the target value that exists in the new environment
Object targetVal = enc.get(oldVal);
Object newVal = new Expression(newInstance, getter.getName(),
null).getValue();
if (targetVal == null ? (newVal != null && oldVal == null)
: targetVal != newVal && !targetVal.equals(newVal)) {
enc.writeStatement(new Statement(oldInstance, setter
.getName(), new Object[] { oldVal }));
}
} catch (Exception ex) {
enc.getExceptionListener().exceptionThrown(ex);
}
}
}
/*
* Get the field value of an object using privileged code.
*/
private Object getFieldValue(Object oldInstance, String fieldName)
throws NoSuchFieldException, IllegalAccessException {
Class<? extends Object> c = oldInstance.getClass();
final Field f = c.getDeclaredField(fieldName);
AccessController.doPrivileged(new PrivilegedAction<Object>() {
public Object run() {
f.setAccessible(true);
return null;
}
});
return f.get(oldInstance);
}
/*
* Get the value for the specified property of the given bean instance.
*/
private Object getPropertyValue(
HashMap<String, PropertyDescriptor> proDscMap, Object oldInstance,
String propName) throws Exception {
// Try to get the read method for the property
Method getter = null;
if (null != proDscMap) {
PropertyDescriptor pd = proDscMap.get(Introspector
.decapitalize(propName));
if (null != pd) {
getter = pd.getReadMethod();
}
}
// Invoke read method to get the value if found
if (null != getter) {
return getter.invoke(oldInstance, (Object[]) null);
}
// Otherwise, try to access the field directly
try {
return getFieldValue(oldInstance, propName);
} catch (Exception ex) {
// Fail, throw an exception
throw new NoSuchMethodException(
"The getter method for the property " //$NON-NLS-1$
+ propName + " can't be found."); //$NON-NLS-1$
}
}
/**
* Returns an expression that represents a call to the bean's constructor.
* The constructor may take zero or more parameters, as specified when this
* <code>DefaultPersistenceDelegate</code> is constructed.
*
* @param oldInstance
* the old instance
* @param enc
* the encoder that wants to record the old instance
* @return an expression for instantiating an object of the same type as the
* old instance
*/
@Override
protected Expression instantiate(Object oldInstance, Encoder enc) {
Object[] args = null;
// Set the constructor arguments if any property names exist
if (this.propertyNames.length > 0) {
// Prepare the property descriptors for finding getter method later
BeanInfo info = null;
HashMap<String, PropertyDescriptor> proDscMap = null;
try {
info = Introspector.getBeanInfo(oldInstance.getClass(),
Introspector.IGNORE_ALL_BEANINFO);
proDscMap = internalAsMap(info.getPropertyDescriptors());
} catch (IntrospectionException ex) {
enc.getExceptionListener().exceptionThrown(ex);
throw new Error(ex);
}
// Get the arguments values
args = new Object[this.propertyNames.length];
for (int i = 0; i < this.propertyNames.length; i++) {
String propertyName = propertyNames[i];
if (null == propertyName || 0 == propertyName.length()) {
continue;
}
// Get the value for each property of the given instance
try {
args[i] = getPropertyValue(proDscMap, oldInstance,
this.propertyNames[i]);
} catch (Exception ex) {
enc.getExceptionListener().exceptionThrown(ex);
}
}
}
return new Expression(oldInstance, oldInstance.getClass(),
BeansUtils.NEW, args);
}
private static HashMap<String, PropertyDescriptor> internalAsMap(
PropertyDescriptor[] propertyDescs) {
HashMap<String, PropertyDescriptor> map = new HashMap<String, PropertyDescriptor>();
for (int i = 0; i < propertyDescs.length; i++) {
map.put(propertyDescs[i].getName(), propertyDescs[i]);
}
return map;
}
/**
* Determines whether one object mutates to the other object. If this
* <code>DefaultPersistenceDelegate</code> is constructed with one or more
* property names, and the class of <code>o1</code> overrides the
* "equals(Object)" method, then <code>o2</code> is considered to mutate to
* <code>o1</code> if <code>o1</code> equals to <code>o2</code>. Otherwise,
* the result is the same as the definition in
* <code>PersistenceDelegate</code>.
*
* @param o1
* one object
* @param o2
* the other object
* @return true if second object mutates to the first object, otherwise
* false
*/
@Override
protected boolean mutatesTo(Object o1, Object o2) {
if (this.propertyNames.length > 0) {
if (BeansUtils.declaredEquals(o1.getClass())) {
return o1.equals(o2);
}
}
return super.mutatesTo(o1, o2);
}
}