Home | History | Annotate | Download | only in dom
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package org.apache.harmony.xml.dom;
     18 
     19 import java.util.ArrayList;
     20 import java.util.List;
     21 import java.util.Map;
     22 import javax.xml.transform.TransformerException;
     23 import org.apache.xml.serializer.utils.SystemIDResolver;
     24 import org.apache.xml.utils.URI;
     25 import org.w3c.dom.Attr;
     26 import org.w3c.dom.CharacterData;
     27 import org.w3c.dom.DOMException;
     28 import org.w3c.dom.Document;
     29 import org.w3c.dom.Element;
     30 import org.w3c.dom.NamedNodeMap;
     31 import org.w3c.dom.Node;
     32 import org.w3c.dom.NodeList;
     33 import org.w3c.dom.ProcessingInstruction;
     34 import org.w3c.dom.TypeInfo;
     35 import org.w3c.dom.UserDataHandler;
     36 
     37 /**
     38  * A straightforward implementation of the corresponding W3C DOM node.
     39  *
     40  * <p>Some fields have package visibility so other classes can access them while
     41  * maintaining the DOM structure.
     42  *
     43  * <p>This class represents a Node that has neither a parent nor children.
     44  * Subclasses may have either.
     45  *
     46  * <p>Some code was adapted from Apache Xerces.
     47  */
     48 public abstract class NodeImpl implements Node {
     49 
     50     private static final NodeList EMPTY_LIST = new NodeListImpl();
     51 
     52     static final TypeInfo NULL_TYPE_INFO = new TypeInfo() {
     53         public String getTypeName() {
     54             return null;
     55         }
     56         public String getTypeNamespace() {
     57             return null;
     58         }
     59         public boolean isDerivedFrom(
     60                 String typeNamespaceArg, String typeNameArg, int derivationMethod) {
     61             return false;
     62         }
     63     };
     64 
     65     DocumentImpl document;
     66 
     67     NodeImpl(DocumentImpl document) {
     68         this.document = document;
     69     }
     70 
     71     public Node appendChild(Node newChild) throws DOMException {
     72         throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
     73     }
     74 
     75     public final Node cloneNode(boolean deep) {
     76         return document.cloneOrImportNode(UserDataHandler.NODE_CLONED, this, deep);
     77     }
     78 
     79     public NamedNodeMap getAttributes() {
     80         return null;
     81     }
     82 
     83     public NodeList getChildNodes() {
     84         return EMPTY_LIST;
     85     }
     86 
     87     public Node getFirstChild() {
     88         return null;
     89     }
     90 
     91     public Node getLastChild() {
     92         return null;
     93     }
     94 
     95     public String getLocalName() {
     96         return null;
     97     }
     98 
     99     public String getNamespaceURI() {
    100         return null;
    101     }
    102 
    103     public Node getNextSibling() {
    104         return null;
    105     }
    106 
    107     public String getNodeName() {
    108         return null;
    109     }
    110 
    111     public abstract short getNodeType();
    112 
    113     public String getNodeValue() throws DOMException {
    114         return null;
    115     }
    116 
    117     public final Document getOwnerDocument() {
    118         return document == this ? null : document;
    119     }
    120 
    121     public Node getParentNode() {
    122         return null;
    123     }
    124 
    125     public String getPrefix() {
    126         return null;
    127     }
    128 
    129     public Node getPreviousSibling() {
    130         return null;
    131     }
    132 
    133     public boolean hasAttributes() {
    134         return false;
    135     }
    136 
    137     public boolean hasChildNodes() {
    138         return false;
    139     }
    140 
    141     public Node insertBefore(Node newChild, Node refChild) throws DOMException {
    142         throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    143     }
    144 
    145     public boolean isSupported(String feature, String version) {
    146         return DOMImplementationImpl.getInstance().hasFeature(feature, version);
    147     }
    148 
    149     public void normalize() {
    150     }
    151 
    152     public Node removeChild(Node oldChild) throws DOMException {
    153         throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    154     }
    155 
    156     public Node replaceChild(Node newChild, Node oldChild) throws DOMException {
    157         throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, null);
    158     }
    159 
    160     public final void setNodeValue(String nodeValue) throws DOMException {
    161         switch (getNodeType()) {
    162             case CDATA_SECTION_NODE:
    163             case COMMENT_NODE:
    164             case TEXT_NODE:
    165                 ((CharacterData) this).setData(nodeValue);
    166                 return;
    167 
    168             case PROCESSING_INSTRUCTION_NODE:
    169                 ((ProcessingInstruction) this).setData(nodeValue);
    170                 return;
    171 
    172             case ATTRIBUTE_NODE:
    173                 ((Attr) this).setValue(nodeValue);
    174                 return;
    175 
    176             case ELEMENT_NODE:
    177             case ENTITY_REFERENCE_NODE:
    178             case ENTITY_NODE:
    179             case DOCUMENT_NODE:
    180             case DOCUMENT_TYPE_NODE:
    181             case DOCUMENT_FRAGMENT_NODE:
    182             case NOTATION_NODE:
    183                 return; // do nothing!
    184 
    185             default:
    186                 throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
    187                         "Unsupported node type " + getNodeType());
    188         }
    189     }
    190 
    191     public void setPrefix(String prefix) throws DOMException {
    192     }
    193 
    194     /**
    195      * Validates the element or attribute namespace prefix on this node.
    196      *
    197      * @param namespaceAware whether this node is namespace aware
    198      * @param namespaceURI this node's namespace URI
    199      */
    200     static String validatePrefix(String prefix, boolean namespaceAware, String namespaceURI) {
    201         if (!namespaceAware) {
    202             throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
    203         }
    204 
    205         if (prefix != null) {
    206             if (namespaceURI == null
    207                     || !DocumentImpl.isXMLIdentifier(prefix)
    208                     || "xml".equals(prefix) && !"http://www.w3.org/XML/1998/namespace".equals(namespaceURI)
    209                     || "xmlns".equals(prefix) && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
    210                 throw new DOMException(DOMException.NAMESPACE_ERR, prefix);
    211             }
    212         }
    213 
    214         return prefix;
    215     }
    216 
    217     /**
    218      * Sets the element or attribute node to be namespace-aware and assign it
    219      * the specified name and namespace URI.
    220      *
    221      * @param node an AttrImpl or ElementImpl node.
    222      * @param namespaceURI this node's namespace URI. May be null.
    223      * @param qualifiedName a possibly-prefixed name like "img" or "html:img".
    224      */
    225     static void setNameNS(NodeImpl node, String namespaceURI, String qualifiedName) {
    226         if (qualifiedName == null) {
    227             throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
    228         }
    229 
    230         String prefix = null;
    231         int p = qualifiedName.lastIndexOf(":");
    232         if (p != -1) {
    233             prefix = validatePrefix(qualifiedName.substring(0, p), true, namespaceURI);
    234             qualifiedName = qualifiedName.substring(p + 1);
    235         }
    236 
    237         if (!DocumentImpl.isXMLIdentifier(qualifiedName)) {
    238             throw new DOMException(DOMException.INVALID_CHARACTER_ERR, qualifiedName);
    239         }
    240 
    241         switch (node.getNodeType()) {
    242             case ATTRIBUTE_NODE:
    243                 if ("xmlns".equals(qualifiedName)
    244                         && !"http://www.w3.org/2000/xmlns/".equals(namespaceURI)) {
    245                     throw new DOMException(DOMException.NAMESPACE_ERR, qualifiedName);
    246                 }
    247 
    248                 AttrImpl attr = (AttrImpl) node;
    249                 attr.namespaceAware = true;
    250                 attr.namespaceURI = namespaceURI;
    251                 attr.prefix = prefix;
    252                 attr.localName = qualifiedName;
    253                 break;
    254 
    255             case ELEMENT_NODE:
    256                 ElementImpl element = (ElementImpl) node;
    257                 element.namespaceAware = true;
    258                 element.namespaceURI = namespaceURI;
    259                 element.prefix = prefix;
    260                 element.localName = qualifiedName;
    261                 break;
    262 
    263             default:
    264                 throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
    265                         "Cannot rename nodes of type " + node.getNodeType());
    266         }
    267     }
    268 
    269     /**
    270      * Checks whether a required string matches an actual string. This utility
    271      * method is used for comparing namespaces and such. It takes into account
    272      * null arguments and the "*" special case.
    273      *
    274      * @param required The required string.
    275      * @param actual The actual string.
    276      * @return True if and only if the actual string matches the required one.
    277      */
    278     private static boolean matchesName(String required, String actual, boolean wildcard) {
    279         if (wildcard && "*".equals(required)) {
    280             return true;
    281         }
    282 
    283         if (required == null) {
    284             return (actual == null);
    285         }
    286 
    287         return required.equals(actual);
    288     }
    289 
    290     /**
    291      * Checks whether this node's name matches a required name. It takes into
    292      * account null arguments and the "*" special case.
    293      *
    294      * @param name The required name.
    295      * @return True if and only if the actual name matches the required one.
    296      */
    297     public boolean matchesName(String name, boolean wildcard) {
    298         return matchesName(name, getNodeName(), wildcard);
    299     }
    300 
    301     /**
    302      * Checks whether this node's namespace and local name match a required
    303      * pair of namespace and local name. It takes into account null arguments
    304      * and the "*" special case.
    305      *
    306      * @param namespaceURI The required namespace.
    307      * @param localName The required local name.
    308      * @return True if and only if the actual namespace and local name match
    309      *         the required pair of namespace and local name.
    310      */
    311     public boolean matchesNameNS(String namespaceURI, String localName, boolean wildcard) {
    312         return matchesName(namespaceURI, getNamespaceURI(), wildcard) && matchesName(localName, getLocalName(), wildcard);
    313     }
    314 
    315     public final String getBaseURI() {
    316         switch (getNodeType()) {
    317             case DOCUMENT_NODE:
    318                 return sanitizeUri(((Document) this).getDocumentURI());
    319 
    320             case ELEMENT_NODE:
    321                 Element element = (Element) this;
    322                 String uri = element.getAttributeNS(
    323                         "http://www.w3.org/XML/1998/namespace", "base"); // or "xml:base"
    324 
    325                 // if this node has no base URI, return the parent's.
    326                 if (uri == null || uri.length() == 0) {
    327                     return getParentBaseUri();
    328                 }
    329 
    330                 // if this node's URI is absolute, return that
    331                 if (SystemIDResolver.isAbsoluteURI(uri)) {
    332                     return uri;
    333                 }
    334 
    335                 // this node has a relative URI. Try to resolve it against the
    336                 // parent, but if that doesn't work just give up and return null.
    337                 String parentUri = getParentBaseUri();
    338                 if (parentUri == null) {
    339                     return null;
    340                 }
    341                 try {
    342                     return SystemIDResolver.getAbsoluteURI(uri, parentUri);
    343                 } catch (TransformerException e) {
    344                     return null; // the spec requires that we swallow exceptions
    345                 }
    346 
    347             case PROCESSING_INSTRUCTION_NODE:
    348                 return getParentBaseUri();
    349 
    350             case NOTATION_NODE:
    351             case ENTITY_NODE:
    352                 // When we support these node types, the parser should
    353                 // initialize a base URI field on these nodes.
    354                 return null;
    355 
    356             case ENTITY_REFERENCE_NODE:
    357                 // TODO: get this value from the parser, falling back to the
    358                 // referenced entity's baseURI if that doesn't exist
    359                 return null;
    360 
    361             case DOCUMENT_TYPE_NODE:
    362             case DOCUMENT_FRAGMENT_NODE:
    363             case ATTRIBUTE_NODE:
    364             case TEXT_NODE:
    365             case CDATA_SECTION_NODE:
    366             case COMMENT_NODE:
    367                 return null;
    368 
    369             default:
    370                 throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
    371                         "Unsupported node type " + getNodeType());
    372         }
    373     }
    374 
    375     private String getParentBaseUri() {
    376         Node parentNode = getParentNode();
    377         return parentNode != null ? parentNode.getBaseURI() : null;
    378     }
    379 
    380     /**
    381      * Returns the sanitized input if it is a URI, or {@code null} otherwise.
    382      */
    383     private String sanitizeUri(String uri) {
    384         if (uri == null || uri.length() == 0) {
    385             return null;
    386         }
    387         try {
    388             return new URI(uri).toString();
    389         } catch (URI.MalformedURIException e) {
    390             return null;
    391         }
    392     }
    393 
    394     public short compareDocumentPosition(Node other)
    395             throws DOMException {
    396         throw new UnsupportedOperationException(); // TODO
    397     }
    398 
    399     public String getTextContent() throws DOMException {
    400         return getNodeValue();
    401     }
    402 
    403     void getTextContent(StringBuilder buf) throws DOMException {
    404         String content = getNodeValue();
    405         if (content != null) {
    406             buf.append(content);
    407         }
    408     }
    409 
    410     public final void setTextContent(String textContent) throws DOMException {
    411         switch (getNodeType()) {
    412             case DOCUMENT_TYPE_NODE:
    413             case DOCUMENT_NODE:
    414                 return; // do nothing!
    415 
    416             case ELEMENT_NODE:
    417             case ENTITY_NODE:
    418             case ENTITY_REFERENCE_NODE:
    419             case DOCUMENT_FRAGMENT_NODE:
    420                 // remove all existing children
    421                 Node child;
    422                 while ((child = getFirstChild()) != null) {
    423                     removeChild(child);
    424                 }
    425                 // create a text node to hold the given content
    426                 if (textContent != null && textContent.length() != 0) {
    427                     appendChild(document.createTextNode(textContent));
    428                 }
    429                 return;
    430 
    431             case ATTRIBUTE_NODE:
    432             case TEXT_NODE:
    433             case CDATA_SECTION_NODE:
    434             case PROCESSING_INSTRUCTION_NODE:
    435             case COMMENT_NODE:
    436             case NOTATION_NODE:
    437                 setNodeValue(textContent);
    438                 return;
    439 
    440             default:
    441                 throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
    442                         "Unsupported node type " + getNodeType());
    443         }
    444     }
    445 
    446     public boolean isSameNode(Node other) {
    447         return this == other;
    448     }
    449 
    450     /**
    451      * Returns the element whose namespace definitions apply to this node. Use
    452      * this element when mapping prefixes to URIs and vice versa.
    453      */
    454     private NodeImpl getNamespacingElement() {
    455         switch (this.getNodeType()) {
    456             case ELEMENT_NODE:
    457                 return this;
    458 
    459             case DOCUMENT_NODE:
    460                 return (NodeImpl) ((Document) this).getDocumentElement();
    461 
    462             case ENTITY_NODE:
    463             case NOTATION_NODE:
    464             case DOCUMENT_FRAGMENT_NODE:
    465             case DOCUMENT_TYPE_NODE:
    466                 return null;
    467 
    468             case ATTRIBUTE_NODE:
    469                 return (NodeImpl) ((Attr) this).getOwnerElement();
    470 
    471             case TEXT_NODE:
    472             case CDATA_SECTION_NODE:
    473             case ENTITY_REFERENCE_NODE:
    474             case PROCESSING_INSTRUCTION_NODE:
    475             case COMMENT_NODE:
    476                 return getContainingElement();
    477 
    478             default:
    479                 throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
    480                         "Unsupported node type " + getNodeType());
    481         }
    482     }
    483 
    484     /**
    485      * Returns the nearest ancestor element that contains this node.
    486      */
    487     private NodeImpl getContainingElement() {
    488         for (Node p = getParentNode(); p != null; p = p.getParentNode()) {
    489             if (p.getNodeType() == ELEMENT_NODE) {
    490                 return (NodeImpl) p;
    491             }
    492         }
    493         return null;
    494     }
    495 
    496     public final String lookupPrefix(String namespaceURI) {
    497         if (namespaceURI == null) {
    498             return null;
    499         }
    500 
    501         // the XML specs define some prefixes (like "xml" and "xmlns") but this
    502         // API is explicitly defined to ignore those.
    503 
    504         NodeImpl target = getNamespacingElement();
    505         for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
    506             // check this element's namespace first
    507             if (namespaceURI.equals(node.getNamespaceURI())
    508                     && target.isPrefixMappedToUri(node.getPrefix(), namespaceURI)) {
    509                 return node.getPrefix();
    510             }
    511 
    512             // search this element for an attribute of this form:
    513             //   xmlns:foo="http://namespaceURI"
    514             if (!node.hasAttributes()) {
    515                 continue;
    516             }
    517             NamedNodeMap attributes = node.getAttributes();
    518             for (int i = 0, length = attributes.getLength(); i < length; i++) {
    519                 Node attr = attributes.item(i);
    520                 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())
    521                         || !"xmlns".equals(attr.getPrefix())
    522                         || !namespaceURI.equals(attr.getNodeValue())) {
    523                     continue;
    524                 }
    525                 if (target.isPrefixMappedToUri(attr.getLocalName(), namespaceURI)) {
    526                     return attr.getLocalName();
    527                 }
    528             }
    529         }
    530 
    531         return null;
    532     }
    533 
    534     /**
    535      * Returns true if the given prefix is mapped to the given URI on this
    536      * element. Since child elements can redefine prefixes, this check is
    537      * necessary: {@code
    538      * <foo xmlns:a="http://good">
    539      *   <bar xmlns:a="http://evil">
    540      *     <a:baz />
    541      *   </bar>
    542      * </foo>}
    543      *
    544      * @param prefix the prefix to find. Nullable.
    545      * @param uri the URI to match. Non-null.
    546      */
    547     boolean isPrefixMappedToUri(String prefix, String uri) {
    548         if (prefix == null) {
    549             return false;
    550         }
    551 
    552         String actual = lookupNamespaceURI(prefix);
    553         return uri.equals(actual);
    554     }
    555 
    556     public final boolean isDefaultNamespace(String namespaceURI) {
    557         String actual = lookupNamespaceURI(null); // null yields the default namespace
    558         return namespaceURI == null
    559                 ? actual == null
    560                 : namespaceURI.equals(actual);
    561     }
    562 
    563     public final String lookupNamespaceURI(String prefix) {
    564         NodeImpl target = getNamespacingElement();
    565         for (NodeImpl node = target; node != null; node = node.getContainingElement()) {
    566             // check this element's namespace first
    567             String nodePrefix = node.getPrefix();
    568             if (node.getNamespaceURI() != null) {
    569                 if (prefix == null // null => default prefix
    570                         ? nodePrefix == null
    571                         : prefix.equals(nodePrefix)) {
    572                     return node.getNamespaceURI();
    573                 }
    574             }
    575 
    576             // search this element for an attribute of the appropriate form.
    577             //    default namespace: xmlns="http://resultUri"
    578             //          non default: xmlns:specifiedPrefix="http://resultUri"
    579             if (!node.hasAttributes()) {
    580                 continue;
    581             }
    582             NamedNodeMap attributes = node.getAttributes();
    583             for (int i = 0, length = attributes.getLength(); i < length; i++) {
    584                 Node attr = attributes.item(i);
    585                 if (!"http://www.w3.org/2000/xmlns/".equals(attr.getNamespaceURI())) {
    586                     continue;
    587                 }
    588                 if (prefix == null // null => default prefix
    589                         ? "xmlns".equals(attr.getNodeName())
    590                         : "xmlns".equals(attr.getPrefix()) && prefix.equals(attr.getLocalName())) {
    591                     String value = attr.getNodeValue();
    592                     return value.length() > 0 ? value : null;
    593                 }
    594             }
    595         }
    596 
    597         return null;
    598     }
    599 
    600     /**
    601      * Returns a list of objects such that two nodes are equal if their lists
    602      * are equal. Be careful: the lists may contain NamedNodeMaps and Nodes,
    603      * neither of which override Object.equals(). Such values must be compared
    604      * manually.
    605      */
    606     private static List<Object> createEqualityKey(Node node) {
    607         List<Object> values = new ArrayList<Object>();
    608         values.add(node.getNodeType());
    609         values.add(node.getNodeName());
    610         values.add(node.getLocalName());
    611         values.add(node.getNamespaceURI());
    612         values.add(node.getPrefix());
    613         values.add(node.getNodeValue());
    614         for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
    615             values.add(child);
    616         }
    617 
    618         switch (node.getNodeType()) {
    619             case DOCUMENT_TYPE_NODE:
    620                 DocumentTypeImpl doctype = (DocumentTypeImpl) node;
    621                 values.add(doctype.getPublicId());
    622                 values.add(doctype.getSystemId());
    623                 values.add(doctype.getInternalSubset());
    624                 values.add(doctype.getEntities());
    625                 values.add(doctype.getNotations());
    626                 break;
    627 
    628             case ELEMENT_NODE:
    629                 Element element = (Element) node;
    630                 values.add(element.getAttributes());
    631                 break;
    632         }
    633 
    634         return values;
    635     }
    636 
    637     public final boolean isEqualNode(Node arg) {
    638         if (arg == this) {
    639             return true;
    640         }
    641 
    642         List<Object> listA = createEqualityKey(this);
    643         List<Object> listB = createEqualityKey(arg);
    644 
    645         if (listA.size() != listB.size()) {
    646             return false;
    647         }
    648 
    649         for (int i = 0; i < listA.size(); i++) {
    650             Object a = listA.get(i);
    651             Object b = listB.get(i);
    652 
    653             if (a == b) {
    654                 continue;
    655 
    656             } else if (a == null || b == null) {
    657                 return false;
    658 
    659             } else if (a instanceof String || a instanceof Short) {
    660                 if (!a.equals(b)) {
    661                     return false;
    662                 }
    663 
    664             } else if (a instanceof NamedNodeMap) {
    665                 if (!(b instanceof NamedNodeMap)
    666                         || !namedNodeMapsEqual((NamedNodeMap) a, (NamedNodeMap) b)) {
    667                     return false;
    668                 }
    669 
    670             } else if (a instanceof Node) {
    671                 if (!(b instanceof Node)
    672                         || !((Node) a).isEqualNode((Node) b)) {
    673                     return false;
    674                 }
    675 
    676             } else {
    677                 throw new AssertionError(); // unexpected type
    678             }
    679         }
    680 
    681         return true;
    682     }
    683 
    684     private boolean namedNodeMapsEqual(NamedNodeMap a, NamedNodeMap b) {
    685         if (a.getLength() != b.getLength()) {
    686             return false;
    687         }
    688         for (int i = 0; i < a.getLength(); i++) {
    689             Node aNode = a.item(i);
    690             Node bNode = aNode.getLocalName() == null
    691                     ? b.getNamedItem(aNode.getNodeName())
    692                     : b.getNamedItemNS(aNode.getNamespaceURI(), aNode.getLocalName());
    693             if (bNode == null || !aNode.isEqualNode(bNode)) {
    694                 return false;
    695             }
    696         }
    697         return true;
    698     }
    699 
    700     public final Object getFeature(String feature, String version) {
    701         return isSupported(feature, version) ? this : null;
    702     }
    703 
    704     public final Object setUserData(String key, Object data, UserDataHandler handler) {
    705         if (key == null) {
    706             throw new NullPointerException();
    707         }
    708         Map<String, UserData> map = document.getUserDataMap(this);
    709         UserData previous = data == null
    710                 ? map.remove(key)
    711                 : map.put(key, new UserData(data, handler));
    712         return previous != null ? previous.value : null;
    713     }
    714 
    715     public final Object getUserData(String key) {
    716         if (key == null) {
    717             throw new NullPointerException();
    718         }
    719         Map<String, UserData> map = document.getUserDataMapForRead(this);
    720         UserData userData = map.get(key);
    721         return userData != null ? userData.value : null;
    722     }
    723 
    724     static class UserData {
    725         final Object value;
    726         final UserDataHandler handler;
    727         UserData(Object value, UserDataHandler handler) {
    728             this.value = value;
    729             this.handler = handler;
    730         }
    731     }
    732 }
    733