View Javadoc

1   /*
2    * $Id: AdapterFactory.java 491622 2007-01-01 20:22:54Z mrdon $
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *  http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  package org.apache.struts2.views.xslt;
22  
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.Map;
26  
27  import org.apache.struts2.StrutsException;
28  import org.w3c.dom.Attr;
29  import org.w3c.dom.Document;
30  import org.w3c.dom.Element;
31  import org.w3c.dom.NamedNodeMap;
32  import org.w3c.dom.Node;
33  import org.w3c.dom.Text;
34  
35  /***
36   * AdapterFactory produces Node adapters for Java object types.
37   * Adapter classes are generally instantiated dynamically via a no-args constructor
38   * and populated with their context information via the AdapterNode interface.
39   *
40   * This factory supports proxying of generic DOM Node trees, allowing arbitrary
41   * Node types to be mixed together.  You may simply return a Document or Node
42   * type as an object property and it will appear as a sub-tree in the XML as
43   * you'd expect. See #proxyNode().
44   *
45   * Customization of the result XML can be accomplished by providing
46   * alternate adapters for Java types.  Adapters are associated with Java
47   * types through the registerAdapterType() method.
48   *
49   * For example, since there is no default Date adapter, Date objects will be
50   * rendered with the generic Bean introspecting adapter, producing output
51   * like:
52   * <pre>
53       <date>
54          <date>19</date>
55          <day>1</day>
56          <hours>0</hours>
57          <minutes>7</minutes>
58          <month>8</month>
59          <seconds>4</seconds>
60          <time>1127106424531</time>
61          <timezoneOffset>300</timezoneOffset>
62          <year>105</year>
63      </date>
64   * </pre>
65   *
66   * By extending the StringAdapter and overriding its normal behavior we can
67   * create a custom Date formatter:
68   *
69   * <pre>
70        public static class CustomDateAdapter extends StringAdapter {
71          protected String getStringValue() {
72              Date date = (Date)getPropertyValue();
73              return DateFormat.getTimeInstance( DateFormat.FULL ).format( date );
74          }
75      }
76   * </pre>
77   *
78   * Producing output like:
79   *
80  <pre>
81       <date>12:02:54 AM CDT</date>
82   </pre>
83   *
84   * The StringAdapter (which is normally invoked only to adapt String values)
85   * is a useful base for these kinds of customizations and can produce
86   * structured XML output as well as plain text by setting its parseStringAsXML()
87   * property to true.
88   *
89   * See provided examples.
90   */
91  public class AdapterFactory {
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 }