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.util.DatatypeHelper;
26  import org.opensaml.xml.util.IDIndex;
27  import org.opensaml.xml.util.LazySet;
28  import org.opensaml.xml.util.XMLHelper;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  import org.w3c.dom.Element;
32  
33  /**
34   * An abstract implementation of XMLObject.
35   */
36  public abstract class AbstractXMLObject implements XMLObject {
37  
38      /** Class logger. */
39      private final Logger log = LoggerFactory.getLogger(AbstractXMLObject.class);
40  
41      /** Parent of this element. */
42      private XMLObject parent;
43  
44      /** The name of this element with namespace and prefix information. */
45      private QName elementQname;
46  
47      /** Schema locations for this XML object. */
48      private String schemaLocation;
49  
50      /** No-namespace schema locations for this XML object. */
51      private String noNamespaceSchemaLocation;
52  
53      /** The schema type of this element with namespace and prefix information. */
54      private QName typeQname;
55  
56      /** Namespaces declared on this element. */
57      private Set<Namespace> namespaces;
58  
59      /** DOM Element representation of this object. */
60      private Element dom;
61  
62      /**
63       * Mapping of ID attributes to XMLObjects in the subtree rooted at this object. This allows constant-time
64       * dereferencing of ID-typed attributes within the subtree.
65       */
66      private final IDIndex idIndex;
67  
68      /**
69       * Constructor.
70       * 
71       * @param namespaceURI the namespace the element is in
72       * @param elementLocalName the local name of the XML element this Object represents
73       * @param namespacePrefix the prefix for the given namespace
74       */
75      protected AbstractXMLObject(String namespaceURI, String elementLocalName, String namespacePrefix) {
76          idIndex = new IDIndex(this);
77          namespaces = new LazySet<Namespace>();
78          elementQname = XMLHelper.constructQName(namespaceURI, elementLocalName, namespacePrefix);
79          if(namespaceURI != null){
80              addNamespace(new Namespace(namespaceURI, namespacePrefix));
81              setElementNamespacePrefix(namespacePrefix);
82          }
83      }
84      
85      /** {@inheritDoc} */
86      public void addNamespace(Namespace newNamespace) {
87          if(newNamespace == null){
88              return;
89          }
90          
91          if(namespaces.size() == 0){
92              namespaces.add(newNamespace);
93              return;
94          }
95          
96          for(Namespace namespace : namespaces){
97              if(DatatypeHelper.safeEquals(namespace.getNamespaceURI(), newNamespace.getNamespaceURI()) &&
98                      DatatypeHelper.safeEquals(namespace.getNamespacePrefix(), newNamespace.getNamespacePrefix())){
99                  if(newNamespace.alwaysDeclare()){
100                     namespace.setAlwaysDeclare(true);
101                     return;
102                 }
103             }
104         }
105         
106         namespaces.add(newNamespace);
107     }
108 
109     /** {@inheritDoc} */
110     public void detach(){
111         releaseParentDOM(true);
112         parent = null;
113     }
114 
115     /** {@inheritDoc} */
116     public Element getDOM() {
117         return dom;
118     }
119 
120     /** {@inheritDoc} */
121     public QName getElementQName() {
122         return new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart(), elementQname.getPrefix());
123     }
124 
125     /** {@inheritDoc} */
126     public IDIndex getIDIndex() {
127         return idIndex;
128     }
129 
130     /** {@inheritDoc} */
131     public Set<Namespace> getNamespaces() {
132         return Collections.unmodifiableSet(namespaces);
133     }
134 
135     /** {@inheritDoc} */
136     public String getNoNamespaceSchemaLocation() {
137         return noNamespaceSchemaLocation;
138     }
139 
140     /**
141      * Gets the parent of this element.
142      * 
143      * @return the parent of this element
144      */
145     public XMLObject getParent() {
146         return parent;
147     }
148 
149     /** {@inheritDoc} */
150     public String getSchemaLocation() {
151         return schemaLocation;
152     }
153 
154     /** {@inheritDoc} */
155     public QName getSchemaType() {
156         return typeQname;
157     }
158 
159     /** {@inheritDoc} */
160     public boolean hasChildren() {
161         List<? extends XMLObject> children = getOrderedChildren();
162         return children != null && children.size() > 0;
163     }
164 
165     /** {@inheritDoc} */
166     public boolean hasParent() {
167         return getParent() != null;
168     }
169 
170     /**
171      * A helper function for derived classes. This checks for semantic equality between two QNames if it they are
172      * different invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
173      * prepareForAssignment(this.foo, foo);
174      * 
175      * @param oldValue - the current value
176      * @param newValue - the new value
177      * 
178      * @return the value that should be assigned
179      */
180     protected QName prepareForAssignment(QName oldValue, QName newValue) {
181         if (oldValue == null) {
182             if (newValue != null) {
183                 Namespace newNamespace = new Namespace(newValue.getNamespaceURI(), newValue.getPrefix());
184                 addNamespace(newNamespace);
185                 releaseThisandParentDOM();
186                 return newValue;
187             } else {
188                 return null;
189             }
190         }
191 
192         if (!oldValue.equals(newValue)) {
193             if (newValue != null) {
194                 Namespace newNamespace = new Namespace(newValue.getNamespaceURI(), newValue.getPrefix());
195                 addNamespace(newNamespace);
196             }
197             releaseThisandParentDOM();
198         }
199 
200         return newValue;
201     }
202 
203     /**
204      * A helper function for derived classes. This 'nornmalizes' newString and then if it is different from oldString
205      * invalidates the DOM. It returns the normalized value so subclasses just have to go. this.foo =
206      * prepareForAssignment(this.foo, foo);
207      * 
208      * @param oldValue - the current value
209      * @param newValue - the new value
210      * 
211      * @return the value that should be assigned
212      */
213     protected String prepareForAssignment(String oldValue, String newValue) {
214         String newString = DatatypeHelper.safeTrimOrNullString(newValue);
215 
216         if (!DatatypeHelper.safeEquals(oldValue, newString)) {
217             releaseThisandParentDOM();
218         }
219 
220         return newString;
221     }
222 
223     /**
224      * A helper function for derived classes that checks to see if the old and new value are equal and if so releases
225      * the cached dom. Derived classes are expected to use this thus: <code>
226      *   this.foo = prepareForAssignment(this.foo, foo);
227      *   </code>
228      * 
229      * This method will do a (null) safe compare of the objects and will also invalidate the DOM if appropriate
230      * 
231      * @param <T> - type of object being compared and assigned
232      * @param oldValue - current value
233      * @param newValue - proposed new value
234      * 
235      * @return The value to assign to the saved Object.
236      */
237     protected <T extends Object> T prepareForAssignment(T oldValue, T newValue) {
238         if (oldValue == null) {
239             if (newValue != null) {
240                 releaseThisandParentDOM();
241                 return newValue;
242             } else {
243                 return null;
244             }
245         }
246 
247         if (!oldValue.equals(newValue)) {
248             releaseThisandParentDOM();
249         }
250 
251         return newValue;
252     }
253 
254     /**
255      * A helper function for derived classes, similar to assignString, but for (singleton) SAML objects. It is
256      * indifferent to whether either the old or the new version of the value is null. Derived classes are expected to
257      * use this thus: <code>
258      *   this.foo = prepareForAssignment(this.foo, foo);
259      *   </code>
260      * 
261      * This method will do a (null) safe compare of the objects and will also invalidate the DOM if appropriate
262      * 
263      * @param <T> type of object being compared and assigned
264      * @param oldValue current value
265      * @param newValue proposed new value
266      * 
267      * @return The value to assign to the saved Object.
268      */
269     protected <T extends XMLObject> T prepareForAssignment(T oldValue, T newValue) {
270 
271         if (newValue != null && newValue.hasParent()) {
272             throw new IllegalArgumentException(newValue.getClass().getName()
273                     + " cannot be added - it is already the child of another SAML Object");
274         }
275 
276         if (oldValue == null) {
277             if (newValue != null) {
278                 releaseThisandParentDOM();
279                 newValue.setParent(this);
280                 idIndex.registerIDMappings(newValue.getIDIndex());
281                 return newValue;
282 
283             } else {
284                 return null;
285             }
286         }
287 
288         if (!oldValue.equals(newValue)) {
289             oldValue.setParent(null);
290             releaseThisandParentDOM();
291             idIndex.deregisterIDMappings(oldValue.getIDIndex());
292             if (newValue != null) {
293                 newValue.setParent(this);
294                 idIndex.registerIDMappings(newValue.getIDIndex());
295             }
296         }
297 
298         return newValue;
299     }
300 
301     /**
302      * A helper function for derived classes. The mutator/setter method for any ID-typed attributes should call this
303      * method in order to handle getting the old value removed from the ID-to-XMLObject mapping, and the new value added
304      * to the mapping.
305      * 
306      * @param oldID the old value of the ID-typed attribute
307      * @param newID the new value of the ID-typed attribute
308      */
309     protected void registerOwnID(String oldID, String newID) {
310         String newString = DatatypeHelper.safeTrimOrNullString(newID);
311 
312         if (!DatatypeHelper.safeEquals(oldID, newString)) {
313             if (oldID != null) {
314                 idIndex.deregisterIDMapping(oldID);
315             }
316 
317             if (newString != null) {
318                 idIndex.registerIDMapping(newString, this);
319             }
320         }
321     }
322 
323     /** {@inheritDoc} */
324     public void releaseChildrenDOM(boolean propagateRelease) {
325         log.trace("Releasing cached DOM reprsentation for children of {} with propagation set to {}",
326                 getElementQName(), propagateRelease);
327         if (getOrderedChildren() != null) {
328             for (XMLObject child : getOrderedChildren()) {
329                 if (child != null) {
330                     child.releaseDOM();
331                     if (propagateRelease) {
332                         child.releaseChildrenDOM(propagateRelease);
333                     }
334                 }
335             }
336         }
337     }
338 
339     /** {@inheritDoc} */
340     public void releaseDOM() {
341         log.trace("Releasing cached DOM reprsentation for {}", getElementQName());
342         setDOM(null);
343     }
344 
345     /** {@inheritDoc} */
346     public void releaseParentDOM(boolean propagateRelease) {
347         log.trace("Releasing cached DOM reprsentation for parent of {} with propagation set to {}", getElementQName(),
348                 propagateRelease);
349         XMLObject parentElement = getParent();
350         if (parentElement != null) {
351             parent.releaseDOM();
352             if (propagateRelease) {
353                 parent.releaseParentDOM(propagateRelease);
354             }
355         }
356     }
357 
358     /**
359      * A convience method that is equal to calling {@link #releaseDOM()} then {@link #releaseChildrenDOM(boolean)} with
360      * the release being propogated.
361      */
362     public void releaseThisAndChildrenDOM() {
363         if (getDOM() != null) {
364             releaseDOM();
365             releaseChildrenDOM(true);
366         }
367     }
368 
369     /**
370      * A convience method that is equal to calling {@link #releaseDOM()} then {@link #releaseParentDOM(boolean)} with
371      * the release being propogated.
372      */
373     public void releaseThisandParentDOM() {
374         if (getDOM() != null) {
375             releaseDOM();
376             releaseParentDOM(true);
377         }
378     }
379 
380     /** {@inheritDoc} */
381     public void removeNamespace(Namespace namespace) {
382         namespaces.remove(namespace);
383     }
384 
385     /** {@inheritDoc} */
386     public XMLObject resolveID(String id) {
387         return idIndex.lookup(id);
388     }
389 
390     /** {@inheritDoc} */
391     public XMLObject resolveIDFromRoot(String id) {
392         XMLObject root = this;
393         while (root.hasParent()) {
394             root = root.getParent();
395         }
396         return root.resolveID(id);
397     }
398 
399     /** {@inheritDoc} */
400     public void setDOM(Element newDom) {
401         dom = newDom;
402     }
403 
404     /**
405      * Sets the prefix for this element's namespace.
406      * 
407      * @param prefix the prefix for this element's namespace
408      */
409     public void setElementNamespacePrefix(String prefix) {
410         if (prefix == null) {
411             elementQname = new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart());
412         } else {
413             elementQname = new QName(elementQname.getNamespaceURI(), elementQname.getLocalPart(), prefix);
414         }
415     }
416 
417     /**
418      * Sets the element QName.
419      * 
420      * @param elementQName the element's QName
421      */
422     protected void setElementQName(QName elementQName) {
423         this.elementQname = XMLHelper.constructQName(elementQName.getNamespaceURI(), elementQName.getLocalPart(),
424                 elementQName.getPrefix());
425         addNamespace(new Namespace(elementQName.getNamespaceURI(), elementQName.getLocalPart()));
426     }
427 
428     /** {@inheritDoc} */
429     public void setNoNamespaceSchemaLocation(String location) {
430         noNamespaceSchemaLocation = DatatypeHelper.safeTrimOrNullString(location);
431     }
432 
433     /** {@inheritDoc} */
434     public void setParent(XMLObject newParent) {
435         parent = newParent;
436     }
437 
438     /** {@inheritDoc} */
439     public void setSchemaLocation(String location) {
440         schemaLocation = DatatypeHelper.safeTrimOrNullString(location);
441     }
442 
443     /**
444      * Sets a given QName as the schema type for the Element represented by this XMLObject. This will add the namespace
445      * to the list of namespaces scoped for this XMLObject. It will not remove any namespaces, for example, if there is
446      * already a schema type set and null is passed in.
447      * 
448      * @param type the schema type
449      */
450     protected void setSchemaType(QName type) {
451         if (type == null) {
452             typeQname = null;
453         } else {
454             typeQname = type;
455             addNamespace(new Namespace(type.getNamespaceURI(), type.getPrefix()));
456         }
457     }
458 
459 }