/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* 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 au.gov.ga.earthsci.common.persistence;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Text;
import au.gov.ga.earthsci.common.util.AnnotationUtil;
import au.gov.ga.earthsci.common.util.StringInstantiable;
import au.gov.ga.earthsci.common.util.Util;
import au.gov.ga.earthsci.common.util.XmlUtil;
/**
* Persists annotated {@link Exportable} types.
*
* @see Exportable
* @see Persistent
* @see Adapter
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class Persister
{
protected static final String TYPE_ATTRIBUTE = "type"; //$NON-NLS-1$
protected static final String NULL_ATTRIBUTE = "null"; //$NON-NLS-1$
protected static final String DEFAULT_ARRAY_ELEMENT_NAME = "element"; //$NON-NLS-1$
private final Map<String, Class<?>> nameToExportable = new HashMap<String, Class<?>>();
private final Map<Class<?>, String> exportableToName = new HashMap<Class<?>, String>();
private final Map<Class<?>, IPersistentAdapter<?>> adapters = new HashMap<Class<?>, IPersistentAdapter<?>>();
private final Set<ClassLoader> classLoaders = new HashSet<ClassLoader>();
private boolean ignoreMissing = false;
private boolean ignoreNulls = false;
/**
* @return Should this {@link Persister} ignore missing XML
* elements/attributes during unpersisting?
* @see #setIgnoreMissing(boolean)
*/
public boolean isIgnoreMissing()
{
return ignoreMissing;
}
/**
* Sets if this {@link Persister} should ignore missing XML
* elements/attributes for fields/methods marked as {@link Persistent} in
* the {@link Exportable}s that it loads.
* <p/>
* The default behaviour is, if a field/method is marked persistent, and an
* XML node does not exist in the XML for that {@link Exportable}, a
* {@link PersistenceException} is thrown. However, if
* {@link #isIgnoreMissing()} is true, this is ignored and the method/field
* is not called/set.
*
* @param ignoreMissing
*/
public void setIgnoreMissing(boolean ignoreMissing)
{
this.ignoreMissing = ignoreMissing;
}
/**
* @return Should this {@link Persister} ignore null {@link Persistent}
* values when persisting?
* @see #setIgnoreNulls(boolean)
*/
public boolean isIgnoreNulls()
{
return ignoreNulls;
}
/**
* Sets if this {@link Persister} should ignore null values for
* methods/fields that are marked {@link Persistent}.
* <p/>
* The default behaviour is, if a field/method is marked persistent, and is
* null, it will be saved as an element with an attribute null="true".
* <p/>
* If this property is true, then {@link #setIgnoreMissing(boolean)} should
* also be set to true; otherwise an exception will be thrown for missing
* elements for null values.
*
* @param ignoreNulls
*/
public void setIgnoreNulls(boolean ignoreNulls)
{
this.ignoreNulls = ignoreNulls;
}
/**
* Register a name for a given {@link Exportable} type. This name is used
* for the top level XML element name (instead of the canonical class name)
* when persisting objects of this type.
*
* @param type
* Class of the type to name
* @param name
* XML element name to use when persisting type objects
*/
public void registerNamedExportable(Class<?> type, String name)
{
try
{
assertIsExportable(type);
}
catch (PersistenceException e)
{
throw new IllegalArgumentException(e);
}
if (Util.isEmpty(name))
{
throw new IllegalArgumentException("name must not be empty"); //$NON-NLS-1$
}
if (!name.matches("^\\w+$")) //$NON-NLS-1$
{
throw new IllegalArgumentException("name must only be word characters"); //$NON-NLS-1$
}
nameToExportable.put(name, type);
exportableToName.put(type, name);
}
/**
* Unregister a named {@link Exportable} type.
*
* @param type
* Type to unregister
* @see #registerNamedExportable(Class, String)
*/
public void unregisterNamedExportable(Class<?> type)
{
String name = exportableToName.remove(type);
nameToExportable.remove(name);
}
/**
* Unregister a named {@link Exportable} name.
*
* @param name
* Name to unregister
* @see #registerNamedExportable(Class, String)
*/
public void unregisterNamedExportable(String name)
{
Class<?> type = nameToExportable.remove(name);
exportableToName.remove(type);
}
/**
* Register an {@link IPersistentAdapter} to use when persisting objects of
* the given type. This overrides any {@link Adapter} annotations for
* fields/methods of this type.
*
* @param type
* Type that the adapter supports
* @param adapter
* Adapter used for persisting type objects
*/
public <E> void registerAdapter(Class<E> type, IPersistentAdapter<E> adapter)
{
adapters.put(type, adapter);
}
/**
* Unregister the {@link IPersistentAdapter} registered for the given type.
*
* @param type
* Type of {@link IPersistentAdapter} to unregister
* @see #registerAdapter(Class, IPersistentAdapter)
*/
public void unregisterAdapter(Class<?> type)
{
adapters.remove(type);
}
/**
* Register a {@link ClassLoader} that can be used for resolving classes
* from class names. This is required if the caller resides in a different
* plugin, which means this plugin's classloader doesn't have access to the
* caller plugin's classes.
*
* @param classLoader
* ClassLoader to register
*/
public void registerClassLoader(ClassLoader classLoader)
{
classLoaders.add(classLoader);
}
/**
* Unregister a registered {@link ClassLoader}.
*
* @param classLoader
* ClassLoader to unregister
* @see #registerClassLoader(ClassLoader)
*/
public void unregisterClassLoader(ClassLoader classLoader)
{
classLoaders.remove(classLoader);
}
/**
* Save the given {@link Exportable} object to XML under the given parent.
*
* @param o
* Object to save/persist
* @param parent
* XML element to save inside
* @param context
* @throws PersistenceException
* If an error occurs during persistance of the object
*/
public void save(Object o, Element parent, URI context) throws PersistenceException
{
if (o == null)
{
throw new NullPointerException("Object cannot be null"); //$NON-NLS-1$
}
if (parent == null)
{
throw new NullPointerException("Parent element cannot be null"); //$NON-NLS-1$
}
assertIsExportable(o.getClass());
String elementName = getNameFromType(o.getClass());
Element element = parent.getOwnerDocument().createElement(elementName);
parent.appendChild(element);
persistMethods(o, element, context);
persistFields(o, element, context);
}
/**
* Persist the {@link Persistent} methods of the given object.
*
* @param o
* Object whose method values should be persisted
* @param element
* XML element to save inside
* @param context
* @throws PersistenceException
*/
protected void persistMethods(Object o, Element element, URI context) throws PersistenceException
{
Method[] methods = AnnotationUtil.getAnnotatedMethods(o.getClass(), Persistent.class);
for (Method method : methods)
{
method.setAccessible(true);
Persistent persistent = AnnotationUtil.getAnnotation(method, Persistent.class);
String name = checkAndGetPersistentName(method, persistent);
Object value;
try
{
value = method.invoke(o);
}
catch (Exception e)
{
throw new PersistenceException(e);
}
Adapter adapter = AnnotationUtil.getAnnotation(method, Adapter.class);
persist(value, method.getReturnType(), name, element, context, persistent, adapter);
}
}
/**
* Persist the {@link Persistent} fields of the given object.
*
* @param o
* Object whose fields should be persisted
* @param element
* XML element to save inside
* @param context
* @throws PersistenceException
*/
protected void persistFields(Object o, Element element, URI context) throws PersistenceException
{
Field[] fields = AnnotationUtil.getAnnotatedFields(o.getClass(), Persistent.class);
for (Field field : fields)
{
field.setAccessible(true);
Persistent persistent = AnnotationUtil.getAnnotation(field, Persistent.class);
String name = checkAndGetPersistentName(field, persistent);
Object value;
try
{
value = field.get(o);
}
catch (Exception e)
{
throw new PersistenceException(e);
}
Adapter adapter = AnnotationUtil.getAnnotation(field, Adapter.class);
persist(value, field.getType(), name, element, context, persistent, adapter);
}
}
/**
* Persist a value into an element (or attribute) with the given name.
* <p/>
* If the type of the value is a subclass of the given baseType, then the
* classname is also persisted. This allows the {@link Persister} to know
* what type to instantiate when loading.
*
* @param value
* Value to persist
* @param baseType
* Type specified by the method/field (can be null)
* @param name
* XML element (or attribute) name to save to
* @param element
* XML element to save inside
* @param context
* @param persistent
* Field/method's {@link Persistent} annotation
* @param adapter
* Field/method's {@link Adapter} annotation
* @throws PersistenceException
*/
protected void persist(Object value, Class<?> baseType, String name, Element element, URI context,
Persistent persistent, Adapter adapter) throws PersistenceException
{
//if should ignore nulls and this value is null, don't create an element
if (isIgnoreNulls() && value == null)
{
return;
}
Element nameElement = element.getOwnerDocument().createElement(name);
element.appendChild(nameElement);
//if the value is null, mark it as such with an attribute on the element, and return
if (value == null)
{
nameElement.setAttribute(NULL_ATTRIBUTE, Boolean.TRUE.toString());
return;
}
IPersistentAdapter<?> persistentAdapter = getAdapter(value.getClass(), adapter);
boolean isExportable = AnnotationUtil.getAnnotation(value.getClass(), Exportable.class) != null;
//if the value type isn't the same as the type specified by the field/method, and
//it isn't a boxed version, then save the type as an attribute on the element
boolean classNameSaved = false;
if (!value.getClass().equals(baseType))
{
boolean boxed =
baseType != null && baseType.isPrimitive()
&& Util.primitiveClassToBoxed(baseType).equals(value.getClass());
boolean isBoxedOrAdapterOrExportable =
boxed || (adapter != null && persistentAdapter != null) || isExportable;
if (value instanceof Collection<?> || !isBoxedOrAdapterOrExportable)
{
nameElement.setAttribute(TYPE_ATTRIBUTE, getNameFromType(value.getClass()));
classNameSaved = true;
}
}
//If the value is an array or Collection, save each element as a separate XML element
if (value.getClass().isArray() || value instanceof Collection<?>)
{
if (persistent.attribute())
{
throw new PersistenceException("Array or collection Persistent cannot be an attribute"); //$NON-NLS-1$
}
String arrayElementName = getArrayElementName(persistent);
if (value.getClass().isArray())
{
for (int i = 0; i < Array.getLength(value); i++)
{
Object arrayElement = Array.get(value, i);
Class<?> componentType = baseType == null ? null : baseType.getComponentType();
persist(arrayElement, componentType, arrayElementName, nameElement, context, persistent, adapter);
}
}
else
{
Collection<?> collection = (Collection<?>) value;
for (Object collectionElement : collection)
{
persist(collectionElement, null, arrayElementName, nameElement, context, persistent, adapter);
}
}
return;
}
if (persistentAdapter != null)
{
//if there's a IPersistentAdapter for this object's type, use it to create the XML
@SuppressWarnings("unchecked")
IPersistentAdapter<Object> objectAdapter = (IPersistentAdapter<Object>) persistentAdapter;
objectAdapter.toXML(value, nameElement, context);
}
else if (isExportable)
{
//if the object is itself exportable, recurse
save(value, nameElement, context);
}
else
{
//once here, the only objects supported for persistance are those that are StringInstantiable
assertIsStringInstantiable(value.getClass());
String stringValue = StringInstantiable.toString(value);
if (persistent.attribute() && !classNameSaved)
{
element.removeChild(nameElement);
element.setAttribute(name, stringValue);
}
else
{
Text text = nameElement.getOwnerDocument().createTextNode(stringValue);
nameElement.appendChild(text);
}
}
}
/**
* Load an {@link Exportable} object from an XML element.
*
* @param element
* Element to load from
* @param context
* @return New object loaded from XML
* @throws PersistenceException
* If an error occurs during persistance of the object
*/
public Object load(Element element, URI context) throws PersistenceException
{
if (element == null)
{
throw new NullPointerException("Element cannot be null"); //$NON-NLS-1$
}
Class<?> c = getTypeFromName(element.getTagName());
assertIsExportable(c);
IPersistentAdapter<?> adapter = getAdapter(c, AnnotationUtil.getAnnotation(c, Adapter.class));
if (adapter != null)
{
@SuppressWarnings("unchecked")
IPersistentAdapter<Object> objectAdapter = (IPersistentAdapter<Object>) adapter;
return objectAdapter.fromXML(element, context);
}
Constructor<?> constructor = null;
try
{
constructor = c.getDeclaredConstructor();
}
catch (NoSuchMethodException e)
{
//impossible; already checked
}
constructor.setAccessible(true);
Object o;
try
{
o = constructor.newInstance();
}
catch (Exception e)
{
throw new IllegalStateException(e);
}
unpersistMethods(o, element, context);
unpersistFields(o, element, context);
return o;
}
/**
* Unpersist the {@link Persistent} methods on the given object from an XML
* element.
*
* @param o
* Object to unpersist methods to
* @param element
* XML element to unpersist
* @param context
* @throws PersistenceException
*/
protected void unpersistMethods(Object o, Element element, URI context) throws PersistenceException
{
Method[] methods = AnnotationUtil.getAnnotatedMethods(o.getClass(), Persistent.class);
for (Method method : methods)
{
method.setAccessible(true);
Persistent persistent = AnnotationUtil.getAnnotation(method, Persistent.class);
String name = checkAndGetPersistentName(method, persistent);
String methodName = removeGetter(method);
Class<?> type = method.getReturnType();
Method setter = getSetter(o.getClass(), methodName, type, persistent);
Adapter adapter = AnnotationUtil.getAnnotation(method, Adapter.class);
try
{
Object value = unpersist(0, element, name, type, context, persistent, adapter);
setter.invoke(o, value);
}
catch (MissingPersistentException e)
{
if (!isIgnoreMissing())
{
throw e;
}
}
catch (PersistenceException e)
{
throw e;
}
catch (Exception e)
{
throw new PersistenceException(e);
}
}
}
/**
* Unpersist the {@link Persistent} fields for the given object from an XML
* element.
*
* @param o
* Object to unpersist fields to
* @param element
* XML element to unpersist
* @param context
* @throws PersistenceException
*/
protected void unpersistFields(Object o, Element element, URI context) throws PersistenceException
{
Field[] fields = AnnotationUtil.getAnnotatedFields(o.getClass(), Persistent.class);
for (Field field : fields)
{
field.setAccessible(true);
Persistent persistent = AnnotationUtil.getAnnotation(field, Persistent.class);
String name = checkAndGetPersistentName(field, persistent);
Class<?> type = field.getType();
Adapter adapter = AnnotationUtil.getAnnotation(field, Adapter.class);
try
{
Object value = unpersist(0, element, name, type, context, persistent, adapter);
field.set(o, value);
}
catch (MissingPersistentException e)
{
if (!isIgnoreMissing())
{
throw e;
}
}
catch (PersistenceException e)
{
throw e;
}
catch (Exception e)
{
throw new PersistenceException(e);
}
}
}
/**
* Load/unpersist an object from an XML element (or attribute) with the
* given name.
*
* @param index
* Index of the element within the list of direct child elements
* of parent with the given tag name
* @param parent
* XML element in which to search for child elements (or an
* attribute) with the given tag name
* @param name
* XML element (or attribute) name that stores the value to
* unpersist
* @param type
* Type to unpersist to (can be null if the element has an
* attribute which specifies the type)
* @param context
* @param persistent
* Field/method's {@link Persistent} annotation
* @param adapter
* Field/method's {@link Adapter} annotation
* @return New object loaded from XML
* @throws PersistenceException
*/
protected Object unpersist(int index, Element parent, String name, Class<?> type, URI context,
Persistent persistent, Adapter adapter) throws PersistenceException
{
//get the index'th named element of parent
Element element = XmlUtil.getChildElementByTagName(index, name, parent);
Attr attribute = parent.getAttributeNode(name);
if (element != null)
{
//if the null attribute is set, return null
String nullAttribute = element.getAttribute(NULL_ATTRIBUTE);
if (Boolean.valueOf(nullAttribute))
{
return null;
}
//the className attribute can override the type (to support subclasses)
String classNameAttribute = element.getAttribute(TYPE_ATTRIBUTE);
if (!Util.isEmpty(classNameAttribute))
{
//for each [] at the end of the class name, increment the array depth
int arrayDepth = 0;
while (classNameAttribute.endsWith("[]")) //$NON-NLS-1$
{
classNameAttribute = classNameAttribute.substring(0, classNameAttribute.length() - 2);
arrayDepth++;
}
//load the class from the name
type = getTypeFromName(classNameAttribute, false);
if (type == null)
{
type = getTypeFromName(element.getTagName(), false);
}
if (type != null)
{
//make the type an array type with the correct depth
while (arrayDepth > 0)
{
type = Array.newInstance(type, 0).getClass();
arrayDepth--;
}
}
}
}
IPersistentAdapter<?> persistentAdapter = getAdapter(type, adapter);
//if there is no type, and no adapter, the first element must be exportable
if (type == null && persistentAdapter == null)
{
Element firstChild = element == null ? null : XmlUtil.getFirstChildElement(element);
if (firstChild == null)
{
throw new PersistenceException("Unpersist type is null"); //$NON-NLS-1$
}
//if the type isn't defined, assume the first child element is exportable
type = getTypeFromName(firstChild.getTagName());
assertIsExportable(type);
persistentAdapter = getAdapter(type, adapter);
}
//handle array/collection types
if (type != null && (type.isArray() || Collection.class.isAssignableFrom(type)))
{
if (element == null)
{
throw new PersistenceException("Could not find element for name: " + name); //$NON-NLS-1$
}
//calculate the array length from the number of child elements
String arrayElementName = getArrayElementName(persistent);
int length = XmlUtil.getCountChildElementsByTagName(arrayElementName, element);
if (type.isArray())
{
//create an array and unpersist the elements into it
Object array = Array.newInstance(type.getComponentType(), length);
for (int i = 0; i < length; i++)
{
//recurse
Object o =
unpersist(i, element, arrayElementName, type.getComponentType(), context, persistent,
adapter);
Array.set(array, i, o);
}
return array;
}
else
{
//instantiate the collection implementation
String collectionClassName = element.getAttribute(TYPE_ATTRIBUTE);
Class<?> collectionType;
if (Util.isEmpty(collectionClassName))
{
if (Modifier.isAbstract(type.getModifiers()) || type.isInterface())
{
throw new PersistenceException("Collection class not specified"); //$NON-NLS-1$
}
collectionType = type;
}
else
{
collectionType = getTypeFromName(collectionClassName);
}
Collection<Object> collection;
try
{
Constructor<?> constructor = collectionType.getConstructor();
@SuppressWarnings("unchecked")
Collection<Object> objectCollection = (Collection<Object>) constructor.newInstance();
collection = objectCollection;
}
catch (Exception e)
{
throw new PersistenceException("Error instantiating collection", e); //$NON-NLS-1$
}
//unpersist the collection's elements
for (int i = 0; i < length; i++)
{
//recurse
//we don't know the type for collection elements (they must specify the className attribute)
Object o = unpersist(i, element, arrayElementName, null, context, persistent, adapter);
collection.add(o);
}
return collection;
}
}
String stringValue = null;
if (element != null)
{
if (persistentAdapter != null)
{
//if there's a IPersistentAdapter for this object's type, use it to load the XML
@SuppressWarnings("unchecked")
IPersistentAdapter<Object> objectAdapter = (IPersistentAdapter<Object>) persistentAdapter;
return objectAdapter.fromXML(element, context);
}
else
{
Element child = XmlUtil.getFirstChildElement(element);
if (child != null)
{
//assume, if there's a child element, the type is exportable: recurse
return load(child, context);
}
else
{
Text text = XmlUtil.getFirstChildText(element);
if (text == null)
{
throw new PersistenceException("No text child found"); //$NON-NLS-1$
}
stringValue = text.getData();
}
}
}
else if (attribute != null)
{
stringValue = attribute.getValue();
}
//if context is non-null, use it to resolve relative URIs/URLs
if (context != null)
{
if (URI.class.isAssignableFrom(type))
{
try
{
return context.resolve(new URI(stringValue));
}
catch (URISyntaxException e)
{
throw new PersistenceException("Error converting string to URI", e); //$NON-NLS-1$
}
}
if (URL.class.isAssignableFrom(type))
{
try
{
return new URL(context.toURL(), stringValue);
}
catch (MalformedURLException e)
{
throw new PersistenceException("Error converting string to URL", e); //$NON-NLS-1$
}
}
}
//once here, the only objects supported for unpersistance are those that are StringInstantiable
if (stringValue != null)
{
assertIsStringInstantiable(type);
return StringInstantiable.newInstance(stringValue, type);
}
//if we get here, there's no element/attribute for the given Persistent
throw new MissingPersistentException("Could not unpersist Persistable: " + name); //$NON-NLS-1$
}
/**
* Check that the method is persistable (no parameters, and a non-void
* return type), and calculate the element/attribute name to save to.
*
* @param method
* Method that will be persisted
* @param persistent
* Method's {@link Persistent} annotation
* @return Element/attribute name for the given method
* @throws PersistenceException
*/
protected String checkAndGetPersistentName(Method method, Persistent persistent) throws PersistenceException
{
if (method.getParameterTypes().length > 0)
{
throw new PersistenceException("Cannot persist parameterized methods: " + method); //$NON-NLS-1$
}
if (void.class.equals(method.getReturnType()))
{
throw new PersistenceException("Cannot persist methods with no return type: " + method); //$NON-NLS-1$
}
String name = persistent.name();
if (Util.isEmpty(name))
{
name = removeGetter(method);
}
if (Util.isEmpty(name))
{
throw new PersistenceException("Could not determine name for method: " + method); //$NON-NLS-1$
}
return name;
}
/**
* Calculate the element/attribute name to save the given field to.
*
* @param field
* Field that will be persisted
* @param persistent
* Field's {@link Persistent} annotation
* @return Element/attribute name for the given field
* @throws PersistenceException
*/
protected String checkAndGetPersistentName(Field field, Persistent persistent) throws PersistenceException
{
String name = persistent.name();
if (Util.isEmpty(name))
{
name = field.getName();
}
if (Util.isEmpty(name))
{
throw new PersistenceException("Could not determine name for field: " + field); //$NON-NLS-1$
}
return name;
}
/**
* Remove the 'get' (or 'is' for boolean return types) method name prefix
* (if it exists), and lowercase the first character (if the prefix was
* present).
*
* @param method
* Method from which to remove the 'get'/'is' prefix from
* @return Method name without the 'get'/'is' prefix
*/
protected String removeGetter(Method method)
{
String name = method.getName();
if (boolean.class.equals(method.getReturnType()) && name.length() > 2 && name.startsWith("is")) //$NON-NLS-1$
{
name = name.substring(2, 3).toLowerCase() + name.substring(3);
}
else if (name.length() > 3 && name.startsWith("get")) //$NON-NLS-1$
{
name = name.substring(3, 4).toLowerCase() + name.substring(4);
}
return name;
}
/**
* Find the setter method in the class for the given property name. If the
* {@link Persistent} annotation defines the setter property, then return
* the method with that name.
*
* @param c
* Class in which to find the setter method
* @param name
* Name of the property to find a setter for (ignored if the
* {@link Persistent} annotation defines the setter)
* @param parameterType
* Type that the setter method should have a single parameter for
* @param persistent
* {@link Persistent} annotation for the corresponding getter
* @return
* @throws PersistenceException
*/
protected Method getSetter(Class<?> c, String name, Class<?> parameterType, Persistent persistent)
throws PersistenceException
{
if (!Util.isEmpty(persistent.setter()))
{
try
{
return getSetterMethod(c, persistent.setter(), parameterType);
}
catch (NoSuchMethodException e)
{
throw new PersistenceException("Cannot find matching Persistent setter: " + persistent.setter() //$NON-NLS-1$
+ " in class " + c, e); //$NON-NLS-1$
}
}
if (Util.isEmpty(name))
{
throw new PersistenceException("Persistent setter name is empty"); //$NON-NLS-1$
}
//first find a method with the property name and a 'set' prefix (ie if property = name, setter = setName)
String setName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1); //$NON-NLS-1$
try
{
return getSetterMethod(c, setName, parameterType);
}
catch (NoSuchMethodException e)
{
}
//next try and find a method that is just named the property name
try
{
return getSetterMethod(c, name, parameterType);
}
catch (NoSuchMethodException e)
{
}
throw new PersistenceException("Cannot find matching Persistent setter: " + setName + " in class " + c); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Find a setter method declared in a class with the given name. If not
* found in the class, recurses to search the class' superclasses and
* implemented interfaces.
*
* @param c
* Class to search for the setter method
* @param name
* Name of the setter method
* @param parameterType
* Single parameter type that the setter accepts
* @return Setter method
* @throws NoSuchMethodException
* If a corresponding setter method could not be found
*/
protected Method getSetterMethod(Class<?> c, String name, Class<?> parameterType) throws NoSuchMethodException
{
NoSuchMethodException noSuchMethodException;
try
{
Method m = c.getDeclaredMethod(name, parameterType);
m.setAccessible(true);
return m;
}
catch (NoSuchMethodException e)
{
noSuchMethodException = e;
}
//search super class
if (c.getSuperclass() != null)
{
try
{
return getSetterMethod(c.getSuperclass(), name, parameterType);
}
catch (NoSuchMethodException e)
{
}
}
//search interfaces
for (Class<?> i : c.getInterfaces())
{
try
{
return getSetterMethod(i, name, parameterType);
}
catch (NoSuchMethodException e)
{
}
}
//could not be found, throw the original exception
throw noSuchMethodException;
}
/**
* Calculate the XML element name to use for array elements. If the
* {@link Persistent} attribute defines an element name, return that,
* otherwise return the default.
*
* @param persistent
* {@link Persistent} annotation that may define the element name
* @return The XML element name to use for array elements
*/
protected String getArrayElementName(Persistent persistent)
{
String arrayElementName = persistent.elementName();
if (Util.isEmpty(arrayElementName))
{
arrayElementName = DEFAULT_ARRAY_ELEMENT_NAME;
}
return arrayElementName;
}
/**
* Get the {@link IPersistentAdapter} used to persist the given type to XML.
* If the type already has a registered adapter (from
* {@link #registerAdapter(Class, IPersistentAdapter)}), return that;
* otherwise, if the {@link Adapter} annotation is non-null, instantiate and
* return an object of the class defined in the annotation.
*
* @param type
* Type for which to get an adapter for
* @param adapter
* Adapter annotation
* @return {@link IPersistentAdapter} used to persist the given type
* @throws PersistenceException
*/
protected IPersistentAdapter<?> getAdapter(Class<?> type, Adapter adapter) throws PersistenceException
{
IPersistentAdapter<?> persistentAdapter = type != null ? adapters.get(type) : null;
if (persistentAdapter == null && adapter != null)
{
Class<? extends IPersistentAdapter<?>> adapterClass = adapter.value();
if (adapterClass != null)
{
try
{
Constructor<? extends IPersistentAdapter<?>> constructor = adapterClass.getDeclaredConstructor();
constructor.setAccessible(true);
persistentAdapter = constructor.newInstance();
}
catch (Exception e)
{
throw new PersistenceException("Error instantiating adapter class: " + adapterClass, e); //$NON-NLS-1$
}
}
}
return persistentAdapter;
}
/**
* Calculate the type for the given name. If the name has been registered
* using {@link #registerNamedExportable(Class, String)}, that type is
* returned. Otherwise {@link ClassLoader#loadClass(String)} is used.
*
* @param name
* Name to calculate type for
* @return Type for name
* @throws PersistenceException
*/
protected Class<?> getTypeFromName(String name) throws PersistenceException
{
return getTypeFromName(name, true);
}
private Class<?> getTypeFromName(String name, boolean failHard) throws PersistenceException
{
Class<?> c = nameToExportable.get(name);
if (c != null)
{
return c;
}
c = PrimitiveNames.NAME_TO_PRIMITIVE.get(name);
if (c != null)
{
return c;
}
name = name.replace('-', '$');
try
{
return getClass().getClassLoader().loadClass(name);
}
catch (ClassNotFoundException e)
{
}
for (ClassLoader classLoader : classLoaders)
{
try
{
return classLoader.loadClass(name);
}
catch (ClassNotFoundException e)
{
}
}
if (failHard)
{
throw new PersistenceException("Could not determine type for name: " + name); //$NON-NLS-1$
}
else
{
return null;
}
}
/**
* Calculate the name for the given type. If the type is marked as
* {@link Exportable} and a named exportable has been registered using
* {@link #registerNamedExportable(Class, String)}, that name is returned.
* Otherwise the canonical class name is returned.
*
* @param type
* Type to calculate name for
* @return Name of type
* @throws PersistenceException
*/
protected String getNameFromType(Class<?> type) throws PersistenceException
{
String name = exportableToName.get(type);
if (Util.isEmpty(name))
{
name = PrimitiveNames.PRIMITIVE_TO_NAME.get(type);
}
if (Util.isEmpty(name))
{
//we want the component type of an array to still use the exportable name, so recurse if array
if (type.isLocalClass() || type.isAnonymousClass())
{
throw new PersistenceException("Local and anonymous classes cannot be persisted: " + type); //$NON-NLS-1$
}
if (type.isMemberClass() && !Modifier.isStatic(type.getModifiers()))
{
throw new PersistenceException("Non-static member classes cannot be persisted: " + type); //$NON-NLS-1$
}
if (type.isArray())
{
name = getNameFromType(type.getComponentType()) + "[]"; //$NON-NLS-1$
}
else
{
name = type.getName().replace('$', '-');
}
}
if (Util.isEmpty(name))
{
throw new PersistenceException("Could not determine name for type: " + type); //$NON-NLS-1$
}
return name;
}
/**
* Throws an {@link IllegalArgumentException} if the given type is not
* {@link Exportable} (or doesn't have a default constructor).
*
* @param type
* Type to test
* @throws PersistenceException
*/
protected void assertIsExportable(Class<?> type) throws PersistenceException
{
if (getAdapter(type, null) != null)
{
return;
}
if (AnnotationUtil.getAnnotation(type, Exportable.class) == null)
{
throw new PersistenceException(type
+ " is not marked " + Exportable.class.getSimpleName() + " and has no registered adapter."); //$NON-NLS-1$ //$NON-NLS-2$
}
try
{
type.getDeclaredConstructor();
}
catch (NoSuchMethodException e)
{
throw new PersistenceException(type + " does not have a default constructor"); //$NON-NLS-1$
}
}
/**
* Throws an {@link IllegalArgumentException} if the given type is not
* {@link StringInstantiable#isInstantiable(Class)}.
*
* @param type
* Type to test
* @throws PersistenceException
*/
protected void assertIsStringInstantiable(Class<?> type) throws PersistenceException
{
if (!StringInstantiable.isInstantiable(type))
{
throw new PersistenceException("Cannot persist type: " + type); //$NON-NLS-1$
}
}
/**
* Helper class used to map primitive and boxed classes to simple names, and
* vice-versa.
*/
protected static class PrimitiveNames
{
public static final Map<Class<?>, String> PRIMITIVE_TO_NAME;
public static final Map<String, Class<?>> NAME_TO_PRIMITIVE;
static
{
Map<Class<?>, String> ptn = new HashMap<Class<?>, String>();
Map<String, Class<?>> ntp = new HashMap<String, Class<?>>();
add(int.class, Integer.class, ptn, ntp);
add(short.class, Short.class, ptn, ntp);
add(long.class, Long.class, ptn, ntp);
add(char.class, Character.class, ptn, ntp);
add(byte.class, Byte.class, ptn, ntp);
add(float.class, Float.class, ptn, ntp);
add(double.class, Double.class, ptn, ntp);
add(boolean.class, Boolean.class, ptn, ntp);
PRIMITIVE_TO_NAME = Collections.unmodifiableMap(ptn);
NAME_TO_PRIMITIVE = Collections.unmodifiableMap(ntp);
}
private static void add(Class<?> primitive, Class<?> boxed, Map<Class<?>, String> primitiveToName,
Map<String, Class<?>> nameToPrimitive)
{
String name = primitive.getCanonicalName();
primitiveToName.put(primitive, name);
primitiveToName.put(boxed, name);
nameToPrimitive.put(name, boxed);
}
}
/**
* Internally used exception that is thrown when an expected
* {@link Persistent} element is missing.
*/
protected static class MissingPersistentException extends PersistenceException
{
public MissingPersistentException(String message)
{
super(message);
}
}
}