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 java.util.List;
20  import java.util.Set;
21  
22  import javax.xml.namespace.QName;
23  import javax.xml.parsers.DocumentBuilderFactory;
24  import javax.xml.parsers.ParserConfigurationException;
25  
26  import org.opensaml.xml.Configuration;
27  import org.opensaml.xml.Namespace;
28  import org.opensaml.xml.XMLObject;
29  import org.opensaml.xml.parse.XMLParserException;
30  import org.opensaml.xml.util.DatatypeHelper;
31  import org.opensaml.xml.util.XMLConstants;
32  import org.opensaml.xml.util.XMLHelper;
33  import org.slf4j.Logger;
34  import org.slf4j.LoggerFactory;
35  import org.w3c.dom.Document;
36  import org.w3c.dom.Element;
37  
38  /**
39   * A thread safe, abstract implementation of the {@link org.opensaml.xml.io.Marshaller} interface. This class handles
40   * most of the boilerplate code:
41   * <ul>
42   * <li>Ensuring elements to be marshalled are of either the correct xsi:type or element QName</li>
43   * <li>Setting the appropriate namespace and prefix for the marshalled element</li>
44   * <li>Setting the xsi:type for the element if the element has an explicit type</li>
45   * <li>Setting namespaces attributes declared for the element</li>
46   * <li>Marshalling of child elements</li>
47   * </ul>
48   */
49  public abstract class AbstractXMLObjectMarshaller implements Marshaller {
50  
51      /** Class logger. */
52      private final Logger log = LoggerFactory.getLogger(AbstractXMLObjectMarshaller.class);
53  
54      /** The target name and namespace for this marshaller. */
55      private QName targetQName;
56  
57      /** Factory for XMLObject Marshallers. */
58      private MarshallerFactory marshallerFactory;
59  
60      /**
61       * Constructor.
62       * 
63       */
64      protected AbstractXMLObjectMarshaller() {
65          marshallerFactory = Configuration.getMarshallerFactory();
66      }
67  
68      /**
69       * This constructor supports checking an XMLObject to be marshalled, either element name or schema type, against a
70       * given namespace/local name pair.
71       * 
72       * @deprecated no replacement
73       * 
74       * @param targetNamespaceURI the namespace URI of either the schema type QName or element QName of the elements this
75       *            unmarshaller operates on
76       * @param targetLocalName the local name of either the schema type QName or element QName of the elements this
77       *            unmarshaller operates on
78       */
79      protected AbstractXMLObjectMarshaller(String targetNamespaceURI, String targetLocalName) {
80          targetQName = XMLHelper.constructQName(targetNamespaceURI, targetLocalName, null);
81  
82          marshallerFactory = Configuration.getMarshallerFactory();
83      }
84  
85      /** {@inheritDoc} */
86      public Element marshall(XMLObject xmlObject) throws MarshallingException {
87          try {
88              Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
89              return marshall(xmlObject, document);
90          } catch (ParserConfigurationException e) {
91              throw new MarshallingException("Unable to create Document to place marshalled elements in", e);
92          }
93      }
94  
95      /** {@inheritDoc} */
96      public Element marshall(XMLObject xmlObject, Document document) throws MarshallingException {
97          Element domElement;
98  
99          log.trace("Starting to marshall {}", xmlObject.getElementQName());
100 
101         if (document == null) {
102             throw new MarshallingException("Given document may not be null");
103         }
104 
105         checkXMLObjectIsTarget(xmlObject);
106 
107         log.trace("Checking if {} contains a cached DOM representation", xmlObject.getElementQName());
108         domElement = xmlObject.getDOM();
109         if (domElement != null) {
110 
111             prepareForAdoption(xmlObject);
112 
113             if (domElement.getOwnerDocument() != document) {
114                 log.trace("Adopting DOM of XMLObject into given Document");
115                 XMLHelper.adoptElement(domElement, document);
116             }
117 
118             log.trace("Setting DOM of XMLObject as document element of given Document");
119             setDocumentElement(document, domElement);
120 
121             return domElement;
122         }
123 
124         log.trace("{} does not contain a cached DOM representation. Creating Element to marshall into.", xmlObject
125                 .getElementQName());
126         domElement = XMLHelper.constructElement(document, xmlObject.getElementQName());
127 
128         log.trace("Setting created element as document root");
129         // we need to do this before the rest of the marshalling so that signing and other ID dependent operations have
130         // a path to the document root
131         setDocumentElement(document, domElement);
132 
133         domElement = marshallInto(xmlObject, domElement);
134 
135         log.trace("Setting created element to DOM cache for XMLObject {}", xmlObject.getElementQName());
136         xmlObject.setDOM(domElement);
137         xmlObject.releaseParentDOM(true);
138 
139         return domElement;
140     }
141 
142     /** {@inheritDoc} */
143     public Element marshall(XMLObject xmlObject, Element parentElement) throws MarshallingException {
144         Element domElement;
145 
146         log.trace("Starting to marshall {} as child of {}", xmlObject.getElementQName(), XMLHelper
147                 .getNodeQName(parentElement));
148 
149         if (parentElement == null) {
150             throw new MarshallingException("Given parent element is null");
151         }
152 
153         checkXMLObjectIsTarget(xmlObject);
154 
155         log.trace("Checking if {} contains a cached DOM representation", xmlObject.getElementQName());
156         domElement = xmlObject.getDOM();
157         if (domElement != null) {
158             log.trace("{} contains a cached DOM representation", xmlObject.getElementQName());
159 
160             prepareForAdoption(xmlObject);
161 
162             log.trace("Appending DOM of XMLObject {} as child of parent element {}", xmlObject.getElementQName(),
163                     XMLHelper.getNodeQName(parentElement));
164             XMLHelper.appendChildElement(parentElement, domElement);
165 
166             return domElement;
167         }
168 
169         log.trace("{} does not contain a cached DOM representation. Creating Element to marshall into.", xmlObject
170                 .getElementQName());
171         Document owningDocument = parentElement.getOwnerDocument();
172         domElement = XMLHelper.constructElement(owningDocument, xmlObject.getElementQName());
173 
174         log.trace("Appending newly created element to given parent element");
175         // we need to do this before the rest of the marshalling so that signing and other ID dependent operations have
176         // a path to the document root
177         XMLHelper.appendChildElement(parentElement, domElement);
178         domElement = marshallInto(xmlObject, domElement);
179 
180         log.trace("Setting created element to DOM cache for XMLObject {}", xmlObject.getElementQName());
181         xmlObject.setDOM(domElement);
182         xmlObject.releaseParentDOM(true);
183 
184         return domElement;
185 
186     }
187 
188     /**
189      * Sets the given element as the Document Element of the given Document. If the document already has a Document
190      * Element it is replaced by the given element.
191      * 
192      * @param document the document
193      * @param element the Element that will serve as the Document Element
194      */
195     protected void setDocumentElement(Document document, Element element) {
196         Element documentRoot = document.getDocumentElement();
197         if (documentRoot != null) {
198             document.replaceChild(element, documentRoot);
199         } else {
200             document.appendChild(element);
201         }
202     }
203 
204     /**
205      * Marshalls the given XMLObject into the given DOM Element. The DOM Element must be within a DOM tree whose root is
206      * the Document Element of the Document that owns the given DOM Element.
207      * 
208      * @param xmlObject the XMLObject to marshall
209      * @param targetElement the Element into which the XMLObject is marshalled into
210      * 
211      * @return the DOM element the {@link XMLObject} is marshalled into
212      * 
213      * @throws MarshallingException thrown if there is a problem marshalling the object
214      */
215     protected Element marshallInto(XMLObject xmlObject, Element targetElement) throws MarshallingException {
216         log.trace("Setting namespace prefix for {} for XMLObject {}", xmlObject.getElementQName().getPrefix(),
217                 xmlObject.getElementQName());
218 
219         marshallNamespacePrefix(xmlObject, targetElement);
220 
221         marshallSchemaInstanceAttributes(xmlObject, targetElement);
222 
223         marshallNamespaces(xmlObject, targetElement);
224 
225         marshallAttributes(xmlObject, targetElement);
226 
227         marshallChildElements(xmlObject, targetElement);
228 
229         marshallElementContent(xmlObject, targetElement);
230 
231         return targetElement;
232     }
233 
234     /**
235      * Checks to make sure the given XMLObject's schema type or element QName matches the target parameters given at
236      * marshaller construction time.
237      * 
238      * @param xmlObject the XMLObject to marshall
239      * 
240      * @throws MarshallingException thrown if the given object is not or the required type
241      */
242     protected void checkXMLObjectIsTarget(XMLObject xmlObject) throws MarshallingException {
243         if (targetQName == null) {
244             log.trace("Targeted QName checking is not available for this marshaller, XMLObject {} was not verified",
245                     xmlObject.getElementQName());
246             return;
247         }
248 
249         log.trace("Checking that {} meets target criteria", xmlObject.getElementQName());
250         QName type = xmlObject.getSchemaType();
251         if (type != null && type.equals(targetQName)) {
252             log.trace("{} schema type matches target", xmlObject.getElementQName());
253             return;
254         } else {
255             QName elementQName = xmlObject.getElementQName();
256             if (elementQName.equals(targetQName)) {
257                 log.trace("{} element QName matches target", xmlObject.getElementQName());
258                 return;
259             }
260         }
261 
262         String errorMsg = "This marshaller only operations on " + targetQName + " elements not "
263                 + xmlObject.getElementQName();
264         log.error(errorMsg);
265         throw new MarshallingException(errorMsg);
266     }
267 
268     /**
269      * Marshalls the namespace prefix of the XMLObject into the DOM element.
270      * 
271      * @param xmlObject the XMLObject being marshalled
272      * @param domElement the DOM element the XMLObject is being marshalled into
273      */
274     protected void marshallNamespacePrefix(XMLObject xmlObject, Element domElement) {
275         String prefix = xmlObject.getElementQName().getPrefix();
276         prefix = DatatypeHelper.safeTrimOrNullString(prefix);
277 
278         if (prefix != null) {
279             domElement.setPrefix(prefix);
280         }
281     }
282 
283     /**
284      * Marshalls the child elements of the given XMLObject.
285      * 
286      * @param xmlObject the XMLObject whose children will be marshalled
287      * @param domElement the DOM element that will recieved the marshalled children
288      * 
289      * @throws MarshallingException thrown if there is a problem marshalling a child element
290      */
291     protected void marshallChildElements(XMLObject xmlObject, Element domElement) throws MarshallingException {
292         log.trace("Marshalling child elements for XMLObject {}", xmlObject.getElementQName());
293 
294         List<XMLObject> childXMLObjects = xmlObject.getOrderedChildren();
295         if (childXMLObjects != null && childXMLObjects.size() > 0) {
296             for (XMLObject childXMLObject : childXMLObjects) {
297                 if (childXMLObject == null) {
298                     continue;
299                 }
300 
301                 log.trace("Getting marshaller for child XMLObject {}", childXMLObject.getElementQName());
302                 Marshaller marshaller = marshallerFactory.getMarshaller(childXMLObject);
303 
304                 if (marshaller == null) {
305                     marshaller = marshallerFactory.getMarshaller(Configuration.getDefaultProviderQName());
306 
307                     if (marshaller == null) {
308                         String errorMsg = "No marshaller available for " + childXMLObject.getElementQName()
309                                 + ", child of " + xmlObject.getElementQName();
310                         log.error(errorMsg);
311                         throw new MarshallingException(errorMsg);
312                     } else {
313                         log.trace("No marshaller was registered for {}, child of {}. Using default marshaller",
314                                 childXMLObject.getElementQName(), xmlObject.getElementQName());
315                     }
316                 }
317 
318                 log.trace("Marshalling {} and adding it to DOM", childXMLObject.getElementQName());
319                 marshaller.marshall(childXMLObject, domElement);
320             }
321         } else {
322             log.trace("No child elements to marshall for XMLObject {}", xmlObject.getElementQName());
323         }
324     }
325 
326     /**
327      * Creates the xmlns attributes for any namespaces set on the given XMLObject.
328      * 
329      * @param xmlObject the XMLObject
330      * @param domElement the DOM element the namespaces will be added to
331      */
332     protected void marshallNamespaces(XMLObject xmlObject, Element domElement) {
333         log.trace("Marshalling namespace attributes for XMLObject {}", xmlObject.getElementQName());
334         Set<Namespace> namespaces = xmlObject.getNamespaces();
335 
336         for (Namespace namespace : namespaces) {
337             if (!namespace.alwaysDeclare()
338                     && XMLHelper.lookupNamespaceURI(domElement, namespace.getNamespacePrefix()) != null) {
339                 log.trace("Namespace {} has already been declared on an ancestor of no need to add it here", namespace,
340                         xmlObject.getElementQName());
341             } else {
342                 log.trace("Adding namespace decleration {} to {}", namespace, xmlObject.getElementQName());
343                 String nsURI = DatatypeHelper.safeTrimOrNullString(namespace.getNamespaceURI());
344                 String nsPrefix = DatatypeHelper.safeTrimOrNullString(namespace.getNamespacePrefix());
345 
346                 XMLHelper.appendNamespaceDeclaration(domElement, nsURI, nsPrefix);
347             }
348         }
349     }
350 
351     /**
352      * Creates the XSI type, schemaLocation, and noNamespaceSchemaLocation attributes for an XMLObject.
353      * 
354      * @param xmlObject the XMLObject
355      * @param domElement the DOM element the namespaces will be added to
356      * 
357      * @throws MarshallingException thrown if the schema type information is invalid
358      */
359     protected void marshallSchemaInstanceAttributes(XMLObject xmlObject, Element domElement)
360             throws MarshallingException {
361 
362         if (!DatatypeHelper.isEmpty(xmlObject.getSchemaLocation())) {
363             log.trace("Setting xsi:schemaLocation for XMLObject {} to {}", xmlObject.getElementQName(), xmlObject
364                     .getSchemaLocation());
365             domElement.setAttributeNS(XMLConstants.XSI_NS, XMLConstants.XSI_PREFIX + ":schemaLocation", xmlObject
366                     .getSchemaLocation());
367         }
368 
369         if (!DatatypeHelper.isEmpty(xmlObject.getNoNamespaceSchemaLocation())) {
370             log.trace("Setting xsi:noNamespaceSchemaLocation for XMLObject {} to {}", xmlObject.getElementQName(),
371                     xmlObject.getNoNamespaceSchemaLocation());
372             domElement.setAttributeNS(XMLConstants.XSI_NS, XMLConstants.XSI_PREFIX + ":noNamespaceSchemaLocation",
373                     xmlObject.getNoNamespaceSchemaLocation());
374         }
375 
376         QName type = xmlObject.getSchemaType();
377         if (type == null) {
378             return;
379         }
380 
381         log.trace("Setting xsi:type attribute with for XMLObject {}", xmlObject.getElementQName());
382         String typeLocalName = DatatypeHelper.safeTrimOrNullString(type.getLocalPart());
383         String typePrefix = DatatypeHelper.safeTrimOrNullString(type.getPrefix());
384 
385         if (typeLocalName == null) {
386             throw new MarshallingException("The type QName on XMLObject " + xmlObject.getElementQName()
387                     + " may not have a null local name");
388         }
389 
390         if (type.getNamespaceURI() == null) {
391             throw new MarshallingException("The type URI QName on XMLObject " + xmlObject.getElementQName()
392                     + " may not have a null namespace URI");
393         }
394 
395         String attributeValue;
396         if (typePrefix == null) {
397             attributeValue = typeLocalName;
398         } else {
399             attributeValue = typePrefix + ":" + typeLocalName;
400         }
401 
402         domElement.setAttributeNS(XMLConstants.XSI_NS, XMLConstants.XSI_PREFIX + ":type", attributeValue);
403 
404         log.trace("Adding XSI namespace to list of namespaces used by XMLObject {}", xmlObject.getElementQName());
405         xmlObject.addNamespace(new Namespace(XMLConstants.XSI_NS, XMLConstants.XSI_PREFIX));
406     }
407 
408     /**
409      * Marshalls a given XMLObject into a W3C Element. The given signing context should be blindly passed to the
410      * marshaller for child elements. The XMLObject passed to this method is guaranteed to be of the target name
411      * specified during this unmarshaller's construction.
412      * 
413      * @param xmlObject the XMLObject to marshall
414      * @param domElement the W3C DOM element
415      * 
416      * @throws MarshallingException thrown if there is a problem marshalling the element
417      */
418     protected abstract void marshallAttributes(XMLObject xmlObject, Element domElement) throws MarshallingException;
419 
420     /**
421      * Marshalls data from the XMLObject into content of the DOM Element.
422      * 
423      * @param xmlObject the XMLObject
424      * @param domElement the DOM element recieving the content
425      * 
426      * @throws MarshallingException thrown if the textual content can not be added to the DOM element
427      */
428     protected abstract void marshallElementContent(XMLObject xmlObject, Element domElement) throws MarshallingException;
429 
430     /**
431      * Prepares the given DOM caching XMLObject for adoption into another document. If the XMLObject has a parent then
432      * all visible namespaces used by the given XMLObject and its descendants are declared within that subtree and the
433      * parent's DOM is invalidated.
434      * 
435      * @param domCachingObject the XMLObject to prepare for adoption
436      * 
437      * @throws MarshallingException thrown if a namespace within the XMLObject's DOM subtree can not be resolved.
438      */
439     private void prepareForAdoption(XMLObject domCachingObject) throws MarshallingException {
440         if (domCachingObject.getParent() != null) {
441             log.trace("Rooting all visible namespaces of XMLObject {} before adding it to new parent Element",
442                     domCachingObject.getElementQName());
443             try {
444                 XMLHelper.rootNamespaces(domCachingObject.getDOM());
445             } catch (XMLParserException e) {
446                 String errorMsg = "Unable to root namespaces of cached DOM element, "
447                         + domCachingObject.getElementQName();
448                 log.error(errorMsg, e);
449                 throw new MarshallingException(errorMsg, e);
450             }
451 
452             log.trace("Release DOM of XMLObject parent");
453             domCachingObject.releaseParentDOM(true);
454         }
455     }
456 }