1 /*
   2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
   3  * @LastModified: Oct 2017
   4  */
   5 /*
   6  * Licensed to the Apache Software Foundation (ASF) under one or more
   7  * contributor license agreements.  See the NOTICE file distributed with
   8  * this work for additional information regarding copyright ownership.
   9  * The ASF licenses this file to You under the Apache License, Version 2.0
  10  * (the "License"); you may not use this file except in compliance with
  11  * the License.  You may obtain a copy of the License at
  12  *
  13  *      http://www.apache.org/licenses/LICENSE-2.0
  14  *
  15  * Unless required by applicable law or agreed to in writing, software
  16  * distributed under the License is distributed on an "AS IS" BASIS,
  17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18  * See the License for the specific language governing permissions and
  19  * limitations under the License.
  20  */
  21 
  22 package com.sun.org.apache.xerces.internal.dom;
  23 
  24 import java.io.IOException;
  25 import java.io.ObjectInputStream;
  26 import java.io.ObjectOutputStream;
  27 import java.io.Serializable;
  28 import java.util.ArrayList;
  29 import java.util.List;
  30 import java.util.Vector;
  31 import org.w3c.dom.DOMException;
  32 import org.w3c.dom.NamedNodeMap;
  33 import org.w3c.dom.Node;
  34 
  35 /**
  36  * NamedNodeMaps represent collections of Nodes that can be accessed
  37  * by name. Entity and Notation nodes are stored in NamedNodeMaps
  38  * attached to the DocumentType. Attributes are placed in a NamedNodeMap
  39  * attached to the elem they're related too. However, because attributes
  40  * require more work, such as firing mutation events, they are stored in
  41  * a subclass of NamedNodeMapImpl.
  42  * <P>
  43  * Only one Node may be stored per name; attempting to
  44  * store another will replace the previous value.
  45  * <P>
  46  * NOTE: The "primary" storage key is taken from the NodeName attribute of the
  47  * node. The "secondary" storage key is the namespaceURI and localName, when
  48  * accessed by DOM level 2 nodes. All nodes, even DOM Level 2 nodes are stored
  49  * in a single Vector sorted by the primary "nodename" key.
  50  * <P>
  51  * NOTE: item()'s integer index does _not_ imply that the named nodes
  52  * must be stored in an array; that's only an access method. Note too
  53  * that these indices are "live"; if someone changes the map's
  54  * contents, the indices associated with nodes may change.
  55  * <P>
  56  *
  57  * @xerces.internal
  58  *
  59  * @since  PR-DOM-Level-1-19980818.
  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. */
  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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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     /***/
 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      */
 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 
 331     public NamedNodeMapImpl cloneMap(NodeImpl ownerNode) {
 332         NamedNodeMapImpl newmap = new NamedNodeMapImpl(ownerNode);
 333         newmap.cloneContent(this);
 334         return newmap;
 335     }
 336 
 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      */
 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      */
 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      */
 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 
 408     final boolean isReadOnly() {
 409         return (flags & READONLY) != 0;
 410     }
 411 
 412     final void isReadOnly(boolean value) {
 413         flags = (short) (value ? flags | READONLY : flags & ~READONLY);
 414     }
 415 
 416     final boolean changed() {
 417         return (flags & CHANGED) != 0;
 418     }
 419 
 420     final void changed(boolean value) {
 421         flags = (short) (value ? flags | CHANGED : flags & ~CHANGED);
 422     }
 423 
 424     final boolean hasDefaults() {
 425         return (flags & HASDEFAULTS) != 0;
 426     }
 427 
 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      */
 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      */
 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
 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       */
 535     protected void removeItem(int index) {
 536        if (nodes != null && index < nodes.size()){
 537            nodes.remove(index);
 538        }
 539     }
 540 
 541 
 542     protected Object getItem (int index){
 543         if (nodes != null) {
 544             return nodes.get(index);
 545         }
 546         return null;
 547     }
 548 
 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      */
 578     protected List<Node> cloneMap(List<Node> list) {
 579         if (nodes != null) {
 580             list = new ArrayList<>(nodes);
 581         }
 582         return list;
 583     }
 584 
 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       */
 592     public void removeAll (){
 593         if (nodes != null) {
 594             nodes.clear();
 595         }
 596     }
 597 
 598     private void readObject(ObjectInputStream in)
 599         throws IOException, ClassNotFoundException {
 600         in.defaultReadObject();
 601         if (nodes != null) {
 602             // nodes are written as a Vector for compatibility.
 603             nodes = new ArrayList<>(nodes);
 604         }
 605     }
 606 
 607     private void writeObject(ObjectOutputStream out) throws IOException {
 608         List<Node> oldNodes = this.nodes;
 609         try {
 610             if (oldNodes != null) {
 611                 // convert to Vector for backward-compatibility
 612                 this.nodes = new Vector<>(oldNodes);
 613             }
 614             out.defaultWriteObject();
 615         }
 616         // If the write fails for some reason ensure
 617         // that we restore the original object.
 618         finally {
 619             this.nodes = oldNodes;
 620         }
 621     }
 622 
 623 } // class NamedNodeMapImpl