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