/*
* 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 org.apache.pivot.wtkx;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.xml.stream.Location;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamException;
import org.apache.pivot.beans.BeanDictionary;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.serialization.Serializer;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Resources;
import org.apache.pivot.util.ThreadUtilities;
import org.apache.pivot.util.Vote;
/**
* Loads an object hierarchy from an XML document.
*/
public class WTKXSerializer implements Serializer<Object>, Dictionary<String, Object> {
private class NamedObjectBindings implements Bindings {
@Override
public Object get(Object key) {
return namedObjects.get(key.toString());
}
@Override
public Object put(String key, Object value) {
return namedObjects.put(key, value);
}
@Override
public void putAll(Map<? extends String, ? extends Object> map) {
for (String key : map.keySet()) {
put(key, map.get(key));
}
}
@Override
public Object remove(Object key) {
return namedObjects.remove(key.toString());
}
@Override
public void clear() {
namedObjects.clear();
}
@Override
public boolean containsKey(Object key) {
return namedObjects.containsKey(key.toString());
}
@Override
public boolean containsValue(Object value) {
boolean contains = false;
for (String key : namedObjects) {
if (namedObjects.get(key).equals(value)) {
contains = true;
break;
}
}
return contains;
}
@Override
public boolean isEmpty() {
return namedObjects.isEmpty();
}
@Override
public Set<String> keySet() {
java.util.HashSet<String> keySet = new java.util.HashSet<String>();
for (String key : namedObjects) {
keySet.add(key);
}
return keySet;
}
@Override
public Set<Entry<String, Object>> entrySet() {
java.util.HashMap<String, Object> hashMap = new java.util.HashMap<String, Object>();
for (String key : namedObjects) {
hashMap.put(key, namedObjects.get(key));
}
return hashMap.entrySet();
}
@Override
public int size() {
return namedObjects.getCount();
}
@Override
public Collection<Object> values() {
java.util.ArrayList<Object> values = new java.util.ArrayList<Object>();
for (String key : namedObjects) {
values.add(namedObjects.get(key));
}
return values;
}
}
private static class Element {
public enum Type {
DEFINE,
INSTANCE,
INCLUDE,
SCRIPT,
READ_ONLY_PROPERTY,
WRITABLE_PROPERTY
}
public final Element parent;
public final Type type;
public final String tagName;
public final int lineNumber;
public final List<Attribute> attributes;
public Object value;
public Element(Element parent, Type type, String tagName, int lineNumber,
List<Attribute> attributes, Object value) {
this.parent = parent;
this.type = type;
this.tagName = tagName;
this.lineNumber = lineNumber;
this.attributes = attributes;
this.value = value;
}
}
private static class Attribute {
public final String namespaceURI;
public final String prefix;
public final String localName;
public final String value;
public Attribute(String namespaceURI, String prefix, String localName, String value) {
this.namespaceURI = namespaceURI;
this.prefix = prefix;
this.localName = localName;
this.value = value;
}
}
private static class WTKXSerializerListenerList extends ListenerList<WTKXSerializerListener>
implements WTKXSerializerListener {
@Override
public void includeLoaded(WTKXSerializer serializer, String id) {
for (WTKXSerializerListener listener : this) {
listener.includeLoaded(serializer, id);
}
}
@Override
public void allIncludesLoaded(WTKXSerializer serializer) {
for (WTKXSerializerListener listener : this) {
listener.allIncludesLoaded(serializer);
}
}
}
private URL location = null;
private Resources resources = null;
private HashMap<String, Object> initialBindings = new HashMap<String, Object>();
private Object root = null;
private HashMap<String, Object> namedObjects = new HashMap<String, Object>();
private HashMap<String, WTKXSerializer> includeSerializers = new HashMap<String, WTKXSerializer>();
private ScriptEngineManager scriptEngineManager = null;
private XMLInputFactory xmlInputFactory;
private Element element = null;
private WTKXSerializerListenerList wtkxSerializerListeners = new WTKXSerializerListenerList();
public static final char URL_PREFIX = '@';
public static final char RESOURCE_KEY_PREFIX = '%';
public static final char OBJECT_REFERENCE_PREFIX = '$';
public static final String WTKX_PREFIX = "wtkx";
public static final String ID_ATTRIBUTE = "id";
public static final String INCLUDE_TAG = "include";
public static final String INCLUDE_SRC_ATTRIBUTE = "src";
public static final String INCLUDE_RESOURCES_ATTRIBUTE = "resources";
public static final String INCLUDE_ASYNCHRONOUS_ATTRIBUTE = "asynchronous";
public static final String SCRIPT_TAG = "script";
public static final String SCRIPT_SRC_ATTRIBUTE = "src";
public static final String SCRIPT_LANGUAGE_ATTRIBUTE = "language";
public static final String DEFINE_TAG = "define";
public static final String MIME_TYPE = "application/wtkx";
public WTKXSerializer() {
this(null);
}
public WTKXSerializer(Resources resources) {
this.resources = resources;
xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", true);
}
public Resources getResources() {
return resources;
}
public Object readObject(String resourceName)
throws IOException, SerializationException {
if (resourceName == null) {
throw new IllegalArgumentException("resourceName is null.");
}
ClassLoader classLoader = ThreadUtilities.getClassLoader();
URL location = classLoader.getResource(resourceName);
if (location == null) {
throw new SerializationException("Could not find resource named \""
+ resourceName + "\".");
}
return readObject(location);
}
public Object readObject(Object baseObject, String resourceName)
throws IOException, SerializationException {
if (baseObject == null) {
throw new IllegalArgumentException("baseObject is null.");
}
if (resourceName == null) {
throw new IllegalArgumentException("resourceName is null.");
}
return readObject(baseObject.getClass(), resourceName);
}
public Object readObject(Class<?> baseType, String resourceName)
throws IOException, SerializationException {
if (baseType == null) {
throw new IllegalArgumentException("baseType is null.");
}
if (resourceName == null) {
throw new IllegalArgumentException("resourceName is null.");
}
return readObject(baseType.getResource(resourceName));
}
public Object readObject(URL location)
throws IOException, SerializationException {
if (location == null) {
throw new IllegalArgumentException("location is null.");
}
this.location = location;
InputStream inputStream = new BufferedInputStream(location.openStream());
try {
return readObject(inputStream);
} finally {
inputStream.close();
}
}
@SuppressWarnings({"unchecked"})
@Override
public Object readObject(InputStream inputStream)
throws IOException, SerializationException {
if (inputStream == null) {
throw new IllegalArgumentException("inputStream is null.");
}
// Add the initial bindings
for (String key : initialBindings) {
namedObjects.put(key, initialBindings.get(key));
}
initialBindings.clear();
// Parse the XML stream
element = null;
try {
try {
XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream);
while (reader.hasNext()) {
int event = reader.next();
switch (event) {
case XMLStreamConstants.CHARACTERS: {
if (!reader.isWhiteSpace()) {
String text = reader.getText();
if (text.length() > 0) {
switch (element.type) {
case INSTANCE: {
if (element.value instanceof Sequence) {
Sequence<Object> sequence = (Sequence<Object>)element.value;
try {
Method addMethod = sequence.getClass().getMethod("add",
new Class<?>[] {String.class});
addMethod.invoke(sequence, new Object[] {text});
} catch (NoSuchMethodException exception) {
throw new SerializationException("Text content cannot be added to "
+ sequence.getClass().getName() + ".", exception);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
}
}
break;
}
case SCRIPT:
case WRITABLE_PROPERTY: {
element.value = text;
break;
}
default: {
throw new SerializationException("Unexpected characters in "
+ element.type + " element.");
}
}
}
}
break;
}
case XMLStreamConstants.START_ELEMENT: {
// Get element properties
String namespaceURI = reader.getNamespaceURI();
String prefix = reader.getPrefix();
String localName = reader.getLocalName();
// Build attribute list; these will be processed in the close tag
ArrayList<Attribute> attributes = new ArrayList<Attribute>();
for (int i = 0, n = reader.getAttributeCount(); i < n; i++) {
String attributeNamespaceURI = reader.getAttributeNamespace(i);
if (attributeNamespaceURI == null) {
attributeNamespaceURI = reader.getNamespaceURI("");
}
String attributePrefix = reader.getAttributePrefix(i);
String attributeLocalName = reader.getAttributeLocalName(i);
String attributeValue = reader.getAttributeValue(i);
attributes.add(new Attribute(attributeNamespaceURI,
attributePrefix, attributeLocalName, attributeValue));
}
// Determine the type and value of this element
Element.Type elementType = null;
Object value = null;
if (prefix != null
&& prefix.equals(WTKX_PREFIX)) {
// The element represents a WTKX operation
if (element == null) {
throw new SerializationException(prefix + ":" + localName
+ " is not a valid root element.");
}
if (localName.equals(INCLUDE_TAG)) {
elementType = Element.Type.INCLUDE;
} else if (localName.equals(SCRIPT_TAG)) {
elementType = Element.Type.SCRIPT;
} else if (localName.equals(DEFINE_TAG)) {
if (attributes.getLength() > 0) {
throw new SerializationException(WTKX_PREFIX + ":" + DEFINE_TAG
+ " cannot have attributes.");
}
elementType = Element.Type.DEFINE;
} else {
throw new SerializationException(prefix + ":" + localName
+ " is not a valid element.");
}
} else {
if (Character.isUpperCase(localName.charAt(0))) {
// The element represents a typed object
if (namespaceURI == null) {
throw new SerializationException("No XML namespace specified for "
+ localName + " tag.");
}
String className = namespaceURI + "." + localName.replace('.', '$');
try {
Class<?> type = Class.forName(className);
elementType = Element.Type.INSTANCE;
value = type.newInstance();
} catch (Exception exception) {
throw new SerializationException(exception);
}
} else {
// The element represents a property
if (element == null
|| element.type != Element.Type.INSTANCE) {
throw new SerializationException("Parent element must be a typed object.");
}
if (prefix != null
&& prefix.length() > 0) {
throw new SerializationException("Property elements cannot have a namespace prefix.");
}
BeanDictionary beanDictionary = new BeanDictionary(element.value);
if (beanDictionary.isReadOnly(localName)) {
elementType = Element.Type.READ_ONLY_PROPERTY;
value = beanDictionary.get(localName);
assert (value != null) : "Read-only properties cannot be null.";
if (attributes.getLength() > 0
&& !(value instanceof Dictionary<?, ?>)) {
throw new SerializationException("Only read-only dictionaries can have attributes.");
}
} else {
if (attributes.getLength() > 0) {
throw new SerializationException("Writable property elements cannot have attributes.");
}
elementType = Element.Type.WRITABLE_PROPERTY;
}
}
}
// Set the current element
String tagName = localName;
if (prefix != null
&& prefix.length() > 0) {
tagName = prefix + ":" + tagName;
}
Location xmlStreamLocation = reader.getLocation();
element = new Element(element, elementType, tagName, xmlStreamLocation.getLineNumber(),
attributes, value);
// If this is the root, set it
if (element.parent == null) {
root = element.value;
}
break;
}
case XMLStreamConstants.END_ELEMENT: {
String localName = reader.getLocalName();
switch (element.type) {
case INSTANCE:
case INCLUDE: {
String id = null;
ArrayList<Attribute> instancePropertyAttributes = new ArrayList<Attribute>();
ArrayList<Attribute> staticPropertyAttributes = new ArrayList<Attribute>();
if (element.type == Element.Type.INCLUDE) {
// Process attributes looking for wtkx:id, src, resources, asynchronous,
// and static property setters only
String src = null;
Resources resources = this.resources;
for (Attribute attribute : element.attributes) {
if (attribute.prefix != null
&& attribute.prefix.equals(WTKX_PREFIX)) {
if (attribute.localName.equals(ID_ATTRIBUTE)) {
id = attribute.value;
} else {
throw new SerializationException(WTKX_PREFIX + ":" + attribute.localName
+ " is not a valid attribute.");
}
} else {
if (attribute.localName.equals(INCLUDE_SRC_ATTRIBUTE)) {
src = attribute.value;
} else if (attribute.localName.equals(INCLUDE_RESOURCES_ATTRIBUTE)) {
resources = new Resources(resources, attribute.value);
} else if (attribute.localName.equals(INCLUDE_ASYNCHRONOUS_ATTRIBUTE)) {
// TODO
throw new UnsupportedOperationException("Asynchronous includes are not"
+ " yet supported.");
} else {
if (!Character.isUpperCase(attribute.localName.charAt(0))) {
throw new SerializationException("Instance property setters are not"
+ " supported for " + WTKX_PREFIX + ":" + INCLUDE_TAG
+ " " + " tag.");
}
staticPropertyAttributes.add(attribute);
}
}
}
if (src == null) {
throw new SerializationException(INCLUDE_SRC_ATTRIBUTE
+ " attribute is required for " + WTKX_PREFIX + ":" + INCLUDE_TAG
+ " tag.");
}
// Read the object
WTKXSerializer serializer = new WTKXSerializer(resources);
if (id != null) {
includeSerializers.put(id, serializer);
}
if (src.charAt(0) == '/') {
element.value = serializer.readObject(src.substring(1));
} else {
element.value = serializer.readObject(new URL(location, src));
}
if (id == null
&& !serializer.isEmpty()
&& serializer.scriptEngineManager == null) {
System.err.println("Include \"" + src + "\" defines unreachable objects.");
}
} else {
// Process attributes looking for wtkx:id and all property setters
for (Attribute attribute : element.attributes) {
if (attribute.prefix != null
&& attribute.prefix.equals(WTKX_PREFIX)) {
if (attribute.localName.equals(ID_ATTRIBUTE)) {
id = attribute.value;
} else {
throw new SerializationException(WTKX_PREFIX + ":" + attribute.localName
+ " is not a valid attribute.");
}
} else {
if (Character.isUpperCase(attribute.localName.charAt(0))) {
staticPropertyAttributes.add(attribute);
} else {
instancePropertyAttributes.add(attribute);
}
}
}
}
// If an ID was specified, add the value to the named object map
if (id != null) {
if (id.length() == 0) {
throw new IllegalArgumentException(WTKX_PREFIX + ":" + ID_ATTRIBUTE
+ " must not be null.");
}
namedObjects.put(id, element.value);
}
// Apply instance attributes
Dictionary<String, Object> dictionary;
if (element.value instanceof Dictionary) {
dictionary = (Dictionary<String, Object>)element.value;
} else {
dictionary = new BeanDictionary(element.value);
}
for (Attribute attribute : instancePropertyAttributes) {
dictionary.put(attribute.localName, resolve(attribute.value));
}
// If the element's parent is a sequence or a listener list, add
// the element value to it
if (element.parent != null) {
if (element.parent.value instanceof Sequence) {
Sequence<Object> sequence = (Sequence<Object>)element.parent.value;
sequence.add(element.value);
} else {
if (element.parent.value instanceof ListenerList) {
ListenerList<Object> listenerList = (ListenerList<Object>)element.parent.value;
listenerList.add(element.value);
}
}
}
// Apply static attributes
if (element.value instanceof Dictionary) {
if (staticPropertyAttributes.getLength() > 0) {
throw new SerializationException("Static setters are only supported"
+ " for typed objects.");
}
} else {
for (Attribute attribute : staticPropertyAttributes) {
setStaticProperty(attribute, element.value);
}
}
// If the parent element is a writable property, set this as its
// value; it will be applied later in the parent's closing tag
if (element.parent != null
&& element.parent.type == Element.Type.WRITABLE_PROPERTY) {
element.parent.value = element.value;
}
break;
}
case READ_ONLY_PROPERTY: {
if (element.value instanceof Dictionary<?, ?>) {
// Process attributes looking for instance property setters
for (Attribute attribute : element.attributes) {
if (Character.isUpperCase(attribute.localName.charAt(0))) {
throw new SerializationException("Static setters are not supported"
+ " for read-only properties.");
}
Dictionary<String, Object> dictionary =
(Dictionary<String, Object>)element.value;
dictionary.put(attribute.localName, resolve(attribute.value));
}
}
break;
}
case WRITABLE_PROPERTY: {
BeanDictionary beanDictionary = new BeanDictionary(element.parent.value);
beanDictionary.put(localName, element.value);
break;
}
case SCRIPT: {
// Load the script engine manager
if (scriptEngineManager == null) {
scriptEngineManager = new javax.script.ScriptEngineManager();
scriptEngineManager.setBindings(new NamedObjectBindings());
}
// Process attributes looking for src and language
String src = null;
String language = null;
for (Attribute attribute : element.attributes) {
if (attribute.prefix != null
&& attribute.prefix.length() > 0) {
throw new SerializationException(attribute.prefix + ":" +
attribute.localName + " is not a valid" + " attribute for the "
+ WTKX_PREFIX + ":" + SCRIPT_TAG + " tag.");
}
if (attribute.localName.equals(SCRIPT_SRC_ATTRIBUTE)) {
src = attribute.value;
} else if (attribute.localName.equals(SCRIPT_LANGUAGE_ATTRIBUTE)) {
language = attribute.value;
} else {
throw new SerializationException(attribute.localName + " is not a valid"
+ " attribute for the " + WTKX_PREFIX + ":" + SCRIPT_TAG + " tag.");
}
}
if (element.value != null
&& language == null) {
language = "javascript";
}
Bindings bindings;
if (element.parent.value instanceof ListenerList<?>) {
// Don't pollute the engine namespace with the listener functions
bindings = new SimpleBindings();
} else {
bindings = scriptEngineManager.getBindings();
}
// Execute script
final ScriptEngine scriptEngine;
if (src != null) {
// The script is located in an external file
if (language != null) {
throw new SerializationException("Cannot combine " + SCRIPT_SRC_ATTRIBUTE
+ " and " + SCRIPT_LANGUAGE_ATTRIBUTE + " in a "
+ WTKX_PREFIX + ":" + SCRIPT_TAG + " tag.");
}
int i = src.lastIndexOf(".");
if (i == -1) {
throw new SerializationException("Cannot determine type of script \""
+ src + "\".");
}
String extension = src.substring(i + 1);
scriptEngine = scriptEngineManager.getEngineByExtension(extension);
if (scriptEngine == null) {
throw new SerializationException("Unable to find scripting engine for"
+ " extension " + extension + ".");
}
scriptEngine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
try {
URL scriptLocation;
if (src.charAt(0) == '/') {
ClassLoader classLoader = ThreadUtilities.getClassLoader();
scriptLocation = classLoader.getResource(src);
} else {
scriptLocation = new URL(location, src);
}
BufferedReader scriptReader = null;
try {
scriptReader = new BufferedReader(new InputStreamReader(scriptLocation.openStream()));
scriptEngine.eval(scriptReader);
} catch(ScriptException exception) {
exception.printStackTrace(System.err);
} finally {
if (scriptReader != null) {
scriptReader.close();
}
}
} catch (IOException exception) {
throw new SerializationException(exception);
}
} else if (language != null) {
// The script is inline
scriptEngine = scriptEngineManager.getEngineByName(language);
if (scriptEngine == null) {
throw new SerializationException("Unable to find scripting engine for"
+ " language " + language + ".");
}
scriptEngine.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
if (element.value != null) {
try {
scriptEngine.eval((String)element.value);
} catch (ScriptException exception) {
exception.printStackTrace(System.err);
}
}
} else {
throw new SerializationException("Either " + SCRIPT_SRC_ATTRIBUTE + " or "
+ SCRIPT_LANGUAGE_ATTRIBUTE + " is required for the "
+ WTKX_PREFIX + ":" + SCRIPT_TAG + " tag.");
}
if (element.parent.value instanceof ListenerList<?>) {
// Create an invocation handler for this listener
Class<?> listenerListClass = element.parent.value.getClass();
Method addMethod;
try {
addMethod = listenerListClass.getMethod("add",
new Class<?>[] {Object.class});
} catch (NoSuchMethodException exception) {
throw new RuntimeException(exception);
}
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
String methodName = method.getName();
Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
if (bindings.containsKey(methodName)) {
Invocable invocable;
try {
invocable = (Invocable)scriptEngine;
} catch (ClassCastException exception) {
throw new SerializationException(exception);
}
result = invocable.invokeFunction(methodName, args);
}
// If the function didn't return a value, return the default
Class<?> returnType = method.getReturnType();
if (returnType == Vote.class
&& result == null) {
result = Vote.APPROVE;
} else if (returnType == Boolean.TYPE
&& result == null) {
result = false;
}
return result;
}
};
// Create the listener and add it to the list
java.lang.reflect.Type[] genericInterfaces = listenerListClass.getGenericInterfaces();
Class<?> listenerClass = (Class<?>)genericInterfaces[0];
Object listener =
Proxy.newProxyInstance(ThreadUtilities.getClassLoader(),
new Class[]{listenerClass}, handler);
try {
addMethod.invoke(element.parent.value, new Object[] {listener});
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
}
}
break;
}
case DEFINE: {
// No-op
}
}
// Move up the stack
if (element.parent != null) {
element = element.parent;
}
break;
}
}
}
reader.close();
} catch (XMLStreamException exception) {
throw new SerializationException(exception);
}
} catch (IOException exception) {
logException(exception);
throw exception;
} catch (SerializationException exception) {
logException(exception);
throw exception;
} catch (RuntimeException exception) {
logException(exception);
throw exception;
}
// Clear the location so the previous value won't be re-used in a
// subsequent call to this method
location = null;
return root;
}
private void logException(Exception exception) {
String message = "An error occurred while processing element <" + getTagName() + ">"
+ " starting at line number " + getLineNumber();
if (location != null) {
message += " in file " + location.getPath();
}
message += ":";
System.err.println(message);
exception.printStackTrace();
}
@Override
public void writeObject(Object object, OutputStream outputStream) throws IOException,
SerializationException {
throw new UnsupportedOperationException();
}
@Override
public String getMIMEType(Object object) {
return MIME_TYPE;
}
/**
* Retrieves a named object.
*
* @param name
* The name of the object, relative to this loader. The object's name is
* the concatenation of its parent IDs and its ID, separated by periods
* (e.g. "foo.bar.baz").
*
* @return The named object, or <tt>null</tt> if an object with the given
* name does not exist. Use {@link #containsKey(String)} to distinguish
* between the two cases.
*/
@Override
public Object get(String name) {
if (name == null) {
throw new IllegalArgumentException("name is null.");
}
WTKXSerializer serializer = this;
String[] path = name.split("\\.");
Object object = null;
if (root == null) {
object = initialBindings.get(name);
} else {
int i = 0;
int n = path.length - 1;
while (i < n && serializer != null) {
String namespace = path[i++];
serializer = serializer.includeSerializers.get(namespace);
}
String id = path[i];
if (serializer != null
&& serializer.namedObjects.containsKey(id)) {
object = serializer.namedObjects.get(id);
}
}
return object;
}
@Override
public Object put(String id, Object value) {
if (id == null) {
throw new IllegalArgumentException("id is null.");
}
root = null;
namedObjects.clear();
includeSerializers.clear();
return initialBindings.put(id, value);
}
@Override
public Object remove(String id) {
if (id == null) {
throw new IllegalArgumentException("id is null.");
}
if (root != null) {
throw new IllegalStateException();
}
return initialBindings.remove(id);
}
@Override
public boolean containsKey(String name) {
if (name == null) {
throw new IllegalArgumentException("name is null.");
}
WTKXSerializer serializer = this;
String[] path = name.split("\\.");
boolean result = false;
if (root == null) {
result = initialBindings.containsKey(name);
} else {
int i = 0;
int n = path.length - 1;
while (i < n && serializer != null) {
String namespace = path[i++];
serializer = serializer.includeSerializers.get(namespace);
}
String id = path[i];
result = (serializer != null
&& serializer.namedObjects.containsKey(id));
}
return result;
}
@Override
public boolean isEmpty() {
boolean empty = false;
if (root == null) {
empty = initialBindings.isEmpty();
} else {
empty = namedObjects.isEmpty()
&& includeSerializers.isEmpty();
}
return empty;
}
/**
* Retrieves the root of the object hierarchy most recently processed by
* this serializer.
*
* @return
* The root object, or <tt>null</tt> if this serializer has not yet read an
* object from an input stream.
*/
public Object getRoot() {
return root;
}
/**
* Retrieves an include serializer by its ID.
*
* @param id
* The ID of the serializer, relative to this loader. The serializer's ID
* is the concatentation of its parent IDs and its ID, separated by periods
* (e.g. "foo.bar.baz").
*
* @return The named serializer, or <tt>null</tt> if a serializer with the
* given name does not exist.
*/
public WTKXSerializer getSerializer(String id) {
if (id == null) {
throw new IllegalArgumentException("id is null.");
}
WTKXSerializer serializer = this;
String[] namespacePath = id.split("\\.");
int i = 0;
int n = namespacePath.length;
while (i < n && serializer != null) {
String namespace = namespacePath[i++];
serializer = serializer.includeSerializers.get(namespace);
}
return serializer;
}
/**
* Returns the name of the element currently being processed.
*
* @return
* The name of the element currently being processed, or <tt>null</tt> if
* no element is currently being processed.
*/
public String getTagName() {
return (element == null ? null : element.tagName);
}
/**
* Returns the line number of the element currently being processed.
*
* @return
* The line number of the element currently being processed, or <tt>-1</tt>
* if no element is currently being processed.
*/
public int getLineNumber() {
return (element == null ? -1 : element.lineNumber);
}
/**
* Returns the location of the WTKX currently being processed.
*
* @return
* The location of the WTKX, or <tt>null</tt> if no WTKX is currently being
* processed or the location is not known.
*/
public URL getLocation() {
return location;
}
/**
* Applies WTKX binding annotations to an object.
* <p>
* NOTE This method uses reflection to set internal member variables. As
* a result, it may only be called from trusted code.
*
* @param t
* @param type
*
* @throws BindException
* If an error occurs during binding
*/
public <T> void bind(T t, Class<? super T> type) throws BindException {
Field[] fields = type.getDeclaredFields();
// Process bind annotations
for (int j = 0, n = fields.length; j < n; j++) {
Field field = fields[j];
String fieldName = field.getName();
int fieldModifiers = field.getModifiers();
WTKX wtkxAnnotation = field.getAnnotation(WTKX.class);
if (wtkxAnnotation != null) {
// Ensure that we can write to the field
if ((fieldModifiers & Modifier.FINAL) > 0) {
throw new BindException(fieldName + " is final.");
}
if ((fieldModifiers & Modifier.PUBLIC) == 0) {
try {
field.setAccessible(true);
} catch (SecurityException exception) {
throw new BindException(fieldName + " is not accessible.");
}
}
String id = wtkxAnnotation.id();
if (id.equals("\0")) {
id = field.getName();
}
if (containsKey(id)) {
// Set the value into the field
Object value = get(id);
try {
field.set(t, value);
} catch (IllegalAccessException exception) {
throw new BindException(exception);
}
}
}
}
}
/**
* Resolves an attribute value as either a URL, resource value, or
* object reference, depending on the value's prefix. If the value can't
* or doesn't need to be resolved, the original attribute value is
* returned.
*
* @param attributeValue
* The attribute value to resolve.
*
* @return
* The resolved value.
*/
private Object resolve(String attributeValue)
throws MalformedURLException {
Object resolvedValue = null;
if (attributeValue.length() > 0) {
if (attributeValue.charAt(0) == URL_PREFIX) {
if (attributeValue.length() > 1) {
if (attributeValue.charAt(1) == URL_PREFIX) {
resolvedValue = attributeValue.substring(1);
} else {
if (location == null) {
throw new IllegalStateException("Base location is undefined.");
}
resolvedValue = new URL(location, attributeValue.substring(1));
}
}
} else if (attributeValue.charAt(0) == RESOURCE_KEY_PREFIX) {
if (attributeValue.length() > 1) {
if (attributeValue.charAt(1) == RESOURCE_KEY_PREFIX) {
resolvedValue = attributeValue.substring(1);
} else {
if (resources == null) {
throw new IllegalStateException("Resources is null.");
}
resolvedValue = resources.get(attributeValue.substring(1));
if (resolvedValue == null) {
resolvedValue = attributeValue;
}
}
}
} else if (attributeValue.charAt(0) == OBJECT_REFERENCE_PREFIX) {
if (attributeValue.length() > 1) {
if (attributeValue.charAt(1) == OBJECT_REFERENCE_PREFIX) {
resolvedValue = attributeValue.substring(1);
} else {
resolvedValue = get(attributeValue.substring(1));
if (resolvedValue == null) {
resolvedValue = attributeValue;
}
}
}
} else {
resolvedValue = attributeValue;
}
} else {
resolvedValue = attributeValue;
}
return resolvedValue;
}
/**
* Invokes a static property setter.
*
* @param attribute
* The attribute whose corresponding static setter is to be invoked.
*
* @param object
* The object on which to invoke the static setter.
*/
private void setStaticProperty(Attribute attribute, Object object)
throws SerializationException, MalformedURLException {
Object value = resolve(attribute.value);
Class<?> objectType = object.getClass();
String propertyName =
attribute.localName.substring(attribute.localName.lastIndexOf(".") + 1);
propertyName = Character.toUpperCase(propertyName.charAt(0)) +
propertyName.substring(1);
String propertyClassName = attribute.namespaceURI + "."
+ attribute.localName.substring(0, attribute.localName.length()
- (propertyName.length() + 1));
Class<?> propertyClass = null;
try {
propertyClass = Class.forName(propertyClassName);
} catch (ClassNotFoundException exception) {
throw new SerializationException(exception);
}
Method setterMethod = null;
if (value != null) {
setterMethod = getStaticSetterMethod(propertyClass, propertyName,
objectType, value.getClass());
}
if (setterMethod == null) {
Method getterMethod = getStaticGetterMethod(propertyClass, propertyName, objectType);
if (getterMethod != null) {
Class<?> propertyType = getterMethod.getReturnType();
setterMethod = getStaticSetterMethod(propertyClass, propertyName,
objectType, propertyType);
if (value instanceof String) {
value = BeanDictionary.coerce((String)value, propertyType);
}
}
}
if (setterMethod == null) {
throw new SerializationException(attribute.localName + " is not valid static property.");
}
// Invoke the setter
try {
setterMethod.invoke(null, new Object[] {object, value});
} catch (Exception exception) {
throw new SerializationException(exception);
}
}
private Method getStaticGetterMethod(Class<?> propertyClass, String propertyName,
Class<?> objectType) {
Method method = null;
if (objectType != null) {
try {
method = propertyClass.getMethod(BeanDictionary.GET_PREFIX
+ propertyName, new Class<?>[] {objectType});
} catch (NoSuchMethodException exception) {
// No-op
}
if (method == null) {
try {
method = propertyClass.getMethod(BeanDictionary.IS_PREFIX
+ propertyName, new Class<?>[] {objectType});
} catch (NoSuchMethodException exception) {
// No-op
}
}
if (method == null) {
method = getStaticGetterMethod(propertyClass, propertyName,
objectType.getSuperclass());
}
}
return method;
}
private Method getStaticSetterMethod(Class<?> propertyClass, String propertyName,
Class<?> objectType, Class<?> propertyValueType) {
Method method = null;
if (objectType != null) {
final String methodName = BeanDictionary.SET_PREFIX + propertyName;
try {
method = propertyClass.getMethod(methodName,
new Class<?>[] {objectType, propertyValueType});
} catch (NoSuchMethodException exception) {
// No-op
}
if (method == null) {
// If value type is a primitive wrapper, look for a method
// signature with the corresponding primitive type
try {
Field primitiveTypeField = propertyValueType.getField("TYPE");
Class<?> primitivePropertyValueType = (Class<?>)primitiveTypeField.get(null);
try {
method = propertyClass.getMethod(methodName,
new Class<?>[] {objectType, primitivePropertyValueType});
} catch (NoSuchMethodException exception) {
// No-op
}
} catch (NoSuchFieldException exception) {
// No-op; not a wrapper type
} catch (IllegalAccessException exception) {
// No-op; not a wrapper type
}
}
if (method == null) {
method = getStaticSetterMethod(propertyClass, propertyName,
objectType.getSuperclass(), propertyValueType);
}
}
return method;
}
public ListenerList<WTKXSerializerListener> getWTKXSerializerListeners() {
return wtkxSerializerListeners;
}
}