View Javadoc

1   /*
2    * $Id: AdapterFactory.java 440597 2006-09-06 03:34:39Z wsmoak $
3    *
4    * Copyright 2006 The Apache Software Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.struts2.views.xslt;
19  
20  import java.util.Collection;
21  import java.util.HashMap;
22  import java.util.Map;
23  
24  import org.apache.struts2.StrutsException;
25  import org.w3c.dom.Attr;
26  import org.w3c.dom.Document;
27  import org.w3c.dom.Element;
28  import org.w3c.dom.NamedNodeMap;
29  import org.w3c.dom.Node;
30  import org.w3c.dom.Text;
31  
32  /***
33   * AdapterFactory produces Node adapters for Java object types.
34   * Adapter classes are generally instantiated dynamically via a no-args constructor
35   * and populated with their context information via the AdapterNode interface.
36   *
37   * This factory supports proxying of generic DOM Node trees, allowing arbitrary
38   * Node types to be mixed together.  You may simply return a Document or Node
39   * type as an object property and it will appear as a sub-tree in the XML as
40   * you'd expect. See #proxyNode().
41   *
42   * Customization of the result XML can be accomplished by providing
43   * alternate adapters for Java types.  Adapters are associated with Java
44   * types through the registerAdapterType() method.
45   *
46   * For example, since there is no default Date adapter, Date objects will be
47   * rendered with the generic Bean introspecting adapter, producing output
48   * like:
49   * <pre>
50       <date>
51          <date>19</date>
52          <day>1</day>
53          <hours>0</hours>
54          <minutes>7</minutes>
55          <month>8</month>
56          <seconds>4</seconds>
57          <time>1127106424531</time>
58          <timezoneOffset>300</timezoneOffset>
59          <year>105</year>
60      </date>
61   * </pre>
62   *
63   * By extending the StringAdapter and overriding its normal behavior we can
64   * create a custom Date formatter:
65   *
66   * <pre>
67        public static class CustomDateAdapter extends StringAdapter {
68          protected String getStringValue() {
69              Date date = (Date)getPropertyValue();
70              return DateFormat.getTimeInstance( DateFormat.FULL ).format( date );
71          }
72      }
73   * </pre>
74   *
75   * Producing output like:
76   *
77  <pre>
78       <date>12:02:54 AM CDT</date>
79   </pre>
80   *
81   * The StringAdapter (which is normally invoked only to adapt String values)
82   * is a useful base for these kinds of customizations and can produce
83   * structured XML output as well as plain text by setting its parseStringAsXML()
84   * property to true.
85   *
86   * See provided examples.
87   */
88  public class AdapterFactory {
89  
90      /***
91       * Map<Class, Class<AdapterNode>>
92       */
93      private Map<Class, Class> adapterTypes = new HashMap<Class, Class>();
94  
95      /***
96       * Register an adapter type for a Java class type.
97       *
98       * @param type        the Java class type which is to be handled by the adapter.
99       * @param adapterType The adapter class, which implements AdapterNode.
100      */
101     public void registerAdapterType(Class type, Class adapterType) {
102         adapterTypes.put(type, adapterType);
103     }
104 
105     /***
106      * Create a top level Document adapter for the specified Java object.
107      * The document will have a root element with the specified property name
108      * and contain the specified Java object content.
109      *
110      * @param propertyName The name of the root document element
111      * @return
112      * @throws IllegalAccessException
113      * @throws InstantiationException
114      */
115     public Document adaptDocument(String propertyName, Object propertyValue)
116             throws IllegalAccessException, InstantiationException {
117         //if ( propertyValue instanceof Document )
118         //	return (Document)propertyValue;
119 
120         return new SimpleAdapterDocument(this, null, propertyName, propertyValue);
121     }
122 
123 
124     /***
125      * Create an Node adapter for a child element.
126      * Note that the parent of the created node must be an AdapterNode, however
127      * the child node itself may be any type of Node.
128      *
129      * @see #adaptDocument( String, Object )
130      */
131     public Node adaptNode(AdapterNode parent, String propertyName, Object value) {
132         Class adapterClass = getAdapterForValue(value);
133         if (adapterClass != null)
134             return constructAdapterInstance(adapterClass, parent, propertyName, value);
135 
136         // If the property is a Document, "unwrap" it to the root element
137         if (value instanceof Document)
138             value = ((Document) value).getDocumentElement();
139 
140         // If the property is already a Node, proxy it
141         if (value instanceof Node)
142             return proxyNode(parent, (Node) value);
143 
144         // Check other supported types or default to generic JavaBean introspecting adapter
145         Class valueType = value.getClass();
146 
147         if (valueType.isArray())
148             adapterClass = ArrayAdapter.class;
149         else if (value instanceof String || value instanceof Number || valueType.isPrimitive())
150             adapterClass = StringAdapter.class;
151         else if (value instanceof Collection)
152             adapterClass = CollectionAdapter.class;
153         else if (value instanceof Map)
154             adapterClass = MapAdapter.class;
155         else
156             adapterClass = BeanAdapter.class;
157 
158         return constructAdapterInstance(adapterClass, parent, propertyName, value);
159     }
160 
161     /***
162      * Construct a proxy adapter for a value that is an existing DOM Node.
163      * This allows arbitrary DOM Node trees to be mixed in with our results.
164      * The proxied nodes are read-only and currently support only
165      * limited types of Nodes including Element, Text, and Attributes.  (Other
166      * Node types may be ignored by the proxy and not appear in the result tree).
167      * <p/>
168      * // TODO:
169      * NameSpaces are not yet supported.
170      * <p/>
171      * This method is primarily for use by the adapter node classes.
172      */
173     public Node proxyNode(AdapterNode parent, Node node) {
174         // If the property is a Document, "unwrap" it to the root element
175         if (node instanceof Document)
176             node = ((Document) node).getDocumentElement();
177 
178         if (node == null)
179             return null;
180         if (node.getNodeType() == Node.ELEMENT_NODE)
181             return new ProxyElementAdapter(this, parent, (Element) node);
182         if (node.getNodeType() == Node.TEXT_NODE)
183             return new ProxyTextNodeAdapter(this, parent, (Text) node);
184         if (node.getNodeType() == Node.ATTRIBUTE_NODE)
185             return new ProxyAttrAdapter(this, parent, (Attr) node);
186 
187         return null; // Unsupported Node type - ignore for now
188     }
189 
190     public NamedNodeMap proxyNamedNodeMap(AdapterNode parent, NamedNodeMap nnm) {
191         return new ProxyNamedNodeMap(this, parent, nnm);
192     }
193 
194     /***
195      * Create an instance of an adapter dynamically and set its context via
196      * the AdapterNode interface.
197      */
198     private Node constructAdapterInstance(Class adapterClass, AdapterNode parent, String propertyName, Object propertyValue) {
199         // Check to see if the class has a no-args constructor
200         try {
201             adapterClass.getConstructor(new Class []{});
202         } catch (NoSuchMethodException e1) {
203             throw new StrutsException("Adapter class: " + adapterClass
204                     + " does not have a no-args consructor.");
205         }
206 
207         try {
208             AdapterNode adapterNode = (AdapterNode) adapterClass.newInstance();
209             adapterNode.setAdapterFactory(this);
210             adapterNode.setParent(parent);
211             adapterNode.setPropertyName(propertyName);
212             adapterNode.setPropertyValue(propertyValue);
213 
214             return adapterNode;
215 
216         } catch (IllegalAccessException e) {
217             e.printStackTrace();
218             throw new StrutsException("Cannot adapt " + propertyValue + " (" + propertyName + ") :" + e.getMessage());
219         } catch (InstantiationException e) {
220             e.printStackTrace();
221             throw new StrutsException("Cannot adapt " + propertyValue + " (" + propertyName + ") :" + e.getMessage());
222         }
223     }
224 
225     /***
226      * Create an appropriate adapter for a null value.
227      *
228      * @param parent
229      * @param propertyName
230      */
231     public Node adaptNullValue(BeanAdapter parent, String propertyName) {
232         return new StringAdapter(this, parent, propertyName, "null");
233     }
234 
235     //TODO: implement Configuration option to provide additional adapter classes
236     public Class getAdapterForValue(Object value) {
237         return adapterTypes.get(value.getClass());
238     }
239 }