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