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