View Javadoc

1   /*
2    * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.xml.io;
18  
19  import javax.xml.namespace.QName;
20  
21  import org.opensaml.xml.Configuration;
22  import org.opensaml.xml.Namespace;
23  import org.opensaml.xml.XMLObject;
24  import org.opensaml.xml.XMLObjectBuilder;
25  import org.opensaml.xml.XMLObjectBuilderFactory;
26  import org.opensaml.xml.schema.XSBooleanValue;
27  import org.opensaml.xml.util.DatatypeHelper;
28  import org.opensaml.xml.util.XMLConstants;
29  import org.opensaml.xml.util.XMLHelper;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  import org.w3c.dom.Attr;
33  import org.w3c.dom.Element;
34  import org.w3c.dom.NamedNodeMap;
35  import org.w3c.dom.Node;
36  import org.w3c.dom.NodeList;
37  import org.w3c.dom.Text;
38  
39  /**
40   * An thread safe abstract unmarshaller. This unmarshaller will:
41   * <ul>
42   * <li>Unmarshalling namespace decleration attributes</li>
43   * <li>Unmarshalling schema instance type (xsi:type) decleration attributes</li>
44   * <li>Delegating to child classes element, text, and attribute processing</li>
45   * </ul>
46   * 
47   * <strong>NOTE:</strong> In the case of Text nodes this unmarshaller will use {@link org.w3c.dom.Text#getWholeText()}
48   * to retrieve the textual content. This is probably exceptable in almost all cases, if, however, you need to deal with
49   * elements that contain multiple text node children you will need to override
50   * {@link #unmarshallTextContent(XMLObject, Text)} and do "the right thing" for your implementation.
51   */
52  public abstract class AbstractXMLObjectUnmarshaller implements Unmarshaller {
53  
54      /** Class logger. */
55      private final Logger log = LoggerFactory.getLogger(AbstractXMLObjectUnmarshaller.class);
56  
57      /** The target name and namespace for this unmarshaller. */
58      private QName targetQName;
59  
60      /** Factory for XMLObjectBuilders. */
61      private XMLObjectBuilderFactory xmlObjectBuilderFactory;
62  
63      /** Factory for creating unmarshallers for child elements. */
64      private UnmarshallerFactory unmarshallerFactory;
65  
66      /**
67       * Constructor.
68       */
69      protected AbstractXMLObjectUnmarshaller() {
70          xmlObjectBuilderFactory = Configuration.getBuilderFactory();
71          unmarshallerFactory = Configuration.getUnmarshallerFactory();
72      }
73  
74      /**
75       * This constructor supports checking a DOM Element to be unmarshalled, either element name or schema type, against
76       * a given namespace/local name pair.
77       * 
78       * @deprecated no replacement
79       * 
80       * @param targetNamespaceURI the namespace URI of either the schema type QName or element QName of the elements this
81       *            unmarshaller operates on
82       * @param targetLocalName the local name of either the schema type QName or element QName of the elements this
83       *            unmarshaller operates on
84       */
85      protected AbstractXMLObjectUnmarshaller(String targetNamespaceURI, String targetLocalName) {
86          targetQName = XMLHelper.constructQName(targetNamespaceURI, targetLocalName, null);
87  
88          xmlObjectBuilderFactory = Configuration.getBuilderFactory();
89          unmarshallerFactory = Configuration.getUnmarshallerFactory();
90      }
91  
92      /** {@inheritDoc} */
93      public XMLObject unmarshall(Element domElement) throws UnmarshallingException {
94          log.trace("Starting to unmarshall DOM element {}", XMLHelper.getNodeQName(domElement));
95  
96          checkElementIsTarget(domElement);
97  
98          XMLObject xmlObject = buildXMLObject(domElement);
99  
100         log.trace("Unmarshalling attributes of DOM Element {}", XMLHelper.getNodeQName(domElement));
101         NamedNodeMap attributes = domElement.getAttributes();
102         Node attribute;
103         for (int i = 0; i < attributes.getLength(); i++) {
104             attribute = attributes.item(i);
105 
106             // These should allows be attribute nodes, but just in case...
107             if (attribute.getNodeType() == Node.ATTRIBUTE_NODE) {
108                 unmarshallAttribute(xmlObject, (Attr) attribute);
109             }
110         }
111 
112         log.trace("Unmarshalling other child nodes of DOM Element {}", XMLHelper.getNodeQName(domElement));
113         NodeList childNodes = domElement.getChildNodes();
114         Node childNode;
115         for (int i = 0; i < childNodes.getLength(); i++) {
116             childNode = childNodes.item(i);
117 
118             if (childNode.getNodeType() == Node.ATTRIBUTE_NODE) {
119                 unmarshallAttribute(xmlObject, (Attr) childNode);
120             } else if (childNode.getNodeType() == Node.ELEMENT_NODE) {
121                 unmarshallChildElement(xmlObject, (Element) childNode);
122             } else if (childNode.getNodeType() == Node.TEXT_NODE) {
123                 unmarshallTextContent(xmlObject, (Text) childNode);
124             }
125         }
126 
127         xmlObject.setDOM(domElement);
128         return xmlObject;
129     }
130 
131     /**
132      * Checks that the given DOM Element's XSI type or namespace qualified element name matches the target QName of this
133      * unmarshaller.
134      * 
135      * @param domElement the DOM element to check
136      * 
137      * @throws UnmarshallingException thrown if the DOM Element does not match the target of this unmarshaller
138      */
139     protected void checkElementIsTarget(Element domElement) throws UnmarshallingException {
140         QName elementName = XMLHelper.getNodeQName(domElement);
141 
142         if (targetQName == null) {
143             log.trace(
144                     "Targeted QName checking is not available for this unmarshaller, DOM Element {} was not verified",
145                     elementName);
146             return;
147         }
148 
149         log.trace("Checking that {} meets target criteria.", elementName);
150 
151         QName type = XMLHelper.getXSIType(domElement);
152 
153         if (type != null && type.equals(targetQName)) {
154             log.trace("{} schema type matches target.", elementName);
155             return;
156         } else {
157             if (elementName.equals(targetQName)) {
158                 log.trace("{} element name matches target.", elementName);
159                 return;
160             } else {
161                 String errorMsg = "This unmarshaller only operates on " + targetQName + " elements not " + elementName;
162                 log.error(errorMsg);
163                 throw new UnmarshallingException(errorMsg);
164             }
165         }
166     }
167 
168     /**
169      * Constructs the XMLObject that the given DOM Element will be unmarshalled into. If the DOM element has an XML
170      * Schema type defined this method will attempt to retrieve an XMLObjectBuilder, from the factory given at
171      * construction time, using the schema type. If no schema type is present or no builder is registered with the
172      * factory for the schema type, the elements QName is used. Once the builder is found the XMLObject is create by
173      * invoking {@link XMLObjectBuilder#buildObject(String, String, String)}. Extending classes may wish to override
174      * this logic if more than just schema type or element name (e.g. element attributes or content) need to be used to
175      * determine which XMLObjectBuilder should be used to create the XMLObject.
176      * 
177      * @param domElement the DOM Element the created XMLObject will represent
178      * 
179      * @return the empty XMLObject that DOM Element can be unmarshalled into
180      * 
181      * @throws UnmarshallingException thrown if there is now XMLObjectBuilder registered for the given DOM Element
182      */
183     protected XMLObject buildXMLObject(Element domElement) throws UnmarshallingException {
184         log.trace("Building XMLObject for {}", XMLHelper.getNodeQName(domElement));
185         XMLObjectBuilder xmlObjectBuilder;
186 
187         xmlObjectBuilder = xmlObjectBuilderFactory.getBuilder(domElement);
188         if (xmlObjectBuilder == null) {
189             xmlObjectBuilder = xmlObjectBuilderFactory.getBuilder(Configuration.getDefaultProviderQName());
190             if (xmlObjectBuilder == null) {
191                 String errorMsg = "Unable to located builder for " + XMLHelper.getNodeQName(domElement);
192                 log.error(errorMsg);
193                 throw new UnmarshallingException(errorMsg);
194             } else {
195                 log.trace("No builder was registered for {} but the default builder {} was available, using it.",
196                         XMLHelper.getNodeQName(domElement), xmlObjectBuilder.getClass().getName());
197             }
198         }
199 
200         return xmlObjectBuilder.buildObject(domElement);
201     }
202 
203     /**
204      * Unmarshalls the attributes from the given DOM Attr into the given XMLObject. If the attribute is an XML namespace
205      * declaration the attribute is passed to
206      * {@link AbstractXMLObjectUnmarshaller#unmarshallNamespaceAttribute(XMLObject, Attr)}. If it is an schema type
207      * decleration (xsi:type) it is ignored because this attribute is handled by {@link #buildXMLObject(Element)}. All
208      * other attributes are passed to the {@link #processAttribute(XMLObject, Attr)}
209      * 
210      * @param attribute the attribute to be unmarshalled
211      * @param xmlObject the XMLObject that will recieve information from the DOM attribute
212      * 
213      * @throws UnmarshallingException thrown if there is a problem unmarshalling an attribute
214      */
215     protected void unmarshallAttribute(XMLObject xmlObject, Attr attribute) throws UnmarshallingException {
216         QName attribName = XMLHelper.getNodeQName(attribute);
217         log.trace("Pre-processing attribute {}", attribName);
218         String attributeNamespace = DatatypeHelper.safeTrimOrNullString(attribute.getNamespaceURI());
219         
220         if (DatatypeHelper.safeEquals(attributeNamespace, XMLConstants.XMLNS_NS)) {
221             unmarshallNamespaceAttribute(xmlObject, attribute);
222         } else if (DatatypeHelper.safeEquals(attributeNamespace, XMLConstants.XSI_NS)) {
223             unmarshallSchemaInstanceAttributes(xmlObject, attribute);
224         } else {
225             log.trace("Attribute {} is neither a schema type nor namespace, calling processAttribute()", XMLHelper
226                     .getNodeQName(attribute));
227             String attributeNSURI = attribute.getNamespaceURI();
228             String attributeNSPrefix;
229             if (attributeNSURI != null) {
230                 attributeNSPrefix = attribute.lookupPrefix(attributeNSURI);
231                 if (attributeNSPrefix == null && XMLConstants.XML_NS.equals(attributeNSURI)) {
232                     attributeNSPrefix = XMLConstants.XML_PREFIX;
233                 }
234                 xmlObject.getNamespaceManager().registerAttributeName(attribName);
235             }
236 
237             checkIDAttribute(attribute);
238 
239             processAttribute(xmlObject, attribute);
240         }
241     }
242 
243     /**
244      * Unmarshalls a namespace declaration attribute.
245      * 
246      * @param xmlObject the xmlObject to recieve the namespace decleration
247      * @param attribute the namespace decleration attribute
248      */
249     protected void unmarshallNamespaceAttribute(XMLObject xmlObject, Attr attribute) {
250         log.trace("{} is a namespace declaration, adding it to the list of namespaces on the XMLObject", XMLHelper
251                 .getNodeQName(attribute));
252         Namespace namespace;
253         if(DatatypeHelper.safeEquals(attribute.getLocalName(), XMLConstants.XMLNS_PREFIX)){
254             namespace = new Namespace(attribute.getValue(), null);
255         }else{
256             namespace = new Namespace(attribute.getValue(), attribute.getLocalName());
257         }
258         namespace.setAlwaysDeclare(true);
259         xmlObject.getNamespaceManager().registerNamespaceDeclaration(namespace);
260     }
261 
262     /**
263      * Unmarshalls the XSI type, schemaLocation, and noNamespaceSchemaLocation attributes.
264      * 
265      * @param xmlObject the xmlObject to recieve the namespace decleration
266      * @param attribute the namespace decleration attribute
267      */
268     protected void unmarshallSchemaInstanceAttributes(XMLObject xmlObject, Attr attribute) {
269         QName attribName = XMLHelper.getNodeQName(attribute);
270         if (XMLConstants.XSI_TYPE_ATTRIB_NAME.equals(attribName)) {
271             log.trace("Saw XMLObject {} with an xsi:type of: {}", xmlObject.getElementQName(), attribute.getValue());
272         } else if (XMLConstants.XSI_SCHEMA_LOCATION_ATTRIB_NAME.equals(attribName)) {
273             log.trace("Saw XMLObject {} with an xsi:schemaLocation of: {}", xmlObject.getElementQName(), 
274                     attribute.getValue());
275             xmlObject.setSchemaLocation(attribute.getValue());
276         } else if (XMLConstants.XSI_NO_NAMESPACE_SCHEMA_LOCATION_ATTRIB_NAME.equals(attribName)) {
277             log.trace("Saw XMLObject {} with an xsi:noNamespaceSchemaLocation of: {}", xmlObject.getElementQName(), 
278                     attribute.getValue());
279             xmlObject.setNoNamespaceSchemaLocation(attribute.getValue());
280         } else if (XMLConstants.XSI_NIL_ATTRIB_NAME.equals(attribName)) {
281             log.trace("Saw XMLObject {} with an xsi:nil of: {}", xmlObject.getElementQName(), 
282                     attribute.getValue());
283             xmlObject.setNil(XSBooleanValue.valueOf(attribute.getValue()));
284         }
285     }
286 
287     /**
288      * Check whether the attribute's QName is registered in the global ID attribute registry. If it is, and the
289      * specified attribute's DOM Level 3 Attr.isId() is false (due to lack of schema validation, for example), then
290      * declare the attribute as an ID type in the DOM on the attribute's owning element. This is to handle cases where
291      * the underlying DOM needs to accurately reflect an attribute's ID-ness, for example ID reference resolution within
292      * the Apache XML Security library.
293      * 
294      * @param attribute the DOM attribute to be checked
295      */
296     protected void checkIDAttribute(Attr attribute) {
297         QName attribName = XMLHelper.getNodeQName(attribute);
298         if (Configuration.isIDAttribute(attribName) && !attribute.isId()) {
299             attribute.getOwnerElement().setIdAttributeNode(attribute, true);
300         }
301     }
302 
303     /**
304      * Unmarshalls given Element's children. For each child an unmarshaller is retrieved using
305      * {@link UnmarshallerFactory#getUnmarshaller(Element)}. The unmarshaller is then used to unmarshall the child
306      * element and the resultant XMLObject is passed to {@link #processChildElement(XMLObject, XMLObject)} for further
307      * processing.
308      * 
309      * @param xmlObject the parent object of the unmarshalled children
310      * @param childElement the child element to be unmarshalled
311      * 
312      * @throws UnmarshallingException thrown if an error occurs unmarshalling the chilren elements
313      */
314     protected void unmarshallChildElement(XMLObject xmlObject, Element childElement) throws UnmarshallingException {
315         log.trace("Unmarshalling child elements of XMLObject {}", xmlObject.getElementQName());
316 
317         Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(childElement);
318 
319         if (unmarshaller == null) {
320             unmarshaller = unmarshallerFactory.getUnmarshaller(Configuration.getDefaultProviderQName());
321             if (unmarshaller == null) {
322                 String errorMsg = "No unmarshaller available for " + XMLHelper.getNodeQName(childElement)
323                         + ", child of " + xmlObject.getElementQName();
324                 log.error(errorMsg);
325                 throw new UnmarshallingException(errorMsg);
326             } else {
327                 log.trace("No unmarshaller was registered for {}, child of {}. Using default unmarshaller.", XMLHelper
328                         .getNodeQName(childElement), xmlObject.getElementQName());
329             }
330         }
331 
332         log.trace("Unmarshalling child element {}with unmarshaller {}", XMLHelper.getNodeQName(childElement),
333                 unmarshaller.getClass().getName());
334         processChildElement(xmlObject, unmarshaller.unmarshall(childElement));
335     }
336 
337     /**
338      * Unmarshalls the given Text node into a usable string by way of {@link Text#getWholeText()} and passes it off to
339      * {@link AbstractXMLObjectUnmarshaller#processElementContent(XMLObject, String)} if the string is not null and
340      * contains something other than whitespace.
341      * 
342      * @param xmlObject the XMLObject recieving the element content
343      * @param content the textual content
344      * 
345      * @throws UnmarshallingException thrown if there is a problem unmarshalling the text node
346      */
347     protected void unmarshallTextContent(XMLObject xmlObject, Text content) throws UnmarshallingException {
348         String textContent = DatatypeHelper.safeTrimOrNullString(content.getWholeText());
349         if (textContent != null) {
350             processElementContent(xmlObject, textContent);
351         }
352     }
353 
354     /**
355      * Called after a child element has been unmarshalled so that it can be added to the parent XMLObject.
356      * 
357      * @param parentXMLObject the parent XMLObject
358      * @param childXMLObject the child XMLObject
359      * 
360      * @throws UnmarshallingException thrown if there is a problem adding the child to the parent
361      */
362     protected abstract void processChildElement(XMLObject parentXMLObject, XMLObject childXMLObject)
363             throws UnmarshallingException;
364 
365     /**
366      * Called after an attribute has been unmarshalled so that it can be added to the XMLObject.
367      * 
368      * @param xmlObject the XMLObject
369      * @param attribute the attribute
370      * 
371      * @throws UnmarshallingException thrown if there is a problem adding the attribute to the XMLObject
372      */
373     protected abstract void processAttribute(XMLObject xmlObject, Attr attribute) throws UnmarshallingException;
374 
375     /**
376      * Called if the element being unmarshalled contained textual content so that it can be added to the XMLObject.
377      * 
378      * @param xmlObject XMLObject the content will be given to
379      * @param elementContent the Element's content
380      */
381     protected abstract void processElementContent(XMLObject xmlObject, String elementContent);
382 }