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;
18  
19  import java.util.Collections;
20  import java.util.List;
21  import java.util.Set;
22  
23  import javax.xml.namespace.QName;
24  
25  import org.opensaml.xml.schema.XSBooleanValue;
26  import org.opensaml.xml.util.DatatypeHelper;
27  import org.opensaml.xml.util.IDIndex;
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.Element;
33  
34  /**
35   * An abstract implementation of XMLObject.
36   */
37  public abstract class AbstractXMLObject implements XMLObject {
38  
39      /** Class logger. */
40      private final Logger log = LoggerFactory.getLogger(AbstractXMLObject.class);
41  
42      /** Parent of this element. */
43      private XMLObject parent;
44  
45      /** The name of this element with namespace and prefix information. */
46      private QName elementQname;
47  
48      /** Schema locations for this XML object. */
49      private String schemaLocation;
50  
51      /** No-namespace schema locations for this XML object. */
52      private String noNamespaceSchemaLocation;
53  
54      /** The schema type of this element with namespace and prefix information. */
55      private QName typeQname;
56  
57      /** DOM Element representation of this object. */
58      private Element dom;
59      
60      /** The value of the <code>xsi:nil</code> attribute. */
61      private  XSBooleanValue nil;
62      
63      /** The namespace manager for this XML object. */
64      private NamespaceManager nsManager;
65  
66      /**
67       * Mapping of ID attributes to XMLObjects in the subtree rooted at this object. This allows constant-time
68       * dereferencing of ID-typed attributes within the subtree.
69       */
70      private final IDIndex idIndex;
71  
72      /**
73       * Constructor.
74       * 
75       * @param namespaceURI the namespace the element is in
76       * @param elementLocalName the local name of the XML element this Object represents
77       * @param namespacePrefix the prefix for the given namespace
78       */
79      protected AbstractXMLObject(String namespaceURI, String elementLocalName, String namespacePrefix) {
80          nsManager = new NamespaceManager(this);
81          idIndex = new IDIndex(this);
82          elementQname = XMLHelper.constructQName(namespaceURI, elementLocalName, namespacePrefix);
83          if(namespaceURI != null){
84              setElementNamespacePrefix(namespacePrefix);
85          }
86      }
87      
88      /** {@inheritDoc} */
89      public void addNamespace(Namespace newNamespace) {
90          getNamespaceManager().registerNamespace(newNamespace);
91      }
92  
93      /** {@inheritDoc} */
94      public void detach(){
95          releaseParentDOM(true);
96          parent = null;
97      }
98  
99      /** {@inheritDoc} */
100     public Element getDOM() {
101         return dom;
102     }
103 
104     /** {@inheritDoc} */
105     public QName getElementQName() {
106         return new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart(), elementQname.getPrefix());
107     }
108 
109     /** {@inheritDoc} */
110     public IDIndex getIDIndex() {
111         return idIndex;
112     }
113     
114     /** {@inheritDoc} */
115     public NamespaceManager getNamespaceManager() {
116         return nsManager;
117     }
118 
119     /** {@inheritDoc} */
120     public Set<Namespace> getNamespaces() {
121         return Collections.unmodifiableSet(getNamespaceManager().getNamespaces());
122     }
123 
124     /** {@inheritDoc} */
125     public String getNoNamespaceSchemaLocation() {
126         return noNamespaceSchemaLocation;
127     }
128 
129     /**
130      * Gets the parent of this element.
131      * 
132      * @return the parent of this element
133      */
134     public XMLObject getParent() {
135         return parent;
136     }
137 
138     /** {@inheritDoc} */
139     public String getSchemaLocation() {
140         return schemaLocation;
141     }
142 
143     /** {@inheritDoc} */
144     public QName getSchemaType() {
145         return typeQname;
146     }
147 
148     /** {@inheritDoc} */
149     public boolean hasChildren() {
150         List<? extends XMLObject> children = getOrderedChildren();
151         return children != null && children.size() > 0;
152     }
153 
154     /** {@inheritDoc} */
155     public boolean hasParent() {
156         return getParent() != null;
157     }
158     
159     /**
160      * A helper function for derived classes.  This method should be called when the value of a
161      * namespace-qualified attribute changes.
162      * 
163      * @param attributeName the attribute name
164      * @param hasValue true to indicate that the attribute has a value, false to indicate it has no value
165      */
166     protected void manageQualifiedAttributeNamespace(QName attributeName, boolean hasValue) {
167         if (hasValue) {
168             getNamespaceManager().registerAttributeName(attributeName);
169         } else {
170             getNamespaceManager().deregisterAttributeName(attributeName);
171         }
172     }
173 
174     /**
175      * A helper function for derived classes. This checks for semantic equality between two QNames if it they are
176      * different invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
177      * prepareForAssignment(this.foo, foo);
178      * 
179      * @param oldValue - the current value
180      * @param newValue - the new value
181      * 
182      * @return the value that should be assigned
183      * 
184      * @deprecated replacement {@link #prepareAttributeValueForAssignment(String, QName, QName)} 
185      *                or {@link #prepareElementContentForAssignment(QName, QName)} as appropriate
186      */
187     protected QName prepareForAssignment(QName oldValue, QName newValue) {
188         if (oldValue == null) {
189             if (newValue != null) {
190                 Namespace newNamespace = new Namespace(newValue.getNamespaceURI(), newValue.getPrefix());
191                 addNamespace(newNamespace);
192                 releaseThisandParentDOM();
193                 return newValue;
194             } else {
195                 return null;
196             }
197         }
198 
199         if (!oldValue.equals(newValue)) {
200             if (newValue != null) {
201                 Namespace newNamespace = new Namespace(newValue.getNamespaceURI(), newValue.getPrefix());
202                 addNamespace(newNamespace);
203             }
204             releaseThisandParentDOM();
205         }
206 
207         return newValue;
208     }
209     
210     /**
211      * A helper function for derived classes. This checks for semantic equality between two QNames if it they are
212      * different invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
213      * prepareElementContentForAssignment(this.foo, foo);
214      * 
215      * @param oldValue - the current value
216      * @param newValue - the new value
217      * 
218      * @return the value that should be assigned
219      */
220     protected QName prepareElementContentForAssignment(QName oldValue, QName newValue) {
221         if (oldValue == null) {
222             if (newValue != null) {
223                 getNamespaceManager().registerContentValue(newValue);
224                 releaseThisandParentDOM();
225                 return newValue;
226             } else {
227                 return null;
228             }
229         }
230         
231         // Old value was not null, so go ahead and deregister it
232         getNamespaceManager().deregisterContentValue();
233 
234         if (!oldValue.equals(newValue)) {
235             if (newValue != null) {
236                 getNamespaceManager().registerContentValue(newValue);
237             }
238             releaseThisandParentDOM();
239         }
240 
241         return newValue;
242     }
243     
244     
245     /**
246      * A helper function for derived classes. This checks for semantic equality between two QNames if it they are
247      * different invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
248      * prepareAttributeValueForAssignment(this.foo, foo);
249      * 
250      * @param attributeID - unique identifier of the attribute in the content model within this XMLObject, used to 
251      *        identify the attribute within the XMLObject's NamespaceManager
252      * @param oldValue - the current value
253      * @param newValue - the new value
254      * 
255      * @return the value that should be assigned
256      */
257     protected QName prepareAttributeValueForAssignment(String attributeID, QName oldValue, QName newValue) {
258         if (oldValue == null) {
259             if (newValue != null) {
260                 getNamespaceManager().registerAttributeValue(attributeID, newValue);
261                 releaseThisandParentDOM();
262                 return newValue;
263             } else {
264                 return null;
265             }
266         }
267         
268         // Old value was not null, so go ahead and deregister it
269         getNamespaceManager().deregisterAttributeValue(attributeID);
270 
271         if (!oldValue.equals(newValue)) {
272             if (newValue != null) {
273                 getNamespaceManager().registerAttributeValue(attributeID, newValue);
274             }
275             releaseThisandParentDOM();
276         }
277 
278         return newValue;
279     }
280 
281     /**
282      * A helper function for derived classes. This 'nornmalizes' newString and then if it is different from oldString
283      * invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
284      * prepareForAssignment(this.foo, foo);
285      * 
286      * @param oldValue - the current value
287      * @param newValue - the new value
288      * 
289      * @return the value that should be assigned
290      */
291     protected String prepareForAssignment(String oldValue, String newValue) {
292         String newString = DatatypeHelper.safeTrimOrNullString(newValue);
293 
294         if (!DatatypeHelper.safeEquals(oldValue, newString)) {
295             releaseThisandParentDOM();
296         }
297 
298         return newString;
299     }
300 
301     /**
302      * A helper function for derived classes that checks to see if the old and new value are equal and if so releases
303      * the cached dom. Derived classes are expected to use this thus: <code>
304      *   this.foo = prepareForAssignment(this.foo, foo);
305      *   </code>
306      * 
307      * This method will do a (null) safe compare of the objects and will also invalidate the DOM if appropriate
308      * 
309      * @param <T> - type of object being compared and assigned
310      * @param oldValue - current value
311      * @param newValue - proposed new value
312      * 
313      * @return The value to assign to the saved Object.
314      */
315     protected <T extends Object> T prepareForAssignment(T oldValue, T newValue) {
316         if (oldValue == null) {
317             if (newValue != null) {
318                 releaseThisandParentDOM();
319                 return newValue;
320             } else {
321                 return null;
322             }
323         }
324 
325         if (!oldValue.equals(newValue)) {
326             releaseThisandParentDOM();
327         }
328 
329         return newValue;
330     }
331 
332     /**
333      * A helper function for derived classes, similar to assignString, but for (singleton) SAML objects. It is
334      * indifferent to whether either the old or the new version of the value is null. Derived classes are expected to
335      * use this thus: <code>
336      *   this.foo = prepareForAssignment(this.foo, foo);
337      *   </code>
338      * 
339      * This method will do a (null) safe compare of the objects and will also invalidate the DOM if appropriate
340      * 
341      * @param <T> type of object being compared and assigned
342      * @param oldValue current value
343      * @param newValue proposed new value
344      * 
345      * @return The value to assign to the saved Object.
346      */
347     protected <T extends XMLObject> T prepareForAssignment(T oldValue, T newValue) {
348 
349         if (newValue != null && newValue.hasParent()) {
350             throw new IllegalArgumentException(newValue.getClass().getName()
351                     + " cannot be added - it is already the child of another SAML Object");
352         }
353 
354         if (oldValue == null) {
355             if (newValue != null) {
356                 releaseThisandParentDOM();
357                 newValue.setParent(this);
358                 idIndex.registerIDMappings(newValue.getIDIndex());
359                 return newValue;
360 
361             } else {
362                 return null;
363             }
364         }
365 
366         if (!oldValue.equals(newValue)) {
367             oldValue.setParent(null);
368             releaseThisandParentDOM();
369             idIndex.deregisterIDMappings(oldValue.getIDIndex());
370             if (newValue != null) {
371                 newValue.setParent(this);
372                 idIndex.registerIDMappings(newValue.getIDIndex());
373             }
374         }
375 
376         return newValue;
377     }
378 
379     /**
380      * A helper function for derived classes. The mutator/setter method for any ID-typed attributes should call this
381      * method in order to handle getting the old value removed from the ID-to-XMLObject mapping, and the new value added
382      * to the mapping.
383      * 
384      * @param oldID the old value of the ID-typed attribute
385      * @param newID the new value of the ID-typed attribute
386      */
387     protected void registerOwnID(String oldID, String newID) {
388         String newString = DatatypeHelper.safeTrimOrNullString(newID);
389 
390         if (!DatatypeHelper.safeEquals(oldID, newString)) {
391             if (oldID != null) {
392                 idIndex.deregisterIDMapping(oldID);
393             }
394 
395             if (newString != null) {
396                 idIndex.registerIDMapping(newString, this);
397             }
398         }
399     }
400 
401     /** {@inheritDoc} */
402     public void releaseChildrenDOM(boolean propagateRelease) {
403         log.trace("Releasing cached DOM reprsentation for children of {} with propagation set to {}",
404                 getElementQName(), propagateRelease);
405         if (getOrderedChildren() != null) {
406             for (XMLObject child : getOrderedChildren()) {
407                 if (child != null) {
408                     child.releaseDOM();
409                     if (propagateRelease) {
410                         child.releaseChildrenDOM(propagateRelease);
411                     }
412                 }
413             }
414         }
415     }
416 
417     /** {@inheritDoc} */
418     public void releaseDOM() {
419         log.trace("Releasing cached DOM reprsentation for {}", getElementQName());
420         setDOM(null);
421     }
422 
423     /** {@inheritDoc} */
424     public void releaseParentDOM(boolean propagateRelease) {
425         log.trace("Releasing cached DOM reprsentation for parent of {} with propagation set to {}", getElementQName(),
426                 propagateRelease);
427         XMLObject parentElement = getParent();
428         if (parentElement != null) {
429             parent.releaseDOM();
430             if (propagateRelease) {
431                 parent.releaseParentDOM(propagateRelease);
432             }
433         }
434     }
435 
436     /**
437      * A convience method that is equal to calling {@link #releaseDOM()} then {@link #releaseChildrenDOM(boolean)} with
438      * the release being propogated.
439      */
440     public void releaseThisAndChildrenDOM() {
441         if (getDOM() != null) {
442             releaseDOM();
443             releaseChildrenDOM(true);
444         }
445     }
446 
447     /**
448      * A convience method that is equal to calling {@link #releaseDOM()} then {@link #releaseParentDOM(boolean)} with
449      * the release being propogated.
450      */
451     public void releaseThisandParentDOM() {
452         if (getDOM() != null) {
453             releaseDOM();
454             releaseParentDOM(true);
455         }
456     }
457 
458     /** {@inheritDoc} */
459     public void removeNamespace(Namespace namespace) {
460         getNamespaceManager().deregisterNamespace(namespace);
461     }
462 
463     /** {@inheritDoc} */
464     public XMLObject resolveID(String id) {
465         return idIndex.lookup(id);
466     }
467 
468     /** {@inheritDoc} */
469     public XMLObject resolveIDFromRoot(String id) {
470         XMLObject root = this;
471         while (root.hasParent()) {
472             root = root.getParent();
473         }
474         return root.resolveID(id);
475     }
476 
477     /** {@inheritDoc} */
478     public void setDOM(Element newDom) {
479         dom = newDom;
480     }
481 
482     /**
483      * Sets the prefix for this element's namespace.
484      * 
485      * @param prefix the prefix for this element's namespace
486      */
487     public void setElementNamespacePrefix(String prefix) {
488         if (prefix == null) {
489             elementQname = new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart());
490         } else {
491             elementQname = new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart(), prefix);
492         }
493         getNamespaceManager().registerElementName(elementQname);
494     }
495 
496     /**
497      * Sets the element QName.
498      * 
499      * @param elementQName the element's QName
500      */
501     protected void setElementQName(QName elementQName) {
502         this.elementQname = XMLHelper.constructQName(elementQName.getNamespaceURI(), elementQName.getLocalPart(),
503                 elementQName.getPrefix());
504         getNamespaceManager().registerElementName(this.elementQname);
505     }
506 
507     /** {@inheritDoc} */
508     public void setNoNamespaceSchemaLocation(String location) {
509         noNamespaceSchemaLocation = DatatypeHelper.safeTrimOrNullString(location);
510         manageQualifiedAttributeNamespace(XMLConstants.XSI_NO_NAMESPACE_SCHEMA_LOCATION_ATTRIB_NAME, schemaLocation != null);
511     }
512 
513     /** {@inheritDoc} */
514     public void setParent(XMLObject newParent) {
515         parent = newParent;
516     }
517 
518     /** {@inheritDoc} */
519     public void setSchemaLocation(String location) {
520         schemaLocation = DatatypeHelper.safeTrimOrNullString(location);
521         manageQualifiedAttributeNamespace(XMLConstants.XSI_SCHEMA_LOCATION_ATTRIB_NAME, schemaLocation != null);
522     }
523 
524     /**
525      * Sets a given QName as the schema type for the Element represented by this XMLObject. This will register the namespace
526      * for the type as well as for the xsi:type qualified attribute name with the namespace manager for this XMLObject.
527      * If null is passed, the type name and xsi:type name will be deregistered.
528      * 
529      * @param type the schema type
530      */
531     protected void setSchemaType(QName type) {
532         typeQname = type;
533         getNamespaceManager().registerElementType(typeQname);
534         manageQualifiedAttributeNamespace(XMLConstants.XSI_TYPE_ATTRIB_NAME, typeQname != null);
535     }
536     
537     /** {@inheritDoc} */
538     public Boolean isNil() {
539         if (nil != null) {
540             return nil.getValue();
541         }
542 
543         return Boolean.FALSE;
544     }
545 
546     /** {@inheritDoc} */
547     public XSBooleanValue isNilXSBoolean() {
548         return nil;
549     }
550 
551     /** {@inheritDoc} */
552     public void setNil(Boolean newNil) {
553         if (newNil != null) {
554             nil = prepareForAssignment(nil, new XSBooleanValue(newNil, false));
555         } else {
556             nil = prepareForAssignment(nil, null);
557         }
558         manageQualifiedAttributeNamespace(XMLConstants.XSI_NIL_ATTRIB_NAME, nil != null);
559     }
560 
561     /** {@inheritDoc} */
562     public void setNil(XSBooleanValue newNil) {
563         nil = prepareForAssignment(nil, newNil);
564         manageQualifiedAttributeNamespace(XMLConstants.XSI_NIL_ATTRIB_NAME, nil != null);
565     }
566 
567 }