1 /*
2  * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved.
3  */
4 /*
5  * Licensed to the Apache Software Foundation (ASF) under one or more
6  * contributor license agreements.  See the NOTICE file distributed with
7  * this work for additional information regarding copyright ownership.
8  * The ASF licenses this file to You under the Apache License, Version 2.0
9  * (the "License"); you may not use this file except in compliance with
10  * the License.  You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  */
20 
21 package com.sun.org.apache.xerces.internal.dom;
22 
23 import java.io.IOException;
24 import java.io.ObjectInputStream;
25 import java.io.ObjectOutputStream;
26 import java.io.Serializable;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.Vector;
30 import org.w3c.dom.DOMException;
31 import org.w3c.dom.NamedNodeMap;
32 import org.w3c.dom.Node;
33 
34 /**
35  * NamedNodeMaps represent collections of Nodes that can be accessed
36  * by name. Entity and Notation nodes are stored in NamedNodeMaps
37  * attached to the DocumentType. Attributes are placed in a NamedNodeMap
38  * attached to the elem they're related too. However, because attributes
39  * require more work, such as firing mutation events, they are stored in
40  * a subclass of NamedNodeMapImpl.
41  * <P>
42  * Only one Node may be stored per name; attempting to
43  * store another will replace the previous value.
44  * <P>
45  * NOTE: The "primary" storage key is taken from the NodeName attribute of the
46  * node. The "secondary" storage key is the namespaceURI and localName, when
47  * accessed by DOM level 2 nodes. All nodes, even DOM Level 2 nodes are stored
48  * in a single Vector sorted by the primary "nodename" key.
49  * <P>
50  * NOTE: item()'s integer index does _not_ imply that the named nodes
51  * must be stored in an array; that's only an access method. Note too
52  * that these indices are "live"; if someone changes the map's
53  * contents, the indices associated with nodes may change.
54  * <P>
55  *
56  * @xerces.internal
57  *
58  * @since  PR-DOM-Level-1-19980818.
59  * @LastModified: Jan 2018
60  */
61 public class NamedNodeMapImpl
62     implements NamedNodeMap, Serializable {
63 
64     //
65     // Constants
66     //
67 
68     /** Serialization version. */
69     static final long serialVersionUID = -7039242451046758020L;
70 
71     //
72     // Data
73     //
74 
75     protected short flags;
76 
77     protected final static short READONLY     = 0x1<<0;
78     protected final static short CHANGED      = 0x1<<1;
79     protected final static short HASDEFAULTS  = 0x1<<2;
80 
81     /** Nodes. */
82     protected List<Node> nodes;
83 
84     protected NodeImpl ownerNode; // the node this map belongs to
85 
86     //
87     // Constructors
88     //
89 
90     /** Constructs a named node map. */
NamedNodeMapImpl(NodeImpl ownerNode)91     protected NamedNodeMapImpl(NodeImpl ownerNode) {
92         this.ownerNode = ownerNode;
93     }
94 
95     //
96     // NamedNodeMap methods
97     //
98 
99     /**
100      * Report how many nodes are currently stored in this NamedNodeMap.
101      * Caveat: This is a count rather than an index, so the
102      * highest-numbered node at any time can be accessed via
103      * item(getLength()-1).
104      */
getLength()105     public int getLength() {
106         return (nodes != null) ? nodes.size() : 0;
107     }
108 
109     /**
110      * Retrieve an item from the map by 0-based index.
111      *
112      * @param index Which item to retrieve. Note that indices are just an
113      * enumeration of the current contents; they aren't guaranteed to be
114      * stable, nor do they imply any promises about the order of the
115      * NamedNodeMap's contents. In other words, DO NOT assume either that
116      * index(i) will always refer to the same entry, or that there is any
117      * stable ordering of entries... and be prepared for double-reporting
118      * or skips as insertion and deletion occur.
119      *
120      * @return the node which currenly has the specified index, or null if index
121      * is greater than or equal to getLength().
122      */
item(int index)123     public Node item(int index) {
124         return (nodes != null && index < nodes.size()) ? (nodes.get(index)) : null;
125     }
126 
127     /**
128      * Retrieve a node by name.
129      *
130      * @param name Name of a node to look up.
131      * @return the Node (of unspecified sub-class) stored with that name, or
132      * null if no value has been assigned to that name.
133      */
getNamedItem(String name)134     public Node getNamedItem(String name) {
135 
136         int i = findNamePoint(name,0);
137         return (i < 0) ? null : (nodes.get(i));
138 
139     } // getNamedItem(String):Node
140 
141     /**
142      * Introduced in DOM Level 2. <p>
143      * Retrieves a node specified by local name and namespace URI.
144      *
145      * @param namespaceURI  The namespace URI of the node to retrieve.
146      *                      When it is null or an empty string, this
147      *                      method behaves like getNamedItem.
148      * @param localName     The local name of the node to retrieve.
149      * @return Node         A Node (of any type) with the specified name, or null if the specified
150      *                      name did not identify any node in the map.
151      */
getNamedItemNS(String namespaceURI, String localName)152     public Node getNamedItemNS(String namespaceURI, String localName) {
153 
154         int i = findNamePoint(namespaceURI, localName);
155         return (i < 0) ? null : (nodes.get(i));
156 
157     } // getNamedItemNS(String,String):Node
158 
159     /**
160      * Adds a node using its nodeName attribute.
161      * As the nodeName attribute is used to derive the name which the node must be
162      * stored under, multiple nodes of certain types (those that have a "special" string
163      * value) cannot be stored as the names would clash. This is seen as preferable to
164      * allowing nodes to be aliased.
165      * @see org.w3c.dom.NamedNodeMap#setNamedItem
166      * @return If the new Node replaces an existing node the replaced Node is returned,
167      *      otherwise null is returned.
168      * @param arg
169      *      A node to store in a named node map. The node will later be
170      *      accessible using the value of the namespaceURI and localName
171      *      attribute of the node. If a node with those namespace URI and
172      *      local name is already present in the map, it is replaced by the new
173      *      one.
174      * @exception org.w3c.dom.DOMException The exception description.
175      */
setNamedItem(Node arg)176     public Node setNamedItem(Node arg)
177     throws DOMException {
178 
179         CoreDocumentImpl ownerDocument = ownerNode.ownerDocument();
180         if (ownerDocument.errorChecking) {
181             if (isReadOnly()) {
182                 String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
183                 throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
184             }
185             if (arg.getOwnerDocument() != ownerDocument) {
186                 String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "WRONG_DOCUMENT_ERR", null);
187                 throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, msg);
188             }
189         }
190 
191         int i = findNamePoint(arg.getNodeName(),0);
192         NodeImpl previous = null;
193         if (i >= 0) {
194             previous = (NodeImpl) nodes.get(i);
195             nodes.set(i, arg);
196         } else {
197             i = -1 - i; // Insert point (may be end of list)
198             if (null == nodes) {
199                 nodes = new ArrayList<>(5);
200             }
201             nodes.add(i, arg);
202         }
203         return previous;
204 
205     } // setNamedItem(Node):Node
206 
207     /**
208      * Adds a node using its namespaceURI and localName.
209      * @see org.w3c.dom.NamedNodeMap#setNamedItem
210      * @return If the new Node replaces an existing node the replaced Node is returned,
211      *      otherwise null is returned.
212      * @param arg A node to store in a named node map. The node will later be
213      *      accessible using the value of the namespaceURI and localName
214      *      attribute of the node. If a node with those namespace URI and
215      *      local name is already present in the map, it is replaced by the new
216      *      one.
217      */
setNamedItemNS(Node arg)218     public Node setNamedItemNS(Node arg)
219     throws DOMException {
220 
221         CoreDocumentImpl ownerDocument = ownerNode.ownerDocument();
222         if (ownerDocument.errorChecking) {
223             if (isReadOnly()) {
224                 String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
225                 throw new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR, msg);
226             }
227 
228             if(arg.getOwnerDocument() != ownerDocument) {
229                 String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "WRONG_DOCUMENT_ERR", null);
230                 throw new DOMException(DOMException.WRONG_DOCUMENT_ERR, msg);
231             }
232         }
233 
234         int i = findNamePoint(arg.getNamespaceURI(), arg.getLocalName());
235         NodeImpl previous = null;
236         if (i >= 0) {
237             previous = (NodeImpl) nodes.get(i);
238             nodes.set(i, arg);
239         } else {
240             // If we can't find by namespaceURI, localName, then we find by
241             // nodeName so we know where to insert.
242             i = findNamePoint(arg.getNodeName(),0);
243             if (i >= 0) {
244                 previous = (NodeImpl) nodes.get(i);
245                 nodes.add(i, arg);
246             } else {
247                 i = -1 - i; // Insert point (may be end of list)
248                 if (null == nodes) {
249                     nodes = new ArrayList<>(5);
250                 }
251                 nodes.add(i, arg);
252             }
253         }
254         return previous;
255 
256     } // setNamedItemNS(Node):Node
257 
258     /**
259      * Removes a node specified by name.
260      * @param name The name of a node to remove.
261      * @return The node removed from the map if a node with such a name exists.
262      */
263     /***/
removeNamedItem(String name)264     public Node removeNamedItem(String name)
265         throws DOMException {
266 
267         if (isReadOnly()) {
268             String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
269             throw
270                 new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
271                 msg);
272         }
273         int i = findNamePoint(name,0);
274         if (i < 0) {
275             String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NOT_FOUND_ERR", null);
276             throw new DOMException(DOMException.NOT_FOUND_ERR, msg);
277         }
278 
279         NodeImpl n = (NodeImpl)nodes.get(i);
280         nodes.remove(i);
281 
282         return n;
283 
284     } // removeNamedItem(String):Node
285 
286     /**
287      * Introduced in DOM Level 2. <p>
288      * Removes a node specified by local name and namespace URI.
289      * @param namespaceURI
290      *                      The namespace URI of the node to remove.
291      *                      When it is null or an empty string, this
292      *                      method behaves like removeNamedItem.
293      * @param name          The local name of the node to remove.
294      * @return Node         The node removed from the map if a node with such
295      *                      a local name and namespace URI exists.
296      * @throws              NOT_FOUND_ERR: Raised if there is no node named
297      *                      name in the map.
298 
299      */
removeNamedItemNS(String namespaceURI, String name)300      public Node removeNamedItemNS(String namespaceURI, String name)
301         throws DOMException {
302 
303         if (isReadOnly()) {
304             String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NO_MODIFICATION_ALLOWED_ERR", null);
305             throw
306                 new DOMException(DOMException.NO_MODIFICATION_ALLOWED_ERR,
307                 msg);
308         }
309         int i = findNamePoint(namespaceURI, name);
310         if (i < 0) {
311             String msg = DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN, "NOT_FOUND_ERR", null);
312             throw new DOMException(DOMException.NOT_FOUND_ERR, msg);
313         }
314 
315         NodeImpl n = (NodeImpl)nodes.get(i);
316         nodes.remove(i);
317 
318         return n;
319 
320     } // removeNamedItem(String):Node
321 
322     //
323     // Public methods
324     //
325 
326     /**
327      * Cloning a NamedNodeMap is a DEEP OPERATION; it always clones
328      * all the nodes contained in the map.
329      */
330 
cloneMap(NodeImpl ownerNode)331     public NamedNodeMapImpl cloneMap(NodeImpl ownerNode) {
332         NamedNodeMapImpl newmap = new NamedNodeMapImpl(ownerNode);
333         newmap.cloneContent(this);
334         return newmap;
335     }
336 
cloneContent(NamedNodeMapImpl srcmap)337     protected void cloneContent(NamedNodeMapImpl srcmap) {
338         List<Node> srcnodes = srcmap.nodes;
339         if (srcnodes != null) {
340             int size = srcnodes.size();
341             if (size != 0) {
342                 if (nodes == null) {
343                     nodes = new ArrayList<>(size);
344                 }
345                 else {
346                     nodes.clear();
347                 }
348                 for (int i = 0; i < size; ++i) {
349                     NodeImpl n = (NodeImpl) srcmap.nodes.get(i);
350                     NodeImpl clone = (NodeImpl) n.cloneNode(true);
351                     clone.isSpecified(n.isSpecified());
352                     nodes.add(clone);
353                 }
354             }
355         }
356     } // cloneMap():NamedNodeMapImpl
357 
358     //
359     // Package methods
360     //
361 
362     /**
363      * Internal subroutine to allow read-only Nodes to make their contained
364      * NamedNodeMaps readonly too. I expect that in fact the shallow
365      * version of this operation will never be
366      *
367      * @param readOnly boolean true to make read-only, false to permit editing.
368      * @param deep boolean true to pass this request along to the contained
369      * nodes, false to only toggle the NamedNodeMap itself. I expect that
370      * the shallow version of this operation will never be used, but I want
371      * to design it in now, while I'm thinking about it.
372      */
setReadOnly(boolean readOnly, boolean deep)373     void setReadOnly(boolean readOnly, boolean deep) {
374         isReadOnly(readOnly);
375         if (deep && nodes != null) {
376             for (int i = nodes.size() - 1; i >= 0; i--) {
377                 ((NodeImpl) nodes.get(i)).setReadOnly(readOnly,deep);
378             }
379         }
380     } // setReadOnly(boolean,boolean)
381 
382     /**
383      * Internal subroutine returns this NodeNameMap's (shallow) readOnly value.
384      *
385      */
getReadOnly()386     boolean getReadOnly() {
387         return isReadOnly();
388     } // getReadOnly()
389 
390 
391     //
392     // Protected methods
393     //
394 
395     /**
396      * NON-DOM
397      * set the ownerDocument of this node, and the attributes it contains
398      */
setOwnerDocument(CoreDocumentImpl doc)399     protected void setOwnerDocument(CoreDocumentImpl doc) {
400         if (nodes != null) {
401             final int size = nodes.size();
402             for (int i = 0; i < size; ++i) {
403                 ((NodeImpl)item(i)).setOwnerDocument(doc);
404             }
405         }
406     }
407 
isReadOnly()408     final boolean isReadOnly() {
409         return (flags & READONLY) != 0;
410     }
411 
isReadOnly(boolean value)412     final void isReadOnly(boolean value) {
413         flags = (short) (value ? flags | READONLY : flags & ~READONLY);
414     }
415 
changed()416     final boolean changed() {
417         return (flags & CHANGED) != 0;
418     }
419 
changed(boolean value)420     final void changed(boolean value) {
421         flags = (short) (value ? flags | CHANGED : flags & ~CHANGED);
422     }
423 
hasDefaults()424     final boolean hasDefaults() {
425         return (flags & HASDEFAULTS) != 0;
426     }
427 
hasDefaults(boolean value)428     final void hasDefaults(boolean value) {
429         flags = (short) (value ? flags | HASDEFAULTS : flags & ~HASDEFAULTS);
430     }
431 
432     //
433     // Private methods
434     //
435 
436     /**
437      * Subroutine: Locate the named item, or the point at which said item
438      * should be added.
439      *
440      * @param name Name of a node to look up.
441      *
442      * @return If positive or zero, the index of the found item.
443      * If negative, index of the appropriate point at which to insert
444      * the item, encoded as -1-index and hence reconvertable by subtracting
445      * it from -1. (Encoding because I don't want to recompare the strings
446      * but don't want to burn bytes on a datatype to hold a flagged value.)
447      */
findNamePoint(String name, int start)448     protected int findNamePoint(String name, int start) {
449 
450         // Binary search
451         int i = 0;
452         if (nodes != null) {
453             int first = start;
454             int last  = nodes.size() - 1;
455 
456             while (first <= last) {
457                 i = (first + last) / 2;
458                 int test = name.compareTo(((nodes.get(i))).getNodeName());
459                 if (test == 0) {
460                     return i; // Name found
461                 }
462                 else if (test < 0) {
463                     last = i - 1;
464                 }
465                 else {
466                     first = i + 1;
467                 }
468             }
469 
470             if (first > i) {
471                 i = first;
472             }
473         }
474 
475         return -1 - i; // not-found has to be encoded.
476 
477     } // findNamePoint(String):int
478 
479 
480     /** This findNamePoint is for DOM Level 2 Namespaces.
481      */
findNamePoint(String namespaceURI, String name)482     protected int findNamePoint(String namespaceURI, String name) {
483 
484         if (nodes == null) return -1;
485         if (name == null) return -1;
486 
487         // This is a linear search through the same nodes ArrayList.
488         // The ArrayList is sorted on the DOM Level 1 nodename.
489         // The DOM Level 2 NS keys are namespaceURI and Localname,
490         // so we must linear search thru it.
491         // In addition, to get this to work with nodes without any namespace
492         // (namespaceURI and localNames are both null) we then use the nodeName
493         // as a secondary key.
494         final int size = nodes.size();
495         for (int i = 0; i < size; ++i) {
496             NodeImpl a = (NodeImpl)nodes.get(i);
497             String aNamespaceURI = a.getNamespaceURI();
498             String aLocalName = a.getLocalName();
499             if (namespaceURI == null) {
500               if (aNamespaceURI == null
501                   &&
502                   (name.equals(aLocalName)
503                    ||
504                    (aLocalName == null && name.equals(a.getNodeName()))))
505                 return i;
506             } else {
507               if (namespaceURI.equals(aNamespaceURI)
508                   &&
509                   name.equals(aLocalName))
510                 return i;
511             }
512         }
513         return -1;
514     }
515 
516     // compare 2 nodes in the map.  If a precedes b, return true, otherwise
517     // return false
precedes(Node a, Node b)518     protected boolean precedes(Node a, Node b) {
519 
520         if (nodes != null) {
521             final int size = nodes.size();
522             for (int i = 0; i < size; ++i) {
523                 Node n = nodes.get(i);
524                 if (n==a) return true;
525                 if (n==b) return false;
526             }
527         }
528         return false;
529     }
530 
531 
532     /**
533       * NON-DOM: Remove attribute at specified index
534       */
removeItem(int index)535     protected void removeItem(int index) {
536        if (nodes != null && index < nodes.size()){
537            nodes.remove(index);
538        }
539     }
540 
541 
getItem(int index)542     protected Object getItem (int index){
543         if (nodes != null) {
544             return nodes.get(index);
545         }
546         return null;
547     }
548 
addItem(Node arg)549     protected int addItem (Node arg) {
550         int i = findNamePoint(arg.getNamespaceURI(), arg.getLocalName());
551         if (i >= 0) {
552             nodes.set(i, arg);
553         }
554         else {
555             // If we can't find by namespaceURI, localName, then we find by
556             // nodeName so we know where to insert.
557             i = findNamePoint(arg.getNodeName(),0);
558             if (i >= 0) {
559                 nodes.add(i, arg);
560             }
561             else {
562                 i = -1 - i; // Insert point (may be end of list)
563                 if (null == nodes) {
564                     nodes = new ArrayList<>(5);
565                 }
566                 nodes.add(i, arg);
567             }
568         }
569         return i;
570     }
571 
572     /**
573      * NON-DOM: copy content of this map into the specified ArrayList
574      *
575      * @param list   ArrayList to copy information into.
576      * @return A copy of this node named map
577      */
cloneMap(List<Node> list)578     protected List<Node> cloneMap(List<Node> list) {
579         if (nodes != null) {
580             list = new ArrayList<>(nodes);
581         }
582         return list;
583     }
584 
getNamedItemIndex(String namespaceURI, String localName)585      protected int getNamedItemIndex(String namespaceURI, String localName) {
586         return findNamePoint(namespaceURI, localName);
587      }
588 
589     /**
590       * NON-DOM remove all elements from this map
591       */
removeAll()592     public void removeAll (){
593         if (nodes != null) {
594             nodes.clear();
595         }
596     }
597 
598     @SuppressWarnings("unchecked")
readObject(ObjectInputStream in)599     private void readObject(ObjectInputStream in)
600         throws IOException, ClassNotFoundException {
601         in.defaultReadObject();
602         if (nodes != null) {
603             // nodes are written as a Vector for compatibility.
604             // cast to Vector is required
605             nodes = new ArrayList<>((Vector<Node>)nodes);
606         }
607     }
608 
writeObject(ObjectOutputStream out)609     private void writeObject(ObjectOutputStream out) throws IOException {
610         List<Node> oldNodes = this.nodes;
611         try {
612             if (oldNodes != null) {
613                 // convert to Vector for backward-compatibility
614                 this.nodes = new Vector<>(oldNodes);
615             }
616             out.defaultWriteObject();
617         }
618         // If the write fails for some reason ensure
619         // that we restore the original object.
620         finally {
621             this.nodes = oldNodes;
622         }
623     }
624 
625 } // class NamedNodeMapImpl
626