View Javadoc

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