1
2
3
4
5
6
7
8
9
10
11
12 package sk.uniba.euromath.document;
13 import java.util.Collections;
14 import java.util.EnumSet;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.Map;
18 import java.util.Set;
19 import java.util.StringTokenizer;
20 import javax.xml.XMLConstants;
21 import org.apache.commons.lang.StringUtils;
22 import org.w3c.dom.Attr;
23 import org.w3c.dom.Document;
24 import org.w3c.dom.Element;
25 import org.w3c.dom.EntityReference;
26 import org.w3c.dom.NamedNodeMap;
27 import org.w3c.dom.Node;
28 import org.w3c.dom.events.Event;
29 import org.w3c.dom.events.EventListener;
30 import org.w3c.dom.events.EventTarget;
31 import org.w3c.dom.events.MutationEvent;
32 import org.w3c.dom.traversal.DocumentTraversal;
33 import org.w3c.dom.traversal.NodeFilter;
34 import org.w3c.dom.traversal.NodeIterator;
35 import sk.baka.ikslibs.DOMUtils;
36 import sk.baka.ikslibs.EventClassEnum;
37 import sk.baka.ikslibs.EventTypesEnum;
38 import sk.baka.ikslibs.INodeObserver;
39 import sk.baka.ikslibs.UnremovableNodeException;
40 import sk.baka.ikslibs.modify.DOMMutils;
41 import sk.baka.xml.gene.ExportUtils;
42 import sk.uniba.euromath.config.EuromathConfig;
43 import sk.uniba.euromath.config.bind.NamespaceType;
44 import sk.uniba.euromath.document.lang.Messages;
45 /***
46 * <p>
47 * Manages prefixes in given document. Upon constructing, namespace prefixes are
48 * unified by following algorithm:
49 * </p>
50 * <ul>
51 * <li>Prefix for all namespace definitions are remembered, one for each
52 * namespace definition (by the <code>xmlns</code> attribute). First prefix
53 * definition is taken.</li>
54 * <li>If mapping defines no prefix for non-empty namespace (for example,
55 * <code>xmlns="foo:bar"</code>), then this prefix definition is not
56 * remembered. Instead, next prefix definition is remembered.</li>
57 * <li>All <code>xmlns:foo=""</code> definitions are processed as if the
58 * <code>xmlns=""</code> was encountered - empty namespaces are thus mapped to
59 * empty prefix.</li>
60 * <li>Then, all <code>xmlns</code> definitions are removed.</li>
61 * <li>All namespaces, that doesn't have prefix defined, are given new prefix,
62 * generated via <code>getPreferredPrefix()</code> function.</li>
63 * <li>Then, all element's prefixes are set, so following is true: any two
64 * elements, that have same namespace, has same prefix.</li>
65 * </ul>
66 * <p>
67 * As a result, document (except descendants of entity reference nodes) is
68 * stripped of <code>xmlns</code> attributes.
69 * </p>
70 * <p>
71 * Thread unsafe.
72 * </p>
73 * <p>
74 * Even when all elements/attributes from some namespaces are removed the
75 * mapping is not deleted automatically. It is removed when the manager is
76 * queried for all known namespaces, using {@link #getAllNamespaces()},
77 * {@link #getCounts()} or {@link #getElementNamespaces()} methods.
78 * </p>
79 * @author Martin Vysny
80 */
81 public final class NamespaceManager implements INodeObserver {
82 /***
83 * Returns preferred prefix for given namespace due to the emConfig.
84 * @param namespaceUri return preferred prefix for this namespace.
85 * @return preferred prefix from configFile, or <code>null</code> if none
86 * had been found.
87 */
88 private static String getPreferredPrefix(String namespaceUri) {
89 NamespaceType ns = EuromathConfig.getNamespace(namespaceUri);
90 if (ns == null) {
91
92 return null;
93 }
94 return ns.getDefaultPrefix();
95 }
96 /***
97 * Reference to document.
98 */
99 private final Document doc;
100 /***
101 * Maps namespace URI to prefix name. Must be consistent with
102 * <code>mapPrefixToNs</code> map.
103 */
104 private final Map<String, String> mapNsToPrefix = new HashMap<String, String>();
105 /***
106 * Maps prefix to namespace URI. Must be consistent with
107 * <code>mapNsToPrefix</code> map.
108 */
109 private final Map<String, String> mapPrefixToNs = new HashMap<String, String>();
110 /***
111 * <p>
112 * Contains all namespaces present in the document. This may contain more
113 * namespaces than the two maps because entity reference may introduce new
114 * namespace and that is not prefix-normalized. It is always superset of
115 * <code>mapNsToPrefix.keySet()</code>.
116 * </p>
117 * <p>
118 * Maps namespace to a counter that counts elements and attributes with
119 * given namespace. The counter may be zero only when namespace is declared
120 * but no elements/attributes with the namespace are present in the
121 * document. In this case, when
122 */
123 private final Map<String, NodeCount> allNamespaces = new HashMap<String, NodeCount>();
124 /***
125 * Class holding elements and attributes count. Mutable.
126 * @author Martin Vysny
127 */
128 public static final class NodeCount {
129 /***
130 * Count of <code>Element</code> nodes.
131 */
132 public int elements = 0;
133 /***
134 * Count of <code>Attribute</code> nodes.
135 */
136 public int attributes = 0;
137 /***
138 * Constructs zero counter.
139 */
140 public NodeCount() {
141 super();
142 }
143 /***
144 * Copy-constructor.
145 * @param other copies the object.
146 */
147 public NodeCount(final NodeCount other) {
148 this();
149 elements = other.elements;
150 attributes = other.attributes;
151 }
152 /***
153 * Returns count of all nodes.
154 * @return sum of <code>elements</code> and <code>attributes</code>.
155 */
156 public int getCount() {
157 assert elements >= 0 && attributes >= 0;
158 return elements + attributes;
159 }
160
161
162
163
164 @Override
165 public boolean equals(Object obj) {
166 if (this == obj)
167 return true;
168 if (!(obj instanceof NodeCount))
169 return false;
170 final NodeCount that = (NodeCount) obj;
171 return that.attributes == attributes && that.elements == elements;
172 }
173
174
175
176
177 @Override
178 public int hashCode() {
179 return attributes * 2407 + elements;
180 }
181
182
183
184
185 @Override
186 public String toString() {
187 return "e#" + elements + ";a#" + attributes;
188 }
189 }
190 /***
191 * Parent namespace mananger.
192 */
193 private final NamespaceManager parent;
194 /***
195 * Creates an empty namespace manager.
196 * @param doc the document. It is not unified when using this constructor.
197 * @param parent parent namespace manager.
198 */
199 private NamespaceManager(Document doc, NamespaceManager parent) {
200 super();
201 this.doc = doc;
202 this.parent = parent;
203 }
204 /***
205 * Constructs a namespace manager, and modifies the document by unificating
206 * all namespace prefixes.
207 * @param doc document, that will be managed.
208 * @throws DocumentException if an unremovable xmlns attribute occured.
209 */
210 NamespaceManager(Document doc) throws DocumentException {
211 this(doc, null);
212
213
214 if (doc.getDocumentElement() != null) {
215 Map<String, String> lastPrefixName = new HashMap<String, String>();
216 try {
217
218
219
220 mapPrefixes(lastPrefixName);
221
222
223
224 unifyPrefixes(lastPrefixName);
225 } catch (UnremovableNodeException ex) {
226 throw new DocumentException(ex);
227 }
228 }
229 }
230 /***
231 * Constructs a namespace manager, that is a child of given namespace
232 * manager. Document is not modified, changes to this namespace manager are
233 * not displayed in parent manager.
234 * @param parent parent namespace manager.
235 */
236 public NamespaceManager(NamespaceManager parent) {
237 this(parent.doc, parent);
238 }
239 /***
240 * Get map of all namespaces and their prefixes. When colliding prefix is
241 * encountered, it is not processed by this method. Removes all
242 * <code>xmlns</code> attributes also.
243 * @param lastPrefixName maps namespaceURI to prefix name, and serves for
244 * remembering of last wanted prefix name.
245 * @throws UnremovableNodeException if an unremovable xmlns attribute
246 * occured.
247 */
248 private void mapPrefixes(Map<String, String> lastPrefixName)
249 throws DocumentException {
250 NodeIterator iterator = ((DocumentTraversal) doc)
251 .createNodeIterator(doc.getDocumentElement(),
252 NodeFilter.SHOW_ELEMENT, null, false);
253 for (Element actNode = (Element) iterator.nextNode(); actNode != null; actNode = (Element) iterator
254 .nextNode()) {
255 checkEMReserved(actNode);
256 final boolean inEntity = DOMUtils.isInEntity(actNode);
257 final String namespace = StringUtils.defaultString(actNode
258 .getNamespaceURI());
259 final String prefix = actNode.getPrefix();
260 if (DOMUtils.equalsURI(namespace, null)) {
261 addNode(actNode, inEntity);
262 } else {
263 if (processPrefix(namespace, prefix, lastPrefixName)) {
264 addNode(actNode, inEntity);
265 }
266 }
267
268 DOMMutils.removeXmlns(actNode);
269
270 final NamedNodeMap attrs = actNode.getAttributes();
271 if (attrs != null) {
272 for (int i = 0; i < attrs.getLength(); i++) {
273 final Attr a = (Attr) attrs.item(i);
274 checkEMReserved(a);
275
276
277 if (DOMUtils.equalsURI(a.getNamespaceURI(), namespace))
278 throw new DocumentException(
279 Messages
280 .getString(
281 "ELEMENT_ATTR_NS_EQUALS_ERROR", DOMUtils.printQName(actNode), DOMUtils.printQName(a)));
282
283 if (isUnifiable(a)) {
284 if (processPrefix(StringUtils.defaultString(a
285 .getNamespaceURI()), a.getPrefix(),
286 lastPrefixName)) {
287 addNode(a, inEntity);
288 }
289 }
290 }
291 }
292 }
293 }
294 /***
295 * Process pair of namespace;prefix. If this prefix is empty, equal to
296 * reserved XML or <code>emp</code> prefix or already defined, then it is
297 * not added to <code>mapNsToPrefix</code> and <code>mapPrefixToNs</code>.
298 * @param namespaceUri the namespace. Must not be empty string.
299 * @param prefix desired prefix.
300 * @param lastPrefixName if prefix is already used, then it is stored here
301 * for later processing.
302 * @return <code>true</code> if the prefix must be registered immediately,
303 * <code>false</code> if it will be registered in second pass.
304 */
305 private boolean processPrefix(String namespaceUri, String prefix,
306 Map<String, String> lastPrefixName) {
307
308
309
310 if (!DOMUtils.equalsURI(namespaceUri, null)
311 && !DOMUtils.equalsURI(namespaceUri, ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI())
312 && !mapNsToPrefix.containsKey(namespaceUri)
313 && !isReservedPrefix(prefix)) {
314
315
316
317
318 if ((!StringUtils.isEmpty(prefix))
319 && (!prefix.equals(ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getPrefix()))
320 && !isReservedPrefix(prefix)) {
321 if (mapPrefixToNs.containsKey(prefix)) {
322
323
324
325 lastPrefixName.put(namespaceUri, prefix);
326 } else {
327 return true;
328 }
329 }
330 }
331 return false;
332 }
333 /***
334 * Unifies the prefixes. First, it completes the prefix map and set, and
335 * then unifies prefixes by replacing them with one prefix for each
336 * namespace.
337 * @param lastPrefixName helps picking correct prefix name, if needed.
338 */
339 private void unifyPrefixes(Map<String, String> lastPrefixName) {
340 NodeIterator iterator = ((DocumentTraversal) doc)
341 .createNodeIterator(doc.getDocumentElement(),
342 NodeFilter.SHOW_ELEMENT
343 | NodeFilter.SHOW_ENTITY_REFERENCE, null, false);
344 for (Node actNode = iterator.nextNode(); actNode != null; actNode = iterator
345 .nextNode()) {
346 if (actNode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
347 addEntityNamespaces((EntityReference) actNode);
348 continue;
349 }
350
351 unifyPrefix(actNode, lastPrefixName);
352 NamedNodeMap attrs = actNode.getAttributes();
353 for (int i = 0; i < attrs.getLength(); i++) {
354 Attr attr = (Attr) attrs.item(i);
355 unifyPrefix(attr, lastPrefixName);
356 }
357 }
358 iterator.detach();
359 }
360 /***
361 * Unifies prefix, and sets it to node, if required.
362 * @param node node to unify
363 * @param lastPrefixName helps picking correct prefix name, if needed.
364 */
365 private void unifyPrefix(Node node, Map<String, String> lastPrefixName) {
366
367
368 if (!isUnifiable(node))
369 return;
370 final String namespace = StringUtils.defaultString(node.getNamespaceURI());
371 if (!DOMUtils.equalsURI(namespace, ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI())) {
372 if (!DOMUtils.equalsURI(namespace, null)) {
373
374
375 String prefix = mapNsToPrefix.get(namespace);
376
377 if (prefix == null) {
378 prefix = getAlternativePrefix(namespace, lastPrefixName);
379 }
380 if (!prefix.equals(node.getPrefix()))
381 node.setPrefix(prefix);
382 } else {
383 String prefix = node.getPrefix();
384 if (!StringUtils.isEmpty(prefix))
385 node.setPrefix(null);
386 }
387
388 addNode(node, DOMUtils.isInEntity(node));
389 }
390 }
391 /***
392 * Registers a node into <code>mapNsToPrefix</code>,
393 * <code>mapPrefixToNs</code> and <code>allNamespaces</code>. Function
394 * fails if a namespace is already registered to a prefix and the prefix
395 * does not match.
396 * @param node node to register, element or attribute node only. Ignores
397 * child nodes.
398 * @param inEntity if <code>true</code> then the node is not registered in
399 * <code>mapNsToPrefix</code> and <code>mapPrefixToNs</code> maps.
400 */
401 private void addNode(final Node node, final boolean inEntity) {
402 final short nodeType = node.getNodeType();
403 assert (nodeType == Node.ATTRIBUTE_NODE)
404 || (nodeType == Node.ELEMENT_NODE) : "Not an element/attribute node";
405 final String ns = StringUtils.defaultString(node.getNamespaceURI());
406 addMapping(inEntity, ns, node.getPrefix());
407
408 final NodeCount cnt = allNamespaces.get(ns);
409 if (nodeType == Node.ELEMENT_NODE)
410 cnt.elements++;
411 else
412 cnt.attributes++;
413 }
414 /***
415 * Registers a mapping into <code>mapNsToPrefix</code>,
416 * <code>mapPrefixToNs</code> and <code>allNamespaces</code>. Function
417 * fails if a namespace is already registered to a prefix and the prefix
418 * does not match.
419 * @param inEntity if <code>true</code> then the node is not registered in
420 * <code>mapNsToPrefix</code> and <code>mapPrefixToNs</code> maps.
421 * @param namespace the namespace
422 * @param prefix prefix.
423 */
424 private void addMapping(final boolean inEntity, String namespace,
425 String prefix) {
426 namespace = StringUtils.defaultString(namespace);
427 prefix = StringUtils.defaultString(prefix);
428 if (!inEntity) {
429 final String registeredPrefix = mapNsToPrefix.get(namespace);
430 if (registeredPrefix != null) {
431
432 if (!registeredPrefix.equals(prefix))
433 throw new IllegalArgumentException("New prefix '" + prefix
434 + "' does not match registered prefix '"
435 + registeredPrefix + "' for namespace ["
436 + namespace + "]");
437 assert mapPrefixToNs.get(registeredPrefix) != null : "mapPrefixToNs inconsistent with mapNsToPrefix";
438 assert mapPrefixToNs.get(registeredPrefix).equals(namespace) : "mapPrefixToNs inconsistent with mapNsToPrefix";
439 } else {
440
441 if (DOMUtils.equalsURI(namespace, null) && !prefix.equals(""))
442 throw new IllegalArgumentException(
443 "Null namespace must map to null prefix.");
444 if (!DOMUtils.equalsURI(namespace, null) && prefix.equals(""))
445 throw new IllegalArgumentException(
446 "Non-null namespace must map to non-null prefix.");
447 assert !mapPrefixToNs.containsKey(prefix) : "mapPrefixToNs inconsistent with mapNsToPrefix";
448 mapNsToPrefix.put(namespace, prefix);
449 mapPrefixToNs.put(prefix, namespace);
450 }
451 }
452 if (!allNamespaces.containsKey(namespace))
453 allNamespaces.put(namespace, new NodeCount());
454 }
455 /***
456 * Unregisters a node from <code>mapNsToPrefix</code>,
457 * <code>mapPrefixToNs</code> and <code>allNamespaces</code>, but only
458 * if no more nodes with same namespace exists. Function fails if a
459 * namespace is not registered.
460 * @param node node to unregister, element or attribute node only. Ignores
461 * child nodes.
462 * @param inEntity if <code>true</code> then the node is not unregistered
463 * from <code>mapNsToPrefix</code> and <code>mapPrefixToNs</code> maps.
464 */
465 private void removeNode(final Node node, final boolean inEntity) {
466 final short nodeType = node.getNodeType();
467 assert (nodeType == Node.ATTRIBUTE_NODE)
468 || (nodeType == Node.ELEMENT_NODE) : "Not an element/attribute node";
469 final String ns = StringUtils.defaultString(node.getNamespaceURI());
470
471 final NodeCount cnt = allNamespaces.get(ns);
472 if (cnt == null)
473 throw new IllegalArgumentException("Unregistered namespace [" + ns
474 + "]");
475 if (nodeType == Node.ELEMENT_NODE) {
476 cnt.elements--;
477 if (cnt.elements < 0)
478 throw new IllegalArgumentException(
479 "Removing unregistered element");
480 } else {
481 cnt.attributes--;
482 if (cnt.attributes < 0)
483 throw new IllegalArgumentException(
484 "Removing unregistered attribute");
485 }
486
487 if (cnt.getCount() == 0) {
488 removeMapping(ns, inEntity);
489 }
490 }
491 /***
492 * Removes given mapping from <code>mapNsToPrefix</code>,
493 * <code>mapPrefixToNs</code> and <code>allNamespaces</code>. Fails if
494 * the mapping is not yet registered.
495 * @param namespace the namespace to unregister.
496 * @param inEntity if <code>true</code> then the node is not unregistered
497 * from <code>mapNsToPrefix</code> and <code>mapPrefixToNs</code> maps.
498 */
499 private void removeMapping(String namespace, final boolean inEntity) {
500 namespace = StringUtils.defaultString(namespace);
501
502 final NodeCount cnt = allNamespaces.get(namespace);
503 if (cnt == null)
504 throw new IllegalArgumentException("Unknown namespace: ["
505 + namespace + "]");
506 if (cnt.getCount() != 0)
507 throw new IllegalStateException("There are still some nodes for ["
508 + namespace + "]");
509 if (!inEntity) {
510
511 final String prefix = mapNsToPrefix.get(namespace);
512 if (prefix != null) {
513 assert mapPrefixToNs.get(prefix).equals(namespace) : "maps are inconsistent";
514 mapNsToPrefix.remove(namespace);
515 mapPrefixToNs.remove(prefix);
516 }
517 }
518 allNamespaces.remove(namespace);
519 }
520 /***
521 * <p>
522 * Finds alternative prefix for given namespace. Alternate prefix is
523 * returned by these rules:
524 * <ol>
525 * <li>Prefix defined in the emConfig file, if defined.</li>
526 * <li>lastPrefixName, if defined.</li>
527 * <li>Last path element from uri path.</li>
528 * <li>The string <code>"nsp"</code>, meaning namespace prefix.</li>
529 * </ol>
530 * </p>
531 * <p>
532 * If returned prefix is already used, then some number is added to prefix
533 * name, until unique prefix name is found.
534 * </p>
535 * @param namespaceUri namespace, for which the prefix will be created. Must
536 * not be <code>null</code> nor empty string.
537 * @param lastPrefixName last used prefix name for this namespace. If
538 * <code>null</code>, second step is skipped.
539 * @return unique prefix, never <code>null</code> nor empty string.
540 */
541 private String getAlternativePrefix(String namespaceUri,
542 Map<String, String> lastPrefixName) {
543 String result = NamespaceManager.getPreferredPrefix(namespaceUri);
544 if (!isValidPrefix(result))
545 result = lastPrefixName == null ? null : lastPrefixName
546 .get(namespaceUri);
547 if (!isValidPrefix(result))
548 result = getLastPathElement(namespaceUri);
549 if (!isValidPrefix(result))
550 result = "nsp";
551
552
553 if (!mapPrefixToNs.containsKey(result))
554 return result;
555 int additionalNumber = 2;
556 String newPrefix;
557 do {
558 newPrefix = result + Integer.toString(additionalNumber++);
559 } while (mapPrefixToNs.containsKey(newPrefix));
560 return newPrefix;
561 }
562 /***
563 * Checks whether the given node can be subject to prefix unification. If
564 * node must not be unified then it must NOT require its namespace to be
565 * defined. Currently these prefixes are not unifiable: xml reserved
566 * prefixes starting with the string <code>xml</code> and <code>emp</code>
567 * prefix.
568 * @param node node to check.
569 * @return true if this node prefix-namespace pair can be unified.
570 */
571 private boolean isUnifiable(Node node) {
572 if (isReserved(node))
573 return false;
574 if (DOMUtils.equalsURI(node.getNamespaceURI(), ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI()))
575 return false;
576 return true;
577 }
578 /***
579 * Checks, if given prefix name may be placed safely into the xml file. It
580 * doesn't check if this prefix is already used, it just check that it isn't
581 * reserved <code>emp:</code> prefix, and it doesn't start with reserved
582 * <code>xml</code> prefix.
583 * @param prefix prefix to check.
584 * @return true if this prefix is assignable to a normal node.
585 */
586 private boolean isValidPrefix(String prefix) {
587 if (prefix == null)
588 return false;
589 if (prefix.equals(ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getPrefix()))
590 return false;
591 return !isReservedPrefix(prefix);
592 }
593 /***
594 * Checks, if given prefix name is reserved for XML use.
595 * @param prefix prefix to check.
596 * @return true if prefix is reserved.
597 */
598 private boolean isReservedPrefix(String prefix) {
599 if (prefix == null)
600 return false;
601 if (prefix.length() < 3)
602 return false;
603 if (prefix.substring(0, 3).equalsIgnoreCase(XMLConstants.XML_NS_PREFIX))
604 return true;
605 return false;
606 }
607 /***
608 * Checks, if given node is reserved for XML use.
609 * @param node node to check.
610 * @return true if this prefix is syntactically placeable into xml.
611 */
612 private boolean isReserved(Node node) {
613
614 if (node.getNodeType() != Node.ATTRIBUTE_NODE)
615 return false;
616 return isReservedPrefix(node.getPrefix())
617 || DOMUtils.isXmlns((Attr)node);
618 }
619 /***
620 * Checks if given node namespace is reserved by EuroMath2.
621 * @param node node to check.
622 * @return <code>true</code> if namespace is reserved and can not be used
623 * by users.
624 */
625 private boolean isEMReserved(Node node) {
626 if (node.getNamespaceURI() == null)
627 return false;
628 return DOMUtils.equalsURI(node.getNamespaceURI(),ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI());
629 }
630 /***
631 * Checks if given node namespace is reserved by EuroMath2.
632 * @param node node to check
633 * @throws DocumentException if reserved node is found. <code>emp:id</code>
634 * is ignored - no exception is thrown.
635 */
636 private void checkEMReserved(Node node) throws DocumentException {
637 if (DOMUtils.equalsNodeQName(node, ExportUtils.GENE_ID_ATTRIBUTE_QNAME))
638 return;
639 if (isEMReserved(node)) {
640 throw new DocumentException(
641 Messages
642 .getString(
643 "EM_RESERVED_NAMESPACE_FOUND", node.getNodeName(), node.getNamespaceURI()));
644 }
645 }
646 /***
647 * Returns last longest path element, that doesn't contain whitespace, nor
648 * one of the following: <code>: / > < ;</code>.
649 * @param uri uri to parse.
650 * @return last path element, or <code>null</code> if it doesn't exist.
651 */
652 private String getLastPathElement(String uri) {
653 StringTokenizer i = new StringTokenizer(uri, "\t\n\r\f/:<>;");
654 String result = null;
655 while (i.hasMoreTokens()) {
656 result = i.nextToken();
657 }
658 return result;
659 }
660 /***
661 * For each used namespace, except empty namespace, creates xmlns mapping.
662 * Erases all other xmlns definitions.
663 * @param e element, where the <code>xmlns</code> attributes will be
664 * re-created.
665 */
666 public void createXmlnsAttributes(Element e) {
667 DOMMutils.removeXmlns(e);
668 createXmlnsAttributes(e, mapNsToPrefix.keySet());
669 }
670 /***
671 * Adds and/or replaces xmlns mapping for all given namespaces in given
672 * element.
673 * @param e the element.
674 * @param namespaces Set of Strings. All these namespaces' mappings are
675 * added to element.
676 */
677 public void createXmlnsAttributes(Element e,
678 Set< ? extends String> namespaces) {
679 for (String namespace : namespaces) {
680 Attr attr = createXmlnsAttribute(e, namespace);
681 if (attr != null)
682 e.setAttributeNodeNS(attr);
683 }
684 }
685 /***
686 * <p>
687 * Returns all namespaces known to this manager. Returned set cannot be
688 * modified in any way. <code>null</code> namespace is represented as
689 * empty string (<code>""</code>), and always maps to empty (
690 * <code>""</code>) namespace. It never contains the EuroMath namespace,
691 * xmlns namespace nor xml namespace.
692 * </p>
693 * <p>
694 * All mappings of namespaces no longer present in the document are removed,
695 * but only for root manager.
696 * </p>
697 * @return all namespaces in document.
698 */
699 public Set<String> getAllNamespaces() {
700 if (parent == null)
701 cleanMappings();
702 return getAllNamespacesInt();
703 }
704 /***
705 * Returns namespaces of all elements present in the document.Returned set
706 * cannot be modified in any way. <code>null</code> namespace is
707 * represented as empty string (<code>""</code>), and always maps to
708 * empty ( <code>""</code>) namespace. It never contains the EuroMath
709 * namespace, xmlns namespace nor xml namespace.
710 * </p>
711 * <p>
712 * All mappings of namespaces no longer present in the document are removed.
713 * </p>
714 * @return set of all namespaces present in elements.
715 */
716 public Set<String> getElementNamespaces() {
717 cleanMappings();
718 final Set<String> result = new HashSet<String>();
719 if (parent != null)
720 result.addAll(parent.getElementNamespaces());
721
722 for (final String namespace : allNamespaces.keySet()) {
723 final NodeCount nc = allNamespaces.get(namespace);
724 if (nc.elements != 0)
725 result.add(namespace);
726 }
727 return result;
728 }
729 /***
730 * Returns all namespaces known to this manager. Returned set cannot be
731 * modified in any way. <code>null</code> namespace is represented as
732 * empty string (<code>""</code>), and always maps to empty (
733 * <code>""</code>) namespace. It never contains the EuroMath namespace,
734 * null namespace, xmlns namespace nor xml namespace.
735 * @return all namespaces in document.
736 */
737 private Set<String> getAllNamespacesInt() {
738 if (parent == null)
739 return Collections.unmodifiableSet(allNamespaces.keySet());
740 final Set<String> result = new HashSet<String>(parent
741 .getAllNamespaces());
742 result.addAll(allNamespaces.keySet());
743 return Collections.unmodifiableSet(result);
744 }
745 /***
746 * <p>
747 * Returns counts of nodes for each namespace. Clones internal map,
748 * therefore the result may be cached when used multiple times.
749 * </p>
750 * <p>
751 * All mappings of namespaces no longer present in the document are removed.
752 * </p>
753 * @return maps namespace URI to a count of nodes with given namespace.
754 * <code>null</code> namespace is represented as an empty string.
755 */
756 public Map<String, NodeCount> getCounts() {
757 cleanMappings();
758 final Map<String, NodeCount> result;
759 if (parent == null) {
760 result = new HashMap<String, NodeCount>(allNamespaces.size());
761 } else {
762 result = parent.getCounts();
763 }
764
765 for (final String ns : allNamespaces.keySet()) {
766 if (!result.containsKey(ns)) {
767 final NodeCount cnt = allNamespaces.get(ns);
768 result.put(ns, new NodeCount(cnt));
769 }
770 }
771 return result;
772 }
773 /***
774 * Removes all mappings of namespaces not present in the document. Cleans
775 * only mappings registered in this instance of manager, it does not clean
776 * parent manager.
777 */
778 public void cleanMappings() {
779
780 final Set<String> obsolete = new HashSet<String>();
781 for (final String ns : allNamespaces.keySet()) {
782 final NodeCount cnt = allNamespaces.get(ns);
783 if (cnt.getCount() == 0)
784 obsolete.add(ns);
785 }
786
787 for (final String ns : obsolete) {
788 removeMapping(ns, false);
789 }
790 }
791 /***
792 * <p>
793 * When creating new element/attribute from another namespace as their
794 * parent, we can specify new prefix. This function returns the best
795 * suggestion of new prefix.
796 * </p>
797 * <p>
798 * However, you can ask for every element/attribute's prefix, but there are
799 * cases, when this prefix is not changeable.
800 * </p>
801 * <p>
802 * Warning: when creating attributes with same namespace as their owner
803 * element (local namespaces), their prefix AND namespace MUST be null.
804 * </p>
805 * @param namespaceUri namespace of new nametree being created.
806 * @return best prefix name. If no prefix is the best choice, then
807 * <code>null</code> is returned.
808 */
809 public String getBestPrefix(String namespaceUri) {
810 if (StringUtils.isEmpty(namespaceUri))
811 return null;
812 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XML_NS_URI))
813 return XMLConstants.XML_NS_PREFIX;
814 String result = getPrefix(namespaceUri);
815 if (result != null)
816 return result;
817 return getAlternativePrefix(namespaceUri, null);
818 }
819 /***
820 * Checks, if the prefix, returned by the <code>getBestPrefix()</code>
821 * function, is only the best choice for new prefix, or if returned prefix
822 * MUST be used.
823 * @param namespaceUri namespace to check.
824 * @return false, if user can choose virtually any prefix, that is accepted
825 * by the <code>acceptsNewPrefix()</code> function. True, if only one
826 * prefix can be used.
827 */
828 public boolean isBestPrefixRequired(String namespaceUri) {
829 if (StringUtils.isEmpty(namespaceUri))
830 return true;
831 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XML_NS_URI))
832 return true;
833 return getPrefix(namespaceUri) != null;
834 }
835 /***
836 * When creating new element/attribute, check, whether chosen prefix can be
837 * used.
838 * @param namespaceUri namespace.
839 * @param newPrefix desired prefix. When no prefix is desired,
840 * <code>null</code> must be specified. Only <code>null</code> namespace
841 * may have <code>null</code> prefix.
842 * @return true, if this prefix can be set to new element/attribute, false
843 * if not. The function will not check if the prefix contains illegal
844 * characters, it merely checks if there is no other registered prefix with
845 * same name.
846 */
847 public boolean acceptsNewPrefix(String namespaceUri, String newPrefix) {
848 if (StringUtils.isEmpty(namespaceUri))
849 return StringUtils.isEmpty(newPrefix);
850 if (StringUtils.isEmpty(newPrefix))
851 return false;
852 if (DOMUtils.equalsURI(namespaceUri, ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI()))
853 return false;
854 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XML_NS_URI))
855 return XMLConstants.XML_NS_PREFIX.equals(newPrefix);
856
857
858 String prefix = getPrefix(namespaceUri);
859 if (prefix != null)
860 return prefix.equals(newPrefix);
861
862
863 String ns = getNamespace(newPrefix);
864 if (ns != null)
865 return false;
866 return true;
867 }
868 /***
869 * Creates an <code>xmlns</code> attribute, that maps known namespace to
870 * registered prefix. EuroMath namespace is always mapped to
871 * <code>emp:</code> prefix. When an empty namespace is given, function
872 * returns <code>null</code>: empty namespace is always mapped to empty
873 * prefix, and thus need not to be declared by the <code>xmlns:</code>
874 * attribute. When an XML namespace is given then <code>null</code> is
875 * returned aswell.
876 * @param e element, where to create element.
877 * @param namespaceUri creates xmlns mapping for this namespace.
878 * @return xmlns attribute node, or <code>null</code> if no prefix is
879 * currently mapped to given namespace.
880 */
881 public Attr createXmlnsAttribute(Element e, String namespaceUri) {
882 if (StringUtils.isEmpty(namespaceUri))
883 return null;
884 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XMLNS_ATTRIBUTE_NS_URI))
885 return null;
886 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XML_NS_URI))
887 return null;
888 final String prefix = getPrefix(namespaceUri);
889 if (prefix == null)
890 return null;
891 final Attr result = DOMMutils.createXmlnsAttribute(e,namespaceUri,prefix);
892 return result;
893 }
894 /***
895 * Returns prefix, that is currently mapped to given namespace. If no prefix
896 * is currently mapped, function returns <code>null</code>. GENE-ID
897 * namespace is always mapped to <code>gene:</code> prefix. Empty or
898 * <code>null</code> namespace ("") is always mapped to empty prefix ("").
899 * XML namespace is always mapped to <code>xml</code> prefix.
900 * @param namespaceUri namespace.
901 * @return prefix, that maps to given namespace. If no prefix is currently
902 * known for given namespace, it return <code>null</code>.
903 */
904 public String getPrefix(String namespaceUri) {
905 if (DOMUtils.equalsURI(ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI(), namespaceUri) )
906 return ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getPrefix();
907 if (StringUtils.isEmpty(namespaceUri))
908 return "";
909 if (DOMUtils.equalsURI(namespaceUri, XMLConstants.XML_NS_URI))
910 return XMLConstants.XML_NS_PREFIX;
911 if (parent != null) {
912 final String result = parent.getPrefix(namespaceUri);
913 if (result != null)
914 return result;
915 }
916 return mapNsToPrefix.get(namespaceUri);
917 }
918 /***
919 * Returns namespace, that is currently mapped to given prefix. If no
920 * namespace is currently mapped, function returns <code>null</code>.
921 * EuroMath namespace is always mapped to <code>emp:</code> prefix. Empty
922 * or <code>null</code> prefix is always mapped to empty namespace ("").
923 * @param prefix the prefix.
924 * @return namespace, that maps to given prefix. If no namespace is
925 * currently known for given prefix, it returns <code>null</code>.
926 */
927 public String getNamespace(String prefix) {
928 if (StringUtils.isEmpty(prefix))
929 return "";
930 if (prefix.equals(ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getPrefix()))
931 return ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI();
932 if (parent != null) {
933 final String result = parent.getNamespace(prefix);
934 if (result != null)
935 return result;
936 }
937 return mapPrefixToNs.get(StringUtils.defaultString(prefix));
938 }
939 /***
940 * Checks, if given namespace is <strong>defined</strong> in given element.
941 * For empty namespace always returns true.
942 * @param namespaceUri namespace
943 * @param e element
944 * @return <code>true</code> if this namespace is defined in given element
945 * with the <code>xmlns:</code> attribute, or if a prefix is registered.
946 */
947 public boolean isDefined(String namespaceUri, Element e) {
948 if (StringUtils.isEmpty(namespaceUri))
949 return true;
950 final String prefix = getPrefix(namespaceUri);
951 if (prefix == null)
952 return false;
953 return DOMUtils.getXmlnsNode(prefix, e)!=null;
954 }
955 /***
956 * Declares the namespace-prefix pair. This operation is not applicable to a
957 * default namespace manager - it is updated automatically when the document
958 * is modified. Neither the namespace, nor the prefix can exist in a parent.
959 * @param namespaceUri the namespace.
960 * @param prefix the prefix.
961 * @throws IllegalArgumentException if such prefix-namespace pair cannot be
962 * registered.
963 */
964 public void declare(String namespaceUri, String prefix) {
965 if (parent == null)
966 throw new IllegalStateException(
967 "This operation is not applicable to default namespace manager.");
968 if (StringUtils.isEmpty(namespaceUri)) {
969 if (!StringUtils.isEmpty(prefix))
970 throw new IllegalArgumentException(
971 "Prefix name must be null when namespace is null.");
972 addMapping(false, namespaceUri, prefix);
973 }
974 if (StringUtils.isEmpty(prefix))
975 throw new IllegalArgumentException("Invalid prefix value.");
976 if (parent != null) {
977 if (parent.getPrefix(namespaceUri) != null)
978 throw new IllegalArgumentException(
979 "Namespace is already declared in parent.");
980 if (parent.getNamespace(prefix) != null)
981 throw new IllegalArgumentException(
982 "Prefix is already declared in parent.");
983 }
984 addMapping(false, namespaceUri, prefix);
985 }
986 /***
987 * Undeclares given namespace. If the namespace is not declared in this
988 * manager then the function will fail - we cannot modify parent.
989 * @param namespaceUri the namespace part of pair.
990 */
991 public void undeclareNamespace(String namespaceUri) {
992 if (parent == null)
993 throw new IllegalStateException(
994 "This operation is not applicable to default namespace manager.");
995 if (StringUtils.isEmpty(namespaceUri))
996 throw new IllegalArgumentException(
997 "Null namespace cannot be undeclared.");
998 String prefix = mapNsToPrefix.get(namespaceUri);
999 if (prefix == null)
1000 throw new IllegalArgumentException(
1001 "The namespace is not defined in this manager.");
1002 removeMapping(namespaceUri, false);
1003 }
1004
1005
1006
1007
1008 @Override
1009 public String toString() {
1010 final StringBuilder builder = new StringBuilder();
1011 for (String ns : getAllNamespacesInt()) {
1012 final String prefix = getPrefix(ns);
1013 builder.append("[" + prefix + "=" + ns + "] ");
1014 }
1015 return builder.toString();
1016 }
1017 /***
1018 * Listens for changes in the document.
1019 * @author Martin Vysny
1020 */
1021 private final class ModifierListener implements EventListener {
1022
1023
1024
1025
1026 public void handleEvent(Event evt) {
1027 final EventTypesEnum eventType = EventTypesEnum.fromEvent(evt);
1028 if (eventType.getEventClass() != EventClassEnum.Mutation)
1029 return;
1030 final MutationEvent e = (MutationEvent) evt;
1031 if (EventClassEnum.Mutation.isAddition(e)) {
1032 nodeAdded(EventClassEnum.Mutation.getMutatedNode(e));
1033 return;
1034 }
1035 if (EventClassEnum.Mutation.isModification(e)) {
1036 nodeModified(e);
1037 return;
1038 }
1039 if (EventClassEnum.Mutation.isRemoval(e)) {
1040 nodeDelete(EventClassEnum.Mutation.getMutatedNode(e));
1041 return;
1042 }
1043 }
1044 private void nodeAdded(Node node) {
1045
1046 if (parent != null)
1047 throw new IllegalStateException(
1048 "This method can be called on main manager only");
1049 if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
1050 final boolean inEntity = DOMUtils.isInEntity(((Attr) node)
1051 .getOwnerElement());
1052 attributeAdded((Attr) node, inEntity);
1053 return;
1054 }
1055 if ((node.getNodeType() != Node.ELEMENT_NODE)
1056 && (node.getNodeType() != Node.ENTITY_REFERENCE_NODE))
1057 return;
1058
1059
1060 NodeIterator iterator = ((DocumentTraversal) doc)
1061 .createNodeIterator(node, NodeFilter.SHOW_ELEMENT
1062 | NodeFilter.SHOW_ENTITY_REFERENCE, null, false);
1063 for (Node e = iterator.nextNode(); e != null; e = iterator
1064 .nextNode()) {
1065 if (e.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
1066 addEntityNamespaces((EntityReference) e);
1067 continue;
1068 }
1069 final boolean inEntity = DOMUtils.isInEntity(e);
1070 final String ns = StringUtils.defaultString(e.getNamespaceURI());
1071 final String validPrefix = mapNsToPrefix.get(ns);
1072 if (validPrefix != null) {
1073
1074 if (!validPrefix.equals(e.getPrefix()))
1075 throw new IllegalArgumentException("Element " + e
1076 + " must have prefix " + validPrefix);
1077 } else {
1078
1079 if (isBestPrefixRequired(ns)) {
1080 String forcedPrefix = getBestPrefix(ns);
1081 if (!forcedPrefix.equals(e.getPrefix()))
1082 throw new IllegalArgumentException("Element " + e
1083 + " must have prefix " + forcedPrefix);
1084 }
1085 addNode(e, inEntity);
1086 }
1087
1088 NamedNodeMap attrs = e.getAttributes();
1089 if (attrs != null) {
1090 for (int i = 0; i < attrs.getLength(); i++) {
1091 Attr attr = (Attr) attrs.item(i);
1092 attributeAdded(attr, inEntity);
1093 }
1094 }
1095 }
1096 iterator.detach();
1097 }
1098 /***
1099 * Informs that the attribute was added.
1100 * @param attr the attribute that was added.
1101 * @param inEntity if <code>true</code> then owner element resides in
1102 * an entity.
1103 */
1104 private void attributeAdded(final Attr attr, final boolean inEntity) {
1105
1106 if (DOMUtils.equalsURI(attr.getNamespaceURI(), ExportUtils.GENE_ID_ATTRIBUTE_QNAME.getNamespaceURI())
1107 || isReserved(attr))
1108 return;
1109 addNode(attr, inEntity);
1110 }
1111 private void nodeDelete(Node node) {
1112 if (parent != null)
1113 throw new IllegalStateException(
1114 "This method can be called on main manager only");
1115
1116 nodeDeleteInt(node, DOMUtils.isInEntity(node));
1117 }
1118 private void nodeModified(MutationEvent e) {
1119 if (parent != null)
1120 throw new IllegalStateException(
1121 "This method can be called on main manager only");
1122
1123 }
1124 }
1125 /***
1126 * The modeifier listener instance.
1127 */
1128 final EventListener modifierListener = new ModifierListener();
1129 /***
1130 * Walks through the descendants of the entity and adds all namespaces found
1131 * in the entity. The entity is not modified - prefixes are not unified.
1132 * @param eRef the entity to check.
1133 */
1134 private void addEntityNamespaces(EntityReference eRef) {
1135 for (Node child = eRef.getFirstChild(); child != null; child = child
1136 .getNextSibling()) {
1137 if (child.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
1138 addEntityNamespaces((EntityReference) child);
1139 continue;
1140 }
1141 if (child.getNodeType() != Node.ELEMENT_NODE)
1142 continue;
1143
1144
1145 NodeIterator iterator = ((DocumentTraversal) doc)
1146 .createNodeIterator(child, NodeFilter.SHOW_ELEMENT
1147 | NodeFilter.SHOW_ENTITY_REFERENCE, null, false);
1148 for (Node e = iterator.nextNode(); e != null; e = iterator
1149 .nextNode()) {
1150 if (e.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
1151 addEntityNamespaces((EntityReference) e);
1152 continue;
1153 }
1154
1155 if (isUnifiable(e)) {
1156 addNode(e, true);
1157 }
1158
1159 NamedNodeMap attrs = e.getAttributes();
1160 if (attrs != null) {
1161 for (int i = 0; i < attrs.getLength(); i++) {
1162 Attr attr = (Attr) attrs.item(i);
1163 if (!StringUtils.isEmpty(attr.getNamespaceURI())
1164 && isUnifiable(attr)) {
1165 addNode(attr, true);
1166 }
1167 }
1168 }
1169 }
1170 iterator.detach();
1171 }
1172 }
1173 /***
1174 * Calls {@link #removeNode(Node, boolean)} on each descendant node,
1175 * including given node.
1176 * @param node node to unmap
1177 * @param inEntity if <code>true</code> then the node resides in an
1178 * entity.
1179 */
1180 private void nodeDeleteInt(final Node node, final boolean inEntity) {
1181 switch (node.getNodeType()) {
1182 case Node.ENTITY_REFERENCE_NODE: {
1183 for (Node child = node.getFirstChild(); child != null; child = child
1184 .getNextSibling()) {
1185 nodeDeleteInt(child, true);
1186 }
1187 }
1188 break;
1189 case Node.ATTRIBUTE_NODE: {
1190 if (isUnifiable(node))
1191 removeNode(node, inEntity);
1192 }
1193 break;
1194 case Node.ELEMENT_NODE: {
1195 if (isUnifiable(node))
1196 removeNode(node, inEntity);
1197 for (Node child = node.getFirstChild(); child != null; child = child
1198 .getNextSibling()) {
1199 nodeDeleteInt(child, inEntity);
1200 }
1201 final NamedNodeMap atts = node.getAttributes();
1202 if (atts != null) {
1203 for (int i = 0; i < atts.getLength(); i++) {
1204 nodeDeleteInt(atts.item(i), inEntity);
1205 }
1206 }
1207 }
1208 break;
1209 }
1210 }
1211
1212
1213
1214 public void observe(Node node) {
1215 final EventTarget et = (EventTarget) node;
1216 EventTypesEnum.addEventListenerTo(et, modifierListener, true, EnumSet.of(
1217 EventTypesEnum.MAttrModified,
1218 EventTypesEnum.MCharacterDataModified,
1219 EventTypesEnum.MNodeInserted, EventTypesEnum.MNodeRemoved));
1220 }
1221
1222
1223
1224 public void stopObservation(Node node) {
1225 final EventTarget et = (EventTarget) node;
1226 EventTypesEnum.removeEventListenerFrom(et, modifierListener, true, EnumSet.of(
1227 EventTypesEnum.MAttrModified,
1228 EventTypesEnum.MCharacterDataModified,
1229 EventTypesEnum.MNodeInserted, EventTypesEnum.MNodeRemoved));
1230 }
1231
1232 }