blob: 77b0b896ece805b9f9629b81aa815001b0f1b34e [file] [log] [blame]
/*
* 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.beans;
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 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.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.util.StreamReaderDelegate;
import org.apache.pivot.collections.Dictionary;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.LinkedList;
import org.apache.pivot.collections.Map;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.collections.adapter.MapAdapter;
import org.apache.pivot.json.JSON;
import org.apache.pivot.json.JSONSerializer;
import org.apache.pivot.serialization.BinarySerializer;
import org.apache.pivot.serialization.ByteArraySerializer;
import org.apache.pivot.serialization.CSVSerializer;
import org.apache.pivot.serialization.PropertiesSerializer;
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.Utils;
import org.apache.pivot.util.Vote;
/**
* Loads an object hierarchy from an XML document.
*/
public class BXMLSerializer implements Serializer<Object>, Resolvable {
private static class Element {
public enum Type {
INSTANCE, READ_ONLY_PROPERTY, WRITABLE_PROPERTY, LISTENER_LIST_PROPERTY, INCLUDE, SCRIPT, DEFINE, REFERENCE
}
public final Element parent;
public final Type type;
public final Class<?> propertyClass;
public final String name;
public Object value;
public String id = null;
public final HashMap<String, String> properties = new HashMap<>();
public final LinkedList<Attribute> attributes = new LinkedList<>();
public Element(final Element parent, final Type type, final String name,
final Class<?> propertyClass, final Object value) {
this.parent = parent;
this.type = type;
this.name = name;
this.propertyClass = propertyClass;
this.value = value;
}
}
private static class Attribute {
public final Element element;
public final String name;
public final Class<?> propertyClass;
public Object value;
public Attribute(final Element element, final String name, final Class<?> propertyClass, final Object value) {
this.element = element;
this.name = name;
this.propertyClass = propertyClass;
this.value = value;
}
}
/* private static void printBindings(final String message, final java.util.Map<String,Object> bindings) {
System.out.format("===== %1$s =====%n", message);
System.out.format("--- Bindings %1$s=%2$s ---%n", bindings, bindings.getClass().getName());
for (String key : bindings.keySet()) {
Object value = bindings.get(key);
System.out.format("key: %1$s, value: %2$s [%3$s]%n",
key, value, Integer.toHexString(System.identityHashCode(value)));
if (key.equals(NASHORN_GLOBAL)) {
Bindings globalBindings = (Bindings)value;
for (String globalKey : globalBindings.keySet()) {
Object globalValue = globalBindings.get(globalKey);
System.out.format(" global key: %1$s, value: %2$s [%3$s]%n",
globalKey, globalValue, Integer.toHexString(System.identityHashCode(globalValue)));
}
}
}
System.out.println("=====================");
} */
private class AttributeInvocationHandler implements InvocationHandler {
private ScriptEngine scriptEngine;
private String event;
private String script;
private static final String ARGUMENTS_KEY = "arguments";
public AttributeInvocationHandler(final ScriptEngine scriptEngine, final String event, final String script) {
this.scriptEngine = scriptEngine;
this.event = event;
this.script = script;
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
Object result = null;
String methodName = method.getName();
if (methodName.equals(event)) {
try {
Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
bindings.put(ARGUMENTS_KEY, args);
result = scriptEngine.eval(script);
bindings.remove(ARGUMENTS_KEY);
} catch (ScriptException exception) {
reportException(exception, script);
}
}
// If the function didn't return a value, return the default
if (result == null) {
Class<?> returnType = method.getReturnType();
if (returnType == Vote.class) {
result = Vote.APPROVE;
} else if (returnType == Boolean.TYPE) {
result = Boolean.FALSE;
}
}
return result;
}
}
private static class ElementInvocationHandler implements InvocationHandler {
private ScriptEngine scriptEngine;
public ElementInvocationHandler(final ScriptEngine scriptEngine) {
this.scriptEngine = scriptEngine;
}
private Object invokeMethod(final String methodName, final Object[] args) throws Throwable {
Invocable invocable;
try {
invocable = (Invocable) scriptEngine;
} catch (ClassCastException exception) {
throw new SerializationException(exception);
}
return invocable.invokeFunction(methodName, args);
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
Object result = null;
String methodName = method.getName();
Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
if (bindings.containsKey(methodName)) {
result = invokeMethod(methodName, args);
} else if (bindings.containsKey(NASHORN_GLOBAL)) {
Bindings globalBindings = (Bindings) bindings.get(NASHORN_GLOBAL);
if (globalBindings.containsKey(methodName)) {
result = invokeMethod(methodName, args);
} else {
bindings = scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
if (bindings.containsKey(methodName)) {
result = invokeMethod(methodName, args);
}
}
} else {
bindings = scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
if (bindings.containsKey(methodName)) {
result = invokeMethod(methodName, args);
}
}
// If the function didn't return a value, return the default
if (result == null) {
Class<?> returnType = method.getReturnType();
if (returnType == Vote.class) {
result = Vote.APPROVE;
} else if (returnType == Boolean.TYPE) {
result = Boolean.FALSE;
}
}
return result;
}
}
private static class ScriptBindMapping implements NamespaceBinding.BindMapping {
private ScriptEngine scriptEngine;
private String functionName;
public ScriptBindMapping(final ScriptEngine scriptEngine, final String functionName) {
this.scriptEngine = scriptEngine;
this.functionName = functionName;
}
private Object invokeFunction(final String functionName, final Object value) {
Invocable invocable;
try {
invocable = (Invocable) scriptEngine;
} catch (ClassCastException exception) {
throw new RuntimeException(exception);
}
Object result = value;
try {
result = invocable.invokeFunction(functionName, value);
} catch (NoSuchMethodException exception) {
throw new RuntimeException(exception);
} catch (ScriptException exception) {
throw new RuntimeException(exception);
}
return result;
}
@Override
public Object evaluate(final Object value) {
Object result = value;
Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
if (bindings.containsKey(functionName)) {
result = invokeFunction(functionName, result);
} else if (bindings.containsKey(NASHORN_GLOBAL)) {
Bindings globalBindings = (Bindings) bindings.get(NASHORN_GLOBAL);
if (globalBindings.containsKey(functionName)) {
result = invokeFunction(functionName, result);
} else {
bindings = scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
if (bindings.containsKey(functionName)) {
result = invokeFunction(functionName, result);
} else {
throw new RuntimeException("Mapping function \"" + functionName
+ "\" is not defined.");
}
}
} else {
bindings = scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
if (bindings.containsKey(functionName)) {
result = invokeFunction(functionName, result);
} else {
throw new RuntimeException("Mapping function \"" + functionName
+ "\" is not defined.");
}
}
return result;
}
}
private XMLInputFactory xmlInputFactory;
private ScriptEngineManager scriptEngineManager;
private Bindings bindings = new SimpleBindings();
private Map<String, Object> namespace = new MapAdapter<String, Object>(bindings);
private URL location = null;
private Resources resources = null;
private XMLStreamReader xmlStreamReader = null;
private Element element = null;
private Object root = null;
private String defaultLanguage = DEFAULT_LANGUAGE;
private String language = null;
private int nextID = 0;
private LinkedList<Attribute> namespaceBindingAttributes = new LinkedList<>();
private static HashMap<String, String> fileExtensions = new HashMap<>();
private static HashMap<String, Class<? extends Serializer<?>>> mimeTypes = new HashMap<>();
private static HashMap<String, ScriptEngine> scriptEngines = new HashMap<>();
private static HashMap<String, ScriptEngine> scriptEnginesExts = new HashMap<>();
public static final char URL_PREFIX = '@';
public static final char RESOURCE_KEY_PREFIX = '%';
public static final char OBJECT_REFERENCE_PREFIX = '$';
public static final char SLASH_PREFIX = '/';
public static final String NAMESPACE_BINDING_PREFIX = OBJECT_REFERENCE_PREFIX + "{";
public static final String NAMESPACE_BINDING_SUFFIX = "}";
public static final String BIND_MAPPING_DELIMITER = ":";
public static final String INTERNAL_ID_PREFIX = "$";
public static final String LANGUAGE_PROCESSING_INSTRUCTION = "language";
public static final String NASHORN_GLOBAL = "nashorn.global";
public static final String NASHORN_COMPAT_SCRIPT =
"if (typeof importClass != \"function\") { load(\"nashorn:mozilla_compat.js\"); }";
public static final String BXML_PREFIX = "bxml";
public static final String BXML_EXTENSION = "bxml";
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_MIME_TYPE_ATTRIBUTE = "mimeType";
public static final String INCLUDE_INLINE_ATTRIBUTE = "inline";
public static final String SCRIPT_TAG = "script";
public static final String SCRIPT_SRC_ATTRIBUTE = "src";
public static final String DEFINE_TAG = "define";
public static final String REFERENCE_TAG = "reference";
public static final String REFERENCE_ID_ATTRIBUTE = "id";
public static final String DEFAULT_LANGUAGE = "javascript";
public static final String MIME_TYPE = "application/bxml";
static {
mimeTypes.put(MIME_TYPE, BXMLSerializer.class);
mimeTypes.put(BinarySerializer.MIME_TYPE, BinarySerializer.class);
mimeTypes.put(ByteArraySerializer.MIME_TYPE, ByteArraySerializer.class);
mimeTypes.put(CSVSerializer.MIME_TYPE, CSVSerializer.class);
mimeTypes.put(JSONSerializer.MIME_TYPE, JSONSerializer.class);
mimeTypes.put(PropertiesSerializer.MIME_TYPE, PropertiesSerializer.class);
fileExtensions.put(BXML_EXTENSION, MIME_TYPE);
fileExtensions.put(CSVSerializer.CSV_EXTENSION, CSVSerializer.MIME_TYPE);
fileExtensions.put(JSONSerializer.JSON_EXTENSION, JSONSerializer.MIME_TYPE);
fileExtensions.put(PropertiesSerializer.PROPERTIES_EXTENSION, PropertiesSerializer.MIME_TYPE);
}
private ScriptEngine newEngineByName(final String scriptLanguage) throws SerializationException {
ScriptEngine engine = scriptEngineManager.getEngineByName(scriptLanguage);
if (engine == null) {
throw new SerializationException("Unable to find scripting engine for"
+ " language \"" + scriptLanguage + "\".");
}
// NOTE: this might not be right for Rhino engine, but works for Nashorn
engine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
if (engine.getFactory().getNames().contains("javascript")) {
try {
engine.eval(NASHORN_COMPAT_SCRIPT);
} catch (ScriptException se) {
throw new SerializationException("Unable to execute Nashorn compatibility script:",
se);
}
}
return engine;
}
/**
* Get a script engine instance for the given script language (typically "JavaScript").
* <p> Two things happen for a new script engine: set the global bindings to our
* {@link #namespace} so that any existing global definitions get set, and the
* {@link #NASHORN_COMPAT_SCRIPT} is run to ensure compatibility with the "Rhino"
* script engine (pre-Java-8).
* <p> Note: an engine found by this method will also be added to the {@link #scriptEnginesExts}
* map indexed by all its supported extensions.
*
* @param scriptLanguage Any script language name supported by the current JVM.
* @return Either an existing engine for that name, or a new one found by the
* {@link #scriptEngineManager} and then cached (in the {@link #scriptEngines} map).
* @throws SerializationException for problems finding the engine.
*/
private ScriptEngine getEngineByName(final String scriptLanguage) throws SerializationException {
String languageKey = scriptLanguage.toLowerCase();
ScriptEngine engine = scriptEngines.get(languageKey);
if (engine != null) {
return engine;
}
engine = newEngineByName(scriptLanguage);
scriptEngines.put(languageKey, engine);
// Also put this engine into the "extensions" map by the extension(s) it supports
for (String ext : engine.getFactory().getExtensions()) {
String extKey = ext.toLowerCase();
if (!scriptEnginesExts.containsKey(extKey)) {
scriptEnginesExts.put(extKey, engine);
}
}
return engine;
}
/**
* Get a script engine instance for the given (file) extension.
* <p> Two things happen for a new script engine: set the global bindings to our
* {@link #namespace} so that any existing global definitions get set, and the
* {@link #NASHORN_COMPAT_SCRIPT} is run to ensure compatibility with the "Rhino"
* script engine (pre-Java-8).
* <p> Note: an engine found by this method will also be added to the {@link #scriptEngines}
* map indexed by all its supported language names.
*
* @param extension Any script language extension supported by the current JVM.
* @return Either an existing engine for that extension, or a new one found by the
* {@link #scriptEngineManager} and then cached (in the {@link #scriptEnginesExts} map).
* @throws SerializationException for problems finding the engine.
*/
private ScriptEngine getEngineByExtension(final String extension) throws SerializationException {
String extensionKey = extension.toLowerCase();
ScriptEngine engine = scriptEnginesExts.get(extensionKey);
if (engine != null) {
return engine;
}
engine = scriptEngineManager.getEngineByExtension(extension);
if (engine == null) {
throw new SerializationException("Unable to find scripting engine for"
+ " extension " + extension + ".");
}
// NOTE: this might not be right for Rhino engine, but works for Nashorn
engine.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
if (engine.getFactory().getNames().contains("javascript")) {
try {
engine.eval(NASHORN_COMPAT_SCRIPT);
} catch (ScriptException se) {
throw new SerializationException("Unable to execute Nashorn compatibility script:",
se);
}
}
scriptEnginesExts.put(extensionKey, engine);
// Also put this engine into the "languages" map by the language(s) it supports
for (String language : engine.getFactory().getNames()) {
String languageKey = language.toLowerCase();
if (!scriptEngines.containsKey(languageKey)) {
scriptEngines.put(languageKey, engine);
}
}
return engine;
}
public BXMLSerializer() {
xmlInputFactory = XMLInputFactory.newInstance();
xmlInputFactory.setProperty("javax.xml.stream.isCoalescing", Boolean.TRUE);
scriptEngineManager = new ScriptEngineManager();
}
/**
* Deserializes an object hierarchy from a BXML resource. <p> This is the
* base version of the method. It does not set the "location" or "resources"
* properties. Callers that wish to use this version of the method to load
* BXML that uses location or resource resolution must manually set these
* properties via a call to {@link #setLocation(URL)} or
* {@link #setResources(Resources)}, respectively, before calling this
* method.
*
* @return The deserialized object hierarchy.
*/
@Override
public Object readObject(final InputStream inputStream) throws IOException, SerializationException {
Utils.checkNull(inputStream, "inputStream");
root = null;
language = null;
// Parse the XML stream
try {
try {
xmlStreamReader = xmlInputFactory.createXMLStreamReader(inputStream);
while (xmlStreamReader.hasNext()) {
int event = xmlStreamReader.next();
switch (event) {
case XMLStreamConstants.PROCESSING_INSTRUCTION:
processProcessingInstruction();
break;
case XMLStreamConstants.CHARACTERS:
processCharacters();
break;
case XMLStreamConstants.START_ELEMENT:
processStartElement();
break;
case XMLStreamConstants.END_ELEMENT:
processEndElement();
break;
default:
break;
}
}
} catch (XMLStreamException exception) {
throw new SerializationException(exception);
}
} catch (IOException | SerializationException | RuntimeException exception) {
logException(exception);
throw exception;
}
xmlStreamReader = null;
// Apply the namespace bindings
for (Attribute attribute : namespaceBindingAttributes) {
Element elementLocal = attribute.element;
String sourcePath = (String) attribute.value;
NamespaceBinding.BindMapping bindMapping;
int i = sourcePath.indexOf(BIND_MAPPING_DELIMITER);
if (i == -1) {
bindMapping = null;
} else {
String bindFunction = sourcePath.substring(0, i);
sourcePath = sourcePath.substring(i + 1);
bindMapping = new ScriptBindMapping(getEngineByName(language), bindFunction);
}
String targetPath;
NamespaceBinding namespaceBinding;
switch (elementLocal.type) {
case INSTANCE:
case INCLUDE:
// Bind to <element ID>.<attribute name>
if (elementLocal.id == null) {
elementLocal.id = INTERNAL_ID_PREFIX + Integer.toString(nextID++);
namespace.put(elementLocal.id, elementLocal.value);
}
targetPath = elementLocal.id + "." + attribute.name;
namespaceBinding = new NamespaceBinding(namespace, sourcePath, targetPath, bindMapping);
namespaceBinding.bind();
break;
case READ_ONLY_PROPERTY:
// Bind to <parent element ID>.<element name>.<attribute name>
if (elementLocal.parent.id == null) {
elementLocal.parent.id = INTERNAL_ID_PREFIX + Integer.toString(nextID++);
namespace.put(elementLocal.parent.id, elementLocal.parent.value);
}
targetPath = elementLocal.parent.id + "." + elementLocal.name + "." + attribute.name;
namespaceBinding = new NamespaceBinding(namespace, sourcePath, targetPath, bindMapping);
namespaceBinding.bind();
break;
default:
break;
}
}
namespaceBindingAttributes.clear();
// Bind the root to the namespace
if (root instanceof Bindable) {
Class<?> type = root.getClass();
while (Bindable.class.isAssignableFrom(type)) {
bind(root, type);
type = type.getSuperclass();
}
Bindable bindable = (Bindable) root;
bindable.initialize(namespace, location, resources);
}
return root;
}
/**
* Deserializes an object hierarchy from a BXML resource, and do not
* localize any text.
*
* @param baseType The base type from which to access needed resources.
* @param resourceName Name of the BXML resource to deserialize.
* @return the top-level deserialized object.
* @throws IllegalArgumentException for <tt>null</tt> type or resource name or if
* the resource could not be found.
* @throws IOException for any error reading the BXML resource.
* @throws SerializationException for any other errors encountered deserializing the resource.
* @see #readObject(Class, String, boolean)
*/
public final Object readObject(final Class<?> baseType, final String resourceName)
throws IOException, SerializationException {
return readObject(baseType, resourceName, false);
}
/**
* Deserializes an object hierarchy from a BXML resource. <p> The location
* of the resource is determined by a call to
* {@link Class#getResource(String)} on the given base type, passing the
* given resource name as an argument. If the resources is localized, the
* base type is also used as the base name of the resource bundle.
*
* @param baseType The base type.
* @param resourceName The name of the BXML resource.
* @param localize If <tt>true</tt>, the deserialized resource will be
* localized using the resource bundle specified by the base type.
* Otherwise, it will not be localized, and any use of the resource
* resolution operator will result in a serialization exception.
* @return the top-level deserialized object.
* @throws IllegalArgumentException for <tt>null</tt> type or resource name or if
* the resource could not be found.
* @throws IOException for any error reading the BXML resource.
* @throws SerializationException for any other errors encountered deserializing the resource.
* @see #readObject(URL, Resources)
*/
public final Object readObject(final Class<?> baseType, final String resourceName, final boolean localize)
throws IOException, SerializationException {
Utils.checkNull(baseType, "baseType");
Utils.checkNull(resourceName, "resourceName");
// throw a nice error so the user knows which resource did not load
URL locationLocal = baseType.getResource(resourceName);
if (locationLocal == null) {
throw new IllegalArgumentException("Could not find resource \"" + resourceName + "\".");
}
return readObject(locationLocal, localize ? new Resources(baseType.getName()) : null);
}
/**
* Deserializes an object hierarchy from a BXML resource. <p> This version
* of the method does not set the "resources" property. Callers that wish to
* use this version of the method to load BXML that uses resource resolution
* must manually set this property via a call to
* {@link #setResources(Resources)} before calling this method.
*
* @param locationArgument The location of the BXML resource.
* @return The top-level deserialized object.
* @throws IOException for any error reading the BXML resource.
* @throws SerializationException for any other errors encountered deserializing the resource.
* @see #readObject(URL, Resources)
*/
public final Object readObject(final URL locationArgument)
throws IOException, SerializationException {
return readObject(locationArgument, null);
}
/**
* Deserializes an object hierarchy from a BXML resource.
*
* @param locationArgument The location of the BXML resource.
* @param resourcesArgument The resources that will be used to localize the
* deserialized resource.
* @return The top-level deserialized object.
* @throws IOException for any error reading the BXML resource.
* @throws SerializationException for any other errors encountered deserializing the resource.
* @see #readObject(InputStream)
*/
public final Object readObject(final URL locationArgument, final Resources resourcesArgument)
throws IOException, SerializationException {
Utils.checkNull(locationArgument, "location");
this.location = locationArgument;
this.resources = resourcesArgument;
Object object;
try (InputStream inputStream = new BufferedInputStream(locationArgument.openStream())) {
object = readObject(inputStream);
}
this.location = null;
this.resources = null;
return object;
}
private void processProcessingInstruction() throws SerializationException {
String piTarget = xmlStreamReader.getPITarget();
String piData = xmlStreamReader.getPIData();
if (piTarget.equals(LANGUAGE_PROCESSING_INSTRUCTION)) {
if (language != null) {
throw new SerializationException("language already set.");
}
language = piData;
}
}
@SuppressWarnings("unchecked")
private void processCharacters() throws SerializationException {
if (!xmlStreamReader.isWhiteSpace()) {
// Process the text
String text = xmlStreamReader.getText();
switch (element.type) {
case INSTANCE:
if (element.value instanceof Sequence<?>) {
Sequence<Object> sequence = (Sequence<Object>) element.value;
try {
Method addMethod = sequence.getClass().getMethod("add", String.class);
addMethod.invoke(sequence, text);
} catch (NoSuchMethodException exception) {
throw new SerializationException("Text content cannot be added to "
+ sequence.getClass().getName() + ": \"" + text + "\"", exception);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
}
}
break;
case WRITABLE_PROPERTY:
case LISTENER_LIST_PROPERTY:
case SCRIPT:
element.value = text;
break;
default:
throw new SerializationException("Unexpected characters in " + element.type
+ " element.");
}
}
}
private void processStartElement() throws IOException, SerializationException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Initialize the page language
if (language == null) {
language = getDefaultLanguage();
}
// Get element properties
String namespaceURI = xmlStreamReader.getNamespaceURI();
String prefix = xmlStreamReader.getPrefix();
String localName = xmlStreamReader.getLocalName();
// Some stream readers incorrectly report an empty string as the prefix
// for the default namespace
if (prefix != null && prefix.length() == 0) {
prefix = null;
}
// Determine the type and value of this element
Element.Type elementType;
String name;
Class<?> propertyClass = null;
Object value = null;
if (prefix != null && prefix.equals(BXML_PREFIX)) {
// The element represents a BXML operation
if (element == null) {
throw new SerializationException("Invalid 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)) {
elementType = Element.Type.DEFINE;
} else if (localName.equals(REFERENCE_TAG)) {
elementType = Element.Type.REFERENCE;
} else {
throw new SerializationException("Invalid element.");
}
name = "<" + prefix + ":" + localName + ">";
} else {
if (Character.isUpperCase(localName.charAt(0))) {
int i = localName.indexOf('.');
if (i != -1 && Character.isLowerCase(localName.charAt(i + 1))) {
// The element represents an attached property
elementType = Element.Type.WRITABLE_PROPERTY;
name = localName.substring(i + 1);
String propertyClassName = namespaceURI + "." + localName.substring(0, i);
try {
propertyClass = Class.forName(propertyClassName, true, classLoader);
} catch (Throwable exception) {
throw new SerializationException(exception);
}
} else {
// The element represents a typed object
if (namespaceURI == null) {
throw new SerializationException("No XML namespace specified for "
+ localName + " tag.");
}
elementType = Element.Type.INSTANCE;
name = "<" + ((prefix == null) ? "" : prefix + ":") + localName + ">";
String className = namespaceURI + "." + localName.replace('.', '$');
try {
Class<?> type = Class.forName(className, true, classLoader);
value = newTypedObject(type);
} catch (Throwable exception) {
throw new SerializationException("Error creating a new '" + className + "' object", exception);
}
}
} else {
// The element represents a property
if (prefix != null) {
throw new SerializationException("Property elements cannot have a namespace prefix.");
}
if (element.value instanceof Dictionary<?, ?>) {
elementType = Element.Type.WRITABLE_PROPERTY;
} else {
BeanAdapter beanAdapter = new BeanAdapter(element.value);
if (beanAdapter.isReadOnly(localName)) {
Class<?> propertyType = beanAdapter.getType(localName);
if (propertyType == null) {
throw new SerializationException("\"" + localName
+ "\" is not a valid property of element " + element.name + ".");
}
if (ListenerList.class.isAssignableFrom(propertyType)) {
elementType = Element.Type.LISTENER_LIST_PROPERTY;
} else {
elementType = Element.Type.READ_ONLY_PROPERTY;
value = beanAdapter.get(localName);
assert (value != null) : "Read-only properties cannot be null.";
}
} else {
elementType = Element.Type.WRITABLE_PROPERTY;
}
}
name = localName;
}
}
// Create the element and process the attributes
element = new Element(element, elementType, name, propertyClass, value);
processAttributes();
if (elementType == Element.Type.INCLUDE) {
// Load the include
if (!element.properties.containsKey(INCLUDE_SRC_ATTRIBUTE)) {
throw new SerializationException(INCLUDE_SRC_ATTRIBUTE
+ " attribute is required for " + BXML_PREFIX + ":" + INCLUDE_TAG + " tag.");
}
String src = element.properties.get(INCLUDE_SRC_ATTRIBUTE);
if (src.charAt(0) == OBJECT_REFERENCE_PREFIX) {
src = src.substring(1);
if (src.length() > 0) {
if (!JSON.containsKey(namespace, src)) {
throw new SerializationException("Value \"" + src + "\" is not defined.");
}
String variableValue = JSON.get(namespace, src);
src = variableValue;
}
}
Resources resourcesLocal = this.resources;
if (element.properties.containsKey(INCLUDE_RESOURCES_ATTRIBUTE)) {
resourcesLocal = new Resources(resourcesLocal,
element.properties.get(INCLUDE_RESOURCES_ATTRIBUTE));
}
String mimeType = null;
if (element.properties.containsKey(INCLUDE_MIME_TYPE_ATTRIBUTE)) {
mimeType = element.properties.get(INCLUDE_MIME_TYPE_ATTRIBUTE);
}
if (mimeType == null) {
// Get the file extension
int i = src.lastIndexOf(".");
if (i != -1) {
String extension = src.substring(i + 1);
mimeType = fileExtensions.get(extension);
}
}
if (mimeType == null) {
throw new SerializationException("Cannot determine MIME type of include \"" + src + "\".");
}
boolean inline = false;
if (element.properties.containsKey(INCLUDE_INLINE_ATTRIBUTE)) {
inline = Boolean.parseBoolean(element.properties.get(INCLUDE_INLINE_ATTRIBUTE));
}
// Determine an appropriate serializer to use for the include
Class<? extends Serializer<?>> serializerClass = mimeTypes.get(mimeType);
if (serializerClass == null) {
throw new SerializationException("No serializer associated with MIME type " + mimeType + ".");
}
Serializer<?> serializer;
try {
serializer = newIncludeSerializer(serializerClass);
} catch (InstantiationException exception) {
throw new SerializationException(exception);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
}
// Determine location from src attribute
URL locationLocal;
if (src.charAt(0) == SLASH_PREFIX) {
locationLocal = classLoader.getResource(src.substring(1));
} else {
locationLocal = new URL(this.location, src);
}
// Set optional resolution properties
if (serializer instanceof Resolvable) {
Resolvable resolvable = (Resolvable) serializer;
if (inline) {
resolvable.setNamespace(namespace);
}
resolvable.setLocation(locationLocal);
resolvable.setResources(resourcesLocal);
}
// Read the object
try (InputStream inputStream = new BufferedInputStream(locationLocal.openStream())) {
element.value = serializer.readObject(inputStream);
}
} else if (element.type == Element.Type.REFERENCE) {
// Dereference the value
if (!element.properties.containsKey(REFERENCE_ID_ATTRIBUTE)) {
throw new SerializationException(REFERENCE_ID_ATTRIBUTE
+ " attribute is required for " + BXML_PREFIX + ":" + REFERENCE_TAG + " tag.");
}
String id = element.properties.get(REFERENCE_ID_ATTRIBUTE);
if (!namespace.containsKey(id)) {
throw new SerializationException("A value with ID \"" + id + "\" does not exist.");
}
element.value = namespace.get(id);
}
// If the element has an ID, add the value to the namespace
if (element.id != null) {
namespace.put(element.id, element.value);
// If the type has an ID property, use it
Class<?> type = element.value.getClass();
IDProperty idProperty = type.getAnnotation(IDProperty.class);
if (idProperty != null) {
BeanAdapter beanAdapter = new BeanAdapter(element.value);
beanAdapter.put(idProperty.value(), element.id);
}
}
}
private void processAttributes() throws SerializationException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
for (int i = 0, n = xmlStreamReader.getAttributeCount(); i < n; i++) {
String prefix = xmlStreamReader.getAttributePrefix(i);
String localName = xmlStreamReader.getAttributeLocalName(i);
String value = xmlStreamReader.getAttributeValue(i);
if (prefix != null && prefix.equals(BXML_PREFIX)) {
// The attribute represents an internal value
if (localName.equals(ID_ATTRIBUTE)) {
if (value.length() == 0 || value.contains(".")) {
throw new IllegalArgumentException("\"" + value + "\" is not a valid ID value.");
}
if (namespace.containsKey(value)) {
throw new SerializationException("ID " + value + " is already in use.");
}
if (element.type != Element.Type.INSTANCE && element.type != Element.Type.INCLUDE) {
throw new SerializationException("An ID cannot be assigned to this element.");
}
element.id = value;
} else {
throw new SerializationException(BXML_PREFIX + ":" + localName
+ " is not a valid attribute.");
}
} else {
boolean property = false;
switch (element.type) {
case INCLUDE:
property = (localName.equals(INCLUDE_SRC_ATTRIBUTE)
|| localName.equals(INCLUDE_RESOURCES_ATTRIBUTE)
|| localName.equals(INCLUDE_MIME_TYPE_ATTRIBUTE)
|| localName.equals(INCLUDE_INLINE_ATTRIBUTE));
break;
case SCRIPT:
property = (localName.equals(SCRIPT_SRC_ATTRIBUTE));
break;
case REFERENCE:
property = (localName.equals(REFERENCE_ID_ATTRIBUTE));
break;
default:
break;
}
if (property) {
element.properties.put(localName, value);
} else {
String name;
Class<?> propertyClass = null;
if (Character.isUpperCase(localName.charAt(0))) {
// The attribute represents a static property or listener list
int j = localName.indexOf('.');
name = localName.substring(j + 1);
String namespaceURI = xmlStreamReader.getAttributeNamespace(i);
if (Utils.isNullOrEmpty(namespaceURI)) {
namespaceURI = xmlStreamReader.getNamespaceURI("");
}
String propertyClassName = namespaceURI + "." + localName.substring(0, j);
try {
propertyClass = Class.forName(propertyClassName, true, classLoader);
} catch (Throwable exception) {
throw new SerializationException(exception);
}
} else {
// The attribute represents an instance property
name = localName;
}
if (value.startsWith(NAMESPACE_BINDING_PREFIX) && value.endsWith(NAMESPACE_BINDING_SUFFIX)) {
// The attribute represents a namespace binding
if (propertyClass != null) {
throw new SerializationException(
"Namespace binding is not supported for static properties.");
}
namespaceBindingAttributes.add(new Attribute(element, name, propertyClass,
value.substring(2, value.length() - 1)));
} else {
// Resolve the attribute value
Attribute attribute = new Attribute(element, name, propertyClass, value);
if (value.length() > 0) {
if (value.charAt(0) == URL_PREFIX) {
value = value.substring(1);
if (value.length() > 0) {
if (value.charAt(0) == URL_PREFIX) {
attribute.value = value;
} else {
if (location == null) {
throw new IllegalStateException("Base location is undefined.");
}
try {
attribute.value = new URL(location, value);
} catch (MalformedURLException exception) {
throw new SerializationException(exception);
}
}
} else {
throw new SerializationException(
"Invalid URL resolution argument.");
}
} else if (value.charAt(0) == RESOURCE_KEY_PREFIX) {
value = value.substring(1);
if (value.length() > 0) {
if (value.charAt(0) == RESOURCE_KEY_PREFIX) {
attribute.value = value;
} else {
if (resources != null && JSON.containsKey(resources, value)) {
attribute.value = JSON.get(resources, value);
} else {
attribute.value = value;
}
}
} else {
throw new SerializationException("Invalid resource resolution argument.");
}
} else if (value.charAt(0) == OBJECT_REFERENCE_PREFIX) {
value = value.substring(1);
if (value.length() > 0) {
if (value.charAt(0) == OBJECT_REFERENCE_PREFIX) {
attribute.value = value;
} else {
if (value.equals(BXML_PREFIX + ":" + null)) {
attribute.value = null;
} else {
if (JSON.containsKey(namespace, value)) {
attribute.value = JSON.get(namespace, value);
} else {
Object nashornGlobal =
scriptEngineManager.getBindings().get(NASHORN_GLOBAL);
if (nashornGlobal == null) {
throw new SerializationException("Value \"" + value
+ "\" is not defined.");
} else {
if (nashornGlobal instanceof Bindings) {
Bindings bindings = (Bindings) nashornGlobal;
if (bindings.containsKey(value)) {
attribute.value = bindings.get(value);
} else {
throw new SerializationException("Value \"" + value
+ "\" is not defined.");
}
} else {
throw new SerializationException("Value \"" + value
+ "\" is not defined.");
}
}
}
}
}
} else {
throw new SerializationException("Invalid object resolution argument.");
}
}
}
element.attributes.add(attribute);
}
}
}
}
}
@SuppressWarnings("unchecked")
private void processEndElement() throws SerializationException {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Dictionary<String, Object> dictionary;
String script;
ScriptEngine scriptEngine;
switch (element.type) {
case INSTANCE:
case INCLUDE:
case REFERENCE:
// Apply attributes
for (Attribute attribute : element.attributes) {
if (attribute.propertyClass == null) {
if (element.value instanceof Dictionary<?, ?>) {
dictionary = (Dictionary<String, Object>) element.value;
} else {
dictionary = new BeanAdapter(element.value);
}
dictionary.put(attribute.name, attribute.value);
} else {
if (attribute.propertyClass.isInterface()) {
// The attribute represents an event listener
String listenerClassName = attribute.propertyClass.getName();
listenerClassName = listenerClassName.substring(listenerClassName.lastIndexOf('.') + 1);
String getListenerListMethodName = "get"
+ Character.toUpperCase(listenerClassName.charAt(0))
+ listenerClassName.substring(1) + "s";
// Get the listener list
Method getListenerListMethod;
try {
Class<?> type = element.value.getClass();
getListenerListMethod = type.getMethod(getListenerListMethodName);
} catch (NoSuchMethodException exception) {
throw new SerializationException(exception);
}
Object listenerList;
try {
listenerList = getListenerListMethod.invoke(element.value);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
}
// Create an invocation handler for this listener
AttributeInvocationHandler handler = new AttributeInvocationHandler(
getEngineByName(language), attribute.name, (String) attribute.value);
Object listener = Proxy.newProxyInstance(classLoader,
new Class<?>[] {attribute.propertyClass}, handler);
// Add the listener
Class<?> listenerListClass = listenerList.getClass();
Method addMethod;
try {
addMethod = listenerListClass.getMethod("add", Object.class);
} catch (NoSuchMethodException exception) {
throw new RuntimeException(exception);
}
try {
addMethod.invoke(listenerList, listener);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
}
} else {
// The attribute represents a static setter
setStaticProperty(element.value, attribute.propertyClass,
attribute.name, attribute.value);
}
}
}
if (element.parent != null) {
if (element.parent.type == Element.Type.WRITABLE_PROPERTY) {
// Set this as the property value; it will be applied
// later in the parent's closing tag
element.parent.value = element.value;
} else if (element.parent.value != null) {
// If the parent element has a default property, use it;
// otherwise, if the parent is a sequence, add the element to it.
Class<?> parentType = element.parent.value.getClass();
DefaultProperty defaultProperty = parentType.getAnnotation(DefaultProperty.class);
if (defaultProperty == null) {
if (element.parent.value instanceof Sequence<?>) {
Sequence<Object> sequence = (Sequence<Object>) element.parent.value;
sequence.add(element.value);
} else {
throw new SerializationException(element.parent.value.getClass()
+ " is not a sequence.");
}
} else {
String defaultPropertyName = defaultProperty.value();
BeanAdapter beanAdapter = new BeanAdapter(element.parent.value);
Object defaultPropertyValue = beanAdapter.get(defaultPropertyName);
if (defaultPropertyValue instanceof Sequence<?>) {
Sequence<Object> sequence = (Sequence<Object>) defaultPropertyValue;
try {
sequence.add(element.value);
} catch (UnsupportedOperationException uoe) {
beanAdapter.put(defaultPropertyName, element.value);
}
} else {
beanAdapter.put(defaultPropertyName, element.value);
}
}
}
}
break;
case READ_ONLY_PROPERTY:
if (element.value instanceof Dictionary<?, ?>) {
dictionary = (Dictionary<String, Object>) element.value;
} else {
dictionary = new BeanAdapter(element.value);
}
// Process attributes looking for instance property setters
for (Attribute attribute : element.attributes) {
if (attribute.propertyClass != null) {
throw new SerializationException("Static setters are not supported"
+ " for read-only properties.");
}
dictionary.put(attribute.name, attribute.value);
}
break;
case WRITABLE_PROPERTY:
if (element.propertyClass == null) {
if (element.parent.value instanceof Dictionary) {
dictionary = (Dictionary<String, Object>) element.parent.value;
} else {
dictionary = new BeanAdapter(element.parent.value);
}
dictionary.put(element.name, element.value);
} else {
if (element.parent == null) {
throw new SerializationException("Element does not have a parent.");
}
if (element.parent.value == null) {
throw new SerializationException("Parent value is null.");
}
setStaticProperty(element.parent.value, element.propertyClass, element.name,
element.value);
}
break;
case LISTENER_LIST_PROPERTY:
// Evaluate the script
script = (String) element.value;
// Get a new engine here in order to make the script function private to this object
scriptEngine = newEngineByName(language);
// ORIGINAL COMMENT: Don't pollute the engine namespace with the listener functions
// Removed for Java 1.8+ because Nashorn handles globals differently
//scriptEngine.setBindings(new SimpleBindings(), ScriptContext.ENGINE_SCOPE);
try {
scriptEngine.eval(script);
} catch (ScriptException exception) {
reportException(exception, script);
break;
}
// Create the listener and add it to the list
BeanAdapter beanAdapter = new BeanAdapter(element.parent.value);
ListenerList<?> listenerList = (ListenerList<?>) beanAdapter.get(element.name);
Class<?> listenerListClass = listenerList.getClass();
java.lang.reflect.Type[] genericInterfaces = listenerListClass.getGenericInterfaces();
Class<?> listenerClass = (Class<?>) genericInterfaces[0];
ElementInvocationHandler handler = new ElementInvocationHandler(scriptEngine);
Method addMethod;
try {
addMethod = listenerListClass.getMethod("add", Object.class);
} catch (NoSuchMethodException exception) {
throw new RuntimeException(exception);
}
Object listener = Proxy.newProxyInstance(classLoader,
new Class<?>[] {listenerClass}, handler);
try {
addMethod.invoke(listenerList, listener);
} catch (IllegalAccessException exception) {
throw new SerializationException(exception);
} catch (InvocationTargetException exception) {
throw new SerializationException(exception);
}
break;
case SCRIPT:
String src = null;
if (element.properties.containsKey(SCRIPT_SRC_ATTRIBUTE)) {
src = element.properties.get(SCRIPT_SRC_ATTRIBUTE);
}
if (src != null && src.charAt(0) == OBJECT_REFERENCE_PREFIX) {
src = src.substring(1);
if (src.length() > 0) {
if (!JSON.containsKey(namespace, src)) {
throw new SerializationException("Value \"" + src + "\" is not defined.");
}
String variableValue = JSON.get(namespace, src);
src = variableValue;
}
}
if (src != null) {
int i = src.lastIndexOf(".");
if (i == -1) {
throw new SerializationException("Cannot determine type of script \"" + src + "\".");
}
String extension = src.substring(i + 1);
scriptEngine = getEngineByExtension(extension);
scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE);
try {
URL scriptLocation;
if (src.charAt(0) == SLASH_PREFIX) {
scriptLocation = classLoader.getResource(src.substring(1));
if (scriptLocation == null) { // add a fallback
scriptLocation = new URL(location, src.substring(1));
}
} else {
scriptLocation = new URL(location, src);
}
BufferedReader scriptReader = null;
try {
scriptReader = new BufferedReader(new InputStreamReader(
scriptLocation.openStream()));
scriptEngine.eval(NASHORN_COMPAT_SCRIPT);
scriptEngine.eval(scriptReader);
} catch (ScriptException exception) {
reportException(exception);
} finally {
if (scriptReader != null) {
scriptReader.close();
}
}
} catch (IOException exception) {
throw new SerializationException(exception);
}
}
if (element.value != null) {
// Evaluate the script
script = (String) element.value;
scriptEngine = getEngineByName(language);
scriptEngine.setBindings(scriptEngineManager.getBindings(), ScriptContext.ENGINE_SCOPE);
try {
scriptEngine.eval(NASHORN_COMPAT_SCRIPT);
scriptEngine.eval(script);
} catch (ScriptException exception) {
reportException(exception, script);
}
}
break;
case DEFINE:
// No-op
break;
default:
break;
}
// Move up the stack
if (element.parent == null) {
root = element.value;
}
element = element.parent;
}
/**
* Return the current location of the XML parser. Useful to ascertain the
* location where an error occurred (if the error was not an
* XMLStreamException, which has its own
* {@link XMLStreamException#getLocation} method).
* @return The current location in the XML stream.
*/
public Location getCurrentLocation() {
return xmlStreamReader.getLocation();
}
private void logException(final Throwable exception) {
Location streamReaderlocation = xmlStreamReader.getLocation();
String message = "An error occurred at line number " + streamReaderlocation.getLineNumber();
if (location != null) {
message += " in file " + location.getPath();
}
message += ":";
reportException(new SerializationException(message, exception));
}
private void reportException(final ScriptException exception, final String script) {
reportException(new SerializationException("Failed to execute script:\n" + script, exception));
}
/**
* Hook used for standardized reporting of exceptions during this process.
* <p>Subclasses should override this method in order to do something besides
* print to <tt>System.err</tt>.
* @param exception Whatever exception has been thrown during processing.
*/
protected void reportException(final Throwable exception) {
String message = exception.getLocalizedMessage();
if (Utils.isNullOrEmpty(message)) {
message = exception.getClass().getSimpleName();
}
System.err.println("Exception: " + message);
exception.printStackTrace(System.err);
}
@Override
public void writeObject(final Object object, final OutputStream outputStream) throws IOException,
SerializationException {
throw new UnsupportedOperationException();
}
@Override
public String getMIMEType(final Object object) {
return MIME_TYPE;
}
/**
* 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;
}
@Override
public Map<String, Object> getNamespace() {
return namespace;
}
@Override
public void setNamespace(final Map<String, Object> namespace) {
Utils.checkNull(namespace, "namespace");
this.namespace = namespace;
}
@Override
public URL getLocation() {
return location;
}
@Override
public void setLocation(final URL location) {
this.location = location;
}
@Override
public Resources getResources() {
return resources;
}
@Override
public void setResources(final Resources resources) {
this.resources = resources;
}
/**
* Applies BXML binding annotations to an object.
*
* @param object The object to bind BXML values to.
* @throws BindException If an error occurs during binding.
* @see #bind(Object, Class)
*/
public void bind(final Object object) {
Utils.checkNull(object, "bind object");
bind(object, object.getClass());
}
/**
* Applies BXML 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 object The object to bind BXML values to.
* @param type The type of the object.
* @throws BindException If an error occurs during binding.
*/
public void bind(final Object object, final Class<?> type) throws BindException {
Utils.checkNull(object, "bind object");
Utils.checkNull(type, "bind type");
if (!type.isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Bind object is not assignable to class " + type.getName() + ".");
}
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();
BXML bindingAnnotation = field.getAnnotation(BXML.class);
if (bindingAnnotation != 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 = bindingAnnotation.id();
if (id.equals("\0")) {
id = field.getName();
}
if (namespace.containsKey(id)) {
// Set the value into the field
Object value = namespace.get(id);
try {
field.set(object, value);
} catch (IllegalAccessException exception) {
throw new BindException(exception);
}
}
}
}
}
/**
* Creates a new serializer to be used on a nested include. The base
* implementation simply calls {@code Class.newInstance()}. Subclasses may
* override this method to provide an alternate instantiation mechanism,
* such as dependency-injected construction.
*
* @param type The type of serializer being requested.
* @return The new serializer to use.
* @throws InstantiationException if an object of the given type cannot be instantiated.
* @throws IllegalAccessException if the class cannot be accessed in the
* current security environment.
*/
protected Serializer<?> newIncludeSerializer(final Class<? extends Serializer<?>> type)
throws InstantiationException, IllegalAccessException {
return type.newInstance();
}
/**
* Creates a new typed object as part of the deserialization process. The
* base implementation simply calls {@code Class.newInstance()}. Subclasses
* may override this method to provide an alternate instantiation mechanism,
* such as dependency-injected construction.
*
* @param type The type of object being requested.
* @return The newly created object.
* @throws InstantiationException if an object of the given type cannot be instantiated.
* @throws IllegalAccessException if the class cannot be accessed in the
* current security environment.
*/
protected Object newTypedObject(final Class<?> type)
throws InstantiationException, IllegalAccessException {
return type.newInstance();
}
/**
* Gets a read-only version of the XML stream reader that's being used by
* this serializer. Subclasses can use this to access information about the
* current event.
* @return The read-only reader.
*/
protected final XMLStreamReader getXMLStreamReader() {
return new StreamReaderDelegate(xmlStreamReader) {
@Override
public void close() {
throw new UnsupportedOperationException();
}
@Override
public int next() {
throw new UnsupportedOperationException();
}
@Override
public int nextTag() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns the file extension/MIME type map. This map associates file
* extensions with MIME types, which are used to automatically determine an
* appropriate serializer to use for an include based on file extension.
*
* @return The map between file extensions and MIME types.
* @see #getMimeTypes()
*/
public static Map<String, String> getFileExtensions() {
return fileExtensions;
}
/**
* Returns the MIME type/serializer class map. This map associates MIME
* types with serializer classes. The serializer for a given MIME type will
* be used to deserialize the data for an include that references the MIME
* type.
* @return The map associating MIME types with serializers.
*/
public static Map<String, Class<? extends Serializer<?>>> getMimeTypes() {
return mimeTypes;
}
private static Method getStaticGetterMethod(final Class<?> propertyClass, final String propertyName,
final Class<?> objectType) {
Method method = null;
if (objectType != null) {
try {
method = propertyClass.getMethod(BeanAdapter.GET_PREFIX + propertyName, objectType);
} catch (NoSuchMethodException exception) {
// No-op
}
if (method == null) {
try {
method = propertyClass.getMethod(BeanAdapter.IS_PREFIX + propertyName, objectType);
} catch (NoSuchMethodException exception) {
// No-op
}
}
if (method == null) {
method = getStaticGetterMethod(propertyClass, propertyName,
objectType.getSuperclass());
}
}
return method;
}
private static Method getStaticSetterMethod(final Class<?> propertyClass, final String propertyName,
final Class<?> objectType, final Class<?> propertyValueType) {
Method method = null;
if (objectType != null) {
final String methodName = BeanAdapter.SET_PREFIX + propertyName;
try {
method = propertyClass.getMethod(methodName, 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, 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;
}
private static void setStaticProperty(final Object object, final Class<?> propertyClass,
final String propertyName, final Object value) throws SerializationException {
Class<?> objectType = object.getClass();
String propertyNameUpdated = Character.toUpperCase(propertyName.charAt(0))
+ propertyName.substring(1);
Object valueToAssign = value;
Method setterMethod = null;
if (valueToAssign != null) {
setterMethod = getStaticSetterMethod(propertyClass, propertyNameUpdated, objectType,
valueToAssign.getClass());
}
if (setterMethod == null) {
Method getterMethod = getStaticGetterMethod(propertyClass, propertyNameUpdated, objectType);
if (getterMethod != null) {
Class<?> propertyType = getterMethod.getReturnType();
setterMethod = getStaticSetterMethod(propertyClass, propertyNameUpdated, objectType, propertyType);
if (valueToAssign instanceof String) {
valueToAssign = BeanAdapter.coerce((String) valueToAssign, propertyType, propertyNameUpdated);
}
}
}
if (setterMethod == null) {
throw new SerializationException(propertyClass.getName() + "." + propertyNameUpdated
+ " is not valid static property.");
}
// Invoke the setter
try {
setterMethod.invoke(null, object, valueToAssign);
} catch (Exception exception) {
throw new SerializationException(exception);
}
}
/**
* Set the default script language to use for all scripts.
*
* @param defaultLanguage Name of the new default script language,
* or {@code null} to set the default, default value.
* @see #DEFAULT_LANGUAGE
*/
protected void setDefaultLanguage(final String defaultLanguage) {
if (defaultLanguage == null) {
this.defaultLanguage = DEFAULT_LANGUAGE;
} else {
this.defaultLanguage = defaultLanguage;
}
}
protected String getDefaultLanguage() {
return this.defaultLanguage;
}
}