View Javadoc

1   /*
2    * Copyright [2006] [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.util;
18  
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.Map;
22  import java.util.Set;
23  
24  import javax.xml.namespace.QName;
25  
26  import net.jcip.annotations.NotThreadSafe;
27  
28  import org.opensaml.xml.Configuration;
29  import org.opensaml.xml.NamespaceManager;
30  import org.opensaml.xml.XMLObject;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * A map of attribute names and attribute values that invalidates the DOM of the attribute owning XMLObject when the
36   * attributes change.
37   * 
38   * <strong>Note:</strong> 
39   */
40  @NotThreadSafe
41  public class AttributeMap implements Map<QName, String> {
42      
43      /** Logger. */
44      private final Logger log = LoggerFactory.getLogger(AttributeMap.class);
45  
46      /** XMLObject owning the attributes. */
47      private XMLObject attributeOwner;
48  
49      /** Map of attributes. */
50      private Map<QName, String> attributes;
51      
52      /** Set of attribute QNames which have been locally registered as having an ID type within this 
53       * AttributeMap instance. */
54      private Set<QName> idAttribNames;
55      
56      /** Set of attribute QNames which have been locally registered as having an QName value type within this 
57       * AttributeMap instance. */
58      private Set<QName> qnameAttribNames;
59      
60      /** Flag indicating whether an attempt should be made to infer QName values, 
61       * if attribute is not registered as a QName type. */
62      private boolean inferQNameValues;
63  
64      /**
65       * Constructor.
66       *
67       * @param newOwner the XMLObject that owns these attributes
68       * 
69       * @throws NullPointerException thrown if the given XMLObject is null
70       */
71      public AttributeMap(XMLObject newOwner) throws NullPointerException {
72          if (newOwner == null) {
73              throw new NullPointerException("Attribute owner XMLObject may not be null");
74          }
75  
76          attributeOwner = newOwner;
77          attributes = new LazyMap<QName, String>();
78          idAttribNames = new LazySet<QName>();
79          qnameAttribNames = new LazySet<QName>();
80      }
81  
82      /** {@inheritDoc} */
83      public String put(QName attributeName, String value) {
84          String oldValue = get(attributeName);
85          if (value != oldValue) {
86              releaseDOM();
87              attributes.put(attributeName, value);
88              if (isIDAttribute(attributeName) || Configuration.isIDAttribute(attributeName)) {
89                  attributeOwner.getIDIndex().deregisterIDMapping(oldValue);
90                  attributeOwner.getIDIndex().registerIDMapping(value, attributeOwner);
91              }
92              if (!DatatypeHelper.isEmpty(attributeName.getNamespaceURI())) {
93                  if (value == null) {
94                      attributeOwner.getNamespaceManager().deregisterAttributeName(attributeName);
95                  } else {
96                      attributeOwner.getNamespaceManager().registerAttributeName(attributeName);
97                  }
98              }
99              checkAndDeregisterQNameValue(attributeName, oldValue);
100             checkAndRegisterQNameValue(attributeName, value);
101         }
102         
103         return oldValue;
104     }
105     
106     /**
107      * Set an attribute value as a QName.  This method takes care of properly registering and 
108      * deregistering the namespace information associated with the new QName being added, and
109      * with the old QName being possibly removed.
110      * 
111      * @param attributeName the attribute name
112      * @param value the QName attribute value
113      * @return the old attribute value, possibly null
114      */
115     public QName put(QName attributeName, QName value) {
116         String oldValueString = get(attributeName);
117         
118         QName oldValue = null;
119         if (!DatatypeHelper.isEmpty(oldValueString)) {
120             oldValue = resolveQName(oldValueString, true);
121         }
122         
123         if (!DatatypeHelper.safeEquals(oldValue, value)) {
124             releaseDOM();
125             if (value != null) {
126                 // new value is not null, old value was either null or non-equal
127                 String newStringValue = constructAttributeValue(value);
128                 attributes.put(attributeName, newStringValue);
129                 registerQNameValue(attributeName, value);
130                 attributeOwner.getNamespaceManager().registerAttributeName(attributeName);
131             } else {
132                 // new value is null, old value was not null
133                 deregisterQNameValue(attributeName);
134                 attributeOwner.getNamespaceManager().deregisterAttributeName(attributeName);
135             }
136         }
137         
138         return oldValue;
139     }
140 
141     /** {@inheritDoc} */
142     public void clear() {
143         LazySet<QName> keys = new LazySet<QName>();
144         keys.addAll(attributes.keySet());
145         for (QName attributeName : keys) {
146             remove(attributeName);
147         }
148     }
149 
150     /**
151      * Returns the set of keys.
152      * 
153      * @return unmodifiable set of keys
154      */
155     public Set<QName> keySet() {
156         return Collections.unmodifiableSet(attributes.keySet());
157     }
158 
159     /** {@inheritDoc} */
160     public int size() {
161         return attributes.size();
162     }
163 
164     /** {@inheritDoc} */
165     public boolean isEmpty() {
166         return attributes.isEmpty();
167     }
168 
169     /** {@inheritDoc} */
170     public boolean containsKey(Object key) {
171         return attributes.containsKey(key);
172     }
173 
174     /** {@inheritDoc} */
175     public boolean containsValue(Object value) {
176         return attributes.containsValue(value);
177     }
178 
179     /** {@inheritDoc} */
180     public String get(Object key) {
181         return attributes.get(key);
182     }
183 
184     /** {@inheritDoc} */
185     public String remove(Object key) {
186         String removedValue = attributes.remove(key);
187         if (removedValue != null) {
188             releaseDOM();
189             QName attributeName = (QName) key;
190             if (isIDAttribute(attributeName) || Configuration.isIDAttribute(attributeName)) {
191                 attributeOwner.getIDIndex().deregisterIDMapping(removedValue);
192             }
193             attributeOwner.getNamespaceManager().deregisterAttributeName(attributeName);
194             checkAndDeregisterQNameValue(attributeName, removedValue);
195         }
196 
197         return removedValue;
198     }
199 
200     /** {@inheritDoc} */
201     public void putAll(Map<? extends QName, ? extends String> t) {
202         if (t != null && t.size() > 0) {
203             for (Entry<? extends QName, ? extends String> entry : t.entrySet()) {
204                 put(entry.getKey(), entry.getValue());
205             }
206         }
207     }
208 
209     /**
210      * Returns the values in this map.
211      * 
212      * @return an unmodifiable collection of values
213      */
214     public Collection<String> values() {
215         return Collections.unmodifiableCollection(attributes.values());
216     }
217 
218     /**
219      * Returns the set of entries.
220      * 
221      * @return unmodifiable set of entries
222      */
223     public Set<Entry<QName, String>> entrySet() {
224         return Collections.unmodifiableSet(attributes.entrySet());
225     }
226     
227     /**
228      * Register an attribute as having a type of ID.
229      * 
230      * @param attributeName the QName of the ID attribute to be registered
231      */
232     public void registerID(QName attributeName) {
233         if (! idAttribNames.contains(attributeName)) {
234             idAttribNames.add(attributeName);
235         }
236         
237         // In case attribute already has a value,
238         // register the current value mapping with the XMLObject owner.
239         if (containsKey(attributeName)) {
240             attributeOwner.getIDIndex().registerIDMapping(get(attributeName), attributeOwner);
241         }
242     }
243     
244     /**
245      * Deregister an attribute as having a type of ID.
246      * 
247      * @param attributeName the QName of the ID attribute to be de-registered
248      */
249     public void deregisterID(QName attributeName) {
250         if (idAttribNames.contains(attributeName)) {
251             idAttribNames.remove(attributeName);
252         }
253         
254         // In case attribute already has a value,
255         // deregister the current value mapping with the XMLObject owner.
256         if (containsKey(attributeName)) {
257             attributeOwner.getIDIndex().deregisterIDMapping(get(attributeName));
258         }
259     }
260     
261     /**
262      * Check whether a given attribute is locally registered as having an ID type within
263      * this AttributeMap instance.
264      * 
265      * @param attributeName the QName of the attribute to be checked for ID type.
266      * @return true if attribute is registered as having an ID type.
267      */
268     public boolean isIDAttribute(QName attributeName) {
269         return idAttribNames.contains(attributeName);
270     }
271     
272     /**
273      * Register an attribute as having a type of QName.
274      * 
275      * @param attributeName the name of the QName-valued attribute to be registered
276      */
277     public void registerQNameAttribute(QName attributeName) {
278         qnameAttribNames.add(attributeName);
279     }
280     
281     /**
282      * Deregister an attribute as having a type of QName.
283      * 
284      * @param attributeName the name of the QName-valued attribute to be registered
285      */
286     public void deregisterQNameAttribute(QName attributeName) {
287         qnameAttribNames.remove(attributeName);
288     }
289     
290     /**
291      * Check whether a given attribute is known to have a QName type.
292      * 
293      * @param attributeName the QName of the attribute to be checked for QName type.
294      * @return true if attribute is registered as having an QName type.
295      */
296     public boolean isQNameAttribute(QName attributeName) {
297         return qnameAttribNames.contains(attributeName);
298     }
299     
300     /**
301      * Get the flag indicating whether an attempt should be made to infer QName values, 
302      * if attribute is not registered via a configuration as a QName type. Default is false.
303      * 
304      * @return true if QName types should be inferred, false if not
305      * 
306      */
307     public boolean isInferQNameValues() {
308         return inferQNameValues;
309     }
310     
311     /**
312      * Set the flag indicating whether an attempt should be made to infer QName values, 
313      * if attribute is not registered via a configuration as a QName type. Default is false.
314      * 
315      * @param flag true if QName types should be inferred, false if not
316      * 
317      */
318     public void setInferQNameValues(boolean flag) {
319         inferQNameValues = flag;
320     }
321     
322     /**
323      * Releases the DOM caching associated XMLObject and its ancestors.
324      */
325     private void releaseDOM() {
326         attributeOwner.releaseDOM();
327         attributeOwner.releaseParentDOM(true);
328     }
329     
330     /**
331      * Check whether the attribute value is a QName type, and if it is,
332      * register it with the owner's namespace manger.
333      * 
334      * @param attributeName the attribute name
335      * @param attributeValue the attribute value
336      */
337     private void checkAndRegisterQNameValue(QName attributeName, String attributeValue) {
338         if (attributeValue == null) {
339             return;
340         }
341         
342         QName qnameValue = checkQName(attributeName, attributeValue);
343         if (qnameValue != null) {
344             log.trace("Attribute '{}' with value '{}' was evaluated to be QName type", 
345                     attributeName, attributeValue);
346             registerQNameValue(attributeName, qnameValue);
347         } else {
348             log.trace("Attribute '{}' with value '{}' was not evaluated to be QName type", 
349                     attributeName, attributeValue);
350         }
351         
352     }
353     
354     /**
355      * Register a QName attribute value with the owner's namespace manger.
356      * 
357      * @param attributeName the attribute name
358      * @param attributeValue the attribute value
359      */
360     private void registerQNameValue(QName attributeName, QName attributeValue) {
361         if (attributeValue == null) {
362             return;
363         }
364         
365         String attributeID = NamespaceManager.generateAttributeID(attributeName);
366         log.trace("Registering QName attribute value '{}' under attibute ID '{}'",
367                 attributeValue, attributeID);
368         attributeOwner.getNamespaceManager().registerAttributeValue(attributeID, attributeValue);
369     }
370     
371     /**
372      * Check whether the attribute value is a QName type, and if it is,
373      * deregister it with the owner's namespace manger.
374      * 
375      * @param attributeName the attribute name
376      * @param attributeValue the attribute value
377      */
378     private void checkAndDeregisterQNameValue(QName attributeName, String attributeValue) {
379         if (attributeValue == null) {
380             return;
381         }
382         
383         QName qnameValue = checkQName(attributeName, attributeValue);
384         if (qnameValue != null) {
385             log.trace("Attribute '{}' with value '{}' was evaluated to be QName type", 
386                     attributeName, attributeValue);
387             deregisterQNameValue(attributeName);
388         } else {
389             log.trace("Attribute '{}' with value '{}' was not evaluated to be QName type", 
390                     attributeName, attributeValue);
391         }
392     }
393     
394     /**
395      * Deregister a QName attribute value with the owner's namespace manger.
396      * 
397      * @param attributeName the attribute name whose QName attribute value should be deregistered
398      */
399     private void deregisterQNameValue(QName attributeName) {
400         String attributeID = NamespaceManager.generateAttributeID(attributeName);
401         log.trace("Deregistering QName attribute with attibute ID '{}'", attributeID);
402         attributeOwner.getNamespaceManager().deregisterAttributeValue(attributeID);
403     }
404     
405     /**
406      * Check where the attribute value is a QName type, and if so, return the QName.
407      * 
408      * @param attributeName the attribute name
409      * @param attributeValue the attribute value
410      * @return the QName if the attribute value is a QName type, otherwise null
411      */
412     private QName checkQName(QName attributeName, String attributeValue) {
413         log.trace("Checking whether attribute '{}' with value {} is a QName type", attributeName, attributeValue);
414         
415         if (attributeValue == null) {
416             log.trace("Attribute value was null, returning null");
417             return null;
418         }
419         
420         if (isQNameAttribute(attributeName)) {
421             log.trace("Configuration indicates attribute with name '{}' is a QName type, resolving value QName", 
422                     attributeName);
423             // Do support the default namespace in this scenario, since we know it should be a QName
424             QName valueName = resolveQName(attributeValue, true);
425             if (valueName != null) {
426                 log.trace("Successfully resolved attribute value to QName: {}", valueName);
427             } else {
428                 log.trace("Could not resolve attribute value to QName, returning null");
429             }
430             return valueName;
431         } else if (isInferQNameValues()) {
432             log.trace("Attempting to infer whether attribute value is a QName");
433             // Do not support the default namespace in this scenario, since we're trying to infer.
434             // Better to fail to resolve than to infer a bogus QName value.
435             QName valueName = resolveQName(attributeValue, false);
436             if (valueName != null) {
437                 log.trace("Resolved attribute as a QName: '{}'", valueName);
438             } else {
439                 log.trace("Attribute value was not resolveable to a QName, returning null");
440             }
441             return valueName;
442         } else {
443             log.trace("Attribute was not registered in configuration as a QName type and QName inference is disabled");
444             return null;
445         }
446 
447     }
448     
449     /**
450      * Attempt to resolve the specified attribute value into a QName.
451      * 
452      * @param attributeValue the value to evaluate
453      * @param isDefaultNSOK flag indicating whether resolution should be attempted if the prefix is null, 
454      *           that is, the value is considered to be be potentially in the default XML namespace
455      * 
456      * @return the QName, or null if unable to resolve into a QName
457      */
458     private QName resolveQName(String attributeValue, boolean isDefaultNSOK) {
459         if (attributeValue == null) {
460             return null;
461         }
462         log.trace("Attemtping to resolve QName from attribute value '{}'", attributeValue);
463         
464         // Attempt to resolve value as a QName by splitting on colon and then attempting to resolve
465         // this candidate prefix into a namespace URI. 
466         String candidatePrefix = null;
467         String localPart = null;
468         int ci = attributeValue.indexOf(':');
469         if (ci > -1) {
470             candidatePrefix = attributeValue.substring(0, ci);
471             log.trace("Evaluating candiate namespace prefix '{}'", candidatePrefix);
472             localPart = attributeValue.substring(ci+1);
473         } else {
474             // No prefix - possibly evaluate as if in the default namespace
475             if (isDefaultNSOK) {
476                 candidatePrefix = null;
477                 log.trace("Value did not contain a colon, evaluating as default namespace");
478                 localPart = attributeValue;
479             } else {
480                 log.trace("Value did not contain a colon, default namespace is disallowed, returning null");
481                 return null;
482             }
483         }
484         
485         log.trace("Evaluated QName local part as '{}'", localPart);
486         
487         String nsURI = XMLObjectHelper.lookupNamespaceURI(attributeOwner, candidatePrefix);
488         log.trace("Resolved namespace URI '{}'", nsURI);
489         if (nsURI != null) {
490             QName name = XMLHelper.constructQName(nsURI, localPart, candidatePrefix);
491             log.trace("Resolved QName '{}'", name);
492             return name;
493         } else {
494             log.trace("Namespace URI for candidate prefix '{}' could not be resolved", candidatePrefix);
495         }
496         
497         log.trace("Value was either not a QName, or namespace URI could not be resolved");
498         
499         return null;
500     }
501     
502     /**
503      * Construct the string representation of a QName attribute value.
504      * 
505      * @param attributeValue the QName to process
506      * @return the attribute value string representation of the QName
507      */
508     private String constructAttributeValue(QName attributeValue) {
509         String trimmedLocalName = DatatypeHelper.safeTrimOrNullString(attributeValue.getLocalPart());
510 
511         if (trimmedLocalName == null) {
512             throw new IllegalArgumentException("Local name may not be null or empty");
513         }
514 
515         String qualifiedName;
516         String trimmedPrefix = DatatypeHelper.safeTrimOrNullString(attributeValue.getPrefix());
517         if (trimmedPrefix != null) {
518             qualifiedName = trimmedPrefix + ":" + DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
519         } else {
520             qualifiedName = DatatypeHelper.safeTrimOrNullString(trimmedLocalName);
521         }
522         return qualifiedName;
523     }
524 
525 }