/*
* $Id$
*
* 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.struts2.views.xslt;
import org.apache.struts2.StrutsException;
import org.w3c.dom.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* AdapterFactory produces Node adapters for Java object types.
* Adapter classes are generally instantiated dynamically via a no-args constructor
* and populated with their context information via the AdapterNode interface.
* </p>
*
* <p>
* This factory supports proxying of generic DOM Node trees, allowing arbitrary
* Node types to be mixed together. You may simply return a Document or Node
* type as an object property and it will appear as a sub-tree in the XML as
* you'd expect. See #proxyNode().
* </p>
*
* <p>
* Customization of the result XML can be accomplished by providing
* alternate adapters for Java types. Adapters are associated with Java
* types through the registerAdapterType() method.
* </p>
*
* <p>
* For example, since there is no default Date adapter, Date objects will be
* rendered with the generic Bean introspecting adapter, producing output
* like:
* </p>
*
* <pre>
* <date>
* <date>19</date>
* <day>1</day>
* <hours>0</hours>
* <minutes>7</minutes>
* <month>8</month>
* <seconds>4</seconds>
* <time>1127106424531</time>
* <timezoneOffset>300</timezoneOffset>
* <year>105</year>
* </date>
* </pre>
*
* <p>
* By extending the StringAdapter and overriding its normal behavior we can
* create a custom Date formatter:
* </p>
*
* <pre>
* public static class CustomDateAdapter extends StringAdapter {
* protected String getStringValue() {
* Date date = (Date)getPropertyValue();
* return DateFormat.getTimeInstance( DateFormat.FULL ).format( date );
* }
* }
* </pre>
*
* <p>
* Producing output like:
* </p>
*
* <pre>
* <date>12:02:54 AM CDT</date>
* </pre>
*
* <p>
* The StringAdapter (which is normally invoked only to adapt String values)
* is a useful base for these kinds of customizations and can produce
* structured XML output as well as plain text by setting its parseStringAsXML()
* property to true.
* </p>
*
* <p>
* See provided examples.
* </p>
*/
public class AdapterFactory {
private Map<Class, Class> adapterTypes = new HashMap<>();
/**
* Register an adapter type for a Java class type.
*
* @param type the Java class type which is to be handled by the adapter.
* @param adapterType The adapter class, which implements AdapterNode.
*/
public void registerAdapterType(Class type, Class adapterType) {
adapterTypes.put(type, adapterType);
}
/**
* Create a top level Document adapter for the specified Java object.
* The document will have a root element with the specified property name
* and contain the specified Java object content.
*
* @param propertyName the name of the root document element
* @param propertyValue the property value
*
* @return the document object
*
* @throws IllegalAccessException in case of illegal access
* @throws InstantiationException in case of instantiation errors
*/
public Document adaptDocument(String propertyName, Object propertyValue)
throws IllegalAccessException, InstantiationException {
return new SimpleAdapterDocument(this, null, propertyName, propertyValue);
}
/**
* Create an Node adapter for a child element.
* Note that the parent of the created node must be an AdapterNode, however
* the child node itself may be any type of Node.
*
* @see #adaptDocument( String, Object )
*
* @param parent the parent adapter node
* @param propertyName the name of th property
* @param value the value
*
* @return a node
*/
public Node adaptNode(AdapterNode parent, String propertyName, Object value) {
Class adapterClass = getAdapterForValue(value);
if (adapterClass != null) {
return constructAdapterInstance(adapterClass, parent, propertyName, value);
}
// If the property is a Document, "unwrap" it to the root element
if (value instanceof Document) {
value = ((Document) value).getDocumentElement();
}
// If the property is already a Node, proxy it
if (value instanceof Node) {
return proxyNode(parent, (Node) value);
}
// Check other supported types or default to generic JavaBean introspecting adapter
Class valueType = value.getClass();
if (valueType.isArray()) {
adapterClass = ArrayAdapter.class;
} else if (value instanceof String || value instanceof Number || value instanceof Boolean || valueType.isPrimitive()) {
adapterClass = StringAdapter.class;
} else if (value instanceof Collection) {
adapterClass = CollectionAdapter.class;
} else if (value instanceof Map) {
adapterClass = MapAdapter.class;
} else {
adapterClass = BeanAdapter.class;
}
return constructAdapterInstance(adapterClass, parent, propertyName, value);
}
/**
* <p>
* Construct a proxy adapter for a value that is an existing DOM Node.
* This allows arbitrary DOM Node trees to be mixed in with our results.
* The proxied nodes are read-only and currently support only
* limited types of Nodes including Element, Text, and Attributes. (Other
* Node types may be ignored by the proxy and not appear in the result tree).
* </p>
*
* <p>
* // TODO:
* NameSpaces are not yet supported.
* </p>
*
* <p>
* This method is primarily for use by the adapter node classes.
* </p>
*
* @param parent parent adapter node
* @param node node
*
* @return proxy node
*/
public Node proxyNode(AdapterNode parent, Node node) {
// If the property is a Document, "unwrap" it to the root element
if (node instanceof Document) {
node = ((Document) node).getDocumentElement();
}
if (node == null) {
return null;
}
if (node.getNodeType() == Node.ELEMENT_NODE) {
return new ProxyElementAdapter(this, parent, (Element) node);
}
if (node.getNodeType() == Node.TEXT_NODE) {
return new ProxyTextNodeAdapter(this, parent, (Text) node);
}
if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
return new ProxyAttrAdapter(this, parent, (Attr) node);
}
return null; // Unsupported Node type - ignore for now
}
public NamedNodeMap proxyNamedNodeMap(AdapterNode parent, NamedNodeMap nnm) {
return new ProxyNamedNodeMap(this, parent, nnm);
}
/**
* Create an instance of an adapter dynamically and set its context via
* the AdapterNode interface.
*
* @param adapterClass adapter class
* @param parent parent adapter node
* @param propertyName the property name
* @param propertyValue the property value
*
* @return the new node
*/
private Node constructAdapterInstance(Class adapterClass, AdapterNode parent, String propertyName, Object propertyValue) {
// Check to see if the class has a no-args constructor
try {
adapterClass.getConstructor(new Class []{});
} catch (NoSuchMethodException e1) {
throw new StrutsException("Adapter class: " + adapterClass + " does not have a no-args constructor.");
}
try {
AdapterNode adapterNode = (AdapterNode) adapterClass.newInstance();
adapterNode.setAdapterFactory(this);
adapterNode.setParent(parent);
adapterNode.setPropertyName(propertyName);
adapterNode.setPropertyValue(propertyValue);
return adapterNode;
} catch (IllegalAccessException | InstantiationException e) {
throw new StrutsException("Cannot adapt " + propertyValue + " (" + propertyName + ") :" + e.getMessage(), e);
}
}
/**
* Create an appropriate adapter for a null value.
*
* @param parent parent adapter node
* @param propertyName the property name
*
* @return the new node
*/
public Node adaptNullValue(AdapterNode parent, String propertyName) {
return new StringAdapter(this, parent, propertyName, "null");
}
//TODO: implement Configuration option to provide additional adapter classes
public Class getAdapterForValue(Object value) {
return adapterTypes.get(value.getClass());
}
}