/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Rhino code, released * May 6, 1999. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1997-2000 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Ethan Hugg * Terry Lucas * Milen Nankov * David P. Caldwell * * Alternatively, the contents of this file may be used under the terms of * the GNU General Public License Version 2 or later (the "GPL"), in which * case the provisions of the GPL are applicable instead of those above. If * you wish to allow use of your version of this file only under the terms of * the GPL and not to allow others to use your version of this file under the * MPL, indicate your decision by deleting the provisions above and replacing * them with the notice and other provisions required by the GPL. If you do * not delete the provisions above, a recipient may use your version of this * file under either the MPL or the GPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.javascript.xmlimpl; import org.mozilla.javascript.*; import org.mozilla.javascript.xml.XMLObject; class XML extends XMLObjectImpl { static final long serialVersionUID = -630969919086449092L; private XmlNode node; XML(XMLLibImpl lib, Scriptable scope, XMLObject prototype, XmlNode node) { super(lib, scope, prototype); initialize(node); } void initialize(XmlNode node) { this.node = node; this.node.setXml(this); } final XML getXML() { return this; } void replaceWith(XML value) { // We use the underlying document structure if the node is not // "standalone," but we need to just replace the XmlNode instance // otherwise if (this.node.parent() != null || false) { this.node.replaceWith(value.node); } else { this.initialize(value.node); } } /** @deprecated I would love to encapsulate this somehow. */ XML makeXmlFromString(XMLName name, String value) { try { return newTextElementXML(this.node, name.toQname(), value.toString()); } catch(Exception e) { throw ScriptRuntime.typeError(e.getMessage()); } } /** @deprecated Rename this, at the very least. But it's not clear it's even necessary */ XmlNode getAnnotation() { return node; } // // Methods from ScriptableObject // // TODO Either cross-reference this next comment with the specification or delete it and change the behavior // The comment: XML[0] should return this, all other indexes are Undefined public Object get(int index, Scriptable start) { if (index == 0) { return this; } else { return Scriptable.NOT_FOUND; } } public boolean has(int index, Scriptable start) { return (index == 0); } public void put(int index, Scriptable start, Object value) { // TODO Clarify the following comment and add a reference to the spec // The comment: Spec says assignment to indexed XML object should return type error throw ScriptRuntime.typeError("Assignment to indexed XML is not allowed"); } public Object[] getIds() { if (isPrototype()) { return new Object[0]; } else { return new Object[] { new Integer(0) }; } } // TODO This is how I found it but I am not sure it makes sense public void delete(int index) { if (index == 0) { this.remove(); } } // // Methods from XMLObjectImpl // boolean hasXMLProperty(XMLName xmlName) { if (isPrototype()) { return getMethod(xmlName.localName()) != NOT_FOUND; } else { return (getPropertyList(xmlName).length() > 0) || (getMethod(xmlName.localName()) != NOT_FOUND); } } Object getXMLProperty(XMLName xmlName) { if (isPrototype()) { return getMethod(xmlName.localName()); } else { return getPropertyList(xmlName); } } // // // Methods that merit further review // // XmlNode.QName getNodeQname() { return this.node.getQname(); } XML[] getChildren() { if (!isElement()) return null; XmlNode[] children = this.node.getMatchingChildren(XmlNode.Filter.TRUE); XML[] rv = new XML[children.length]; for (int i=0; i 0); } return hasProperty; } protected Object jsConstructor(Context cx, boolean inNewExpr, Object[] args) { if (args.length == 0 || args[0] == null || args[0] == Undefined.instance) { args = new Object[] { "" }; } // ECMA 13.4.2 does not appear to specify what to do if multiple arguments are sent. XML toXml = ecmaToXml(args[0]); if (inNewExpr) { return toXml.copy(); } else { return toXml; } } // See ECMA 357, 11_2_2_1, Semantics, 3_f. public Scriptable getExtraMethodSource(Context cx) { if (hasSimpleContent()) { String src = toString(); return ScriptRuntime.toObjectOrNull(cx, src); } return null; } // // TODO Miscellaneous methods not yet grouped // void removeChild(int index) { this.node.removeChild(index); } void normalize() { this.node.normalize(); } private XML toXML(XmlNode node) { if (node.getXml() == null) { node.setXml(newXML(node)); } return node.getXml(); } void setAttribute(XMLName xmlName, Object value) { if (!isElement()) throw new IllegalStateException("Can only set attributes on elements."); // TODO Is this legal, but just not "supported"? If so, support it. if (xmlName.uri() == null && xmlName.localName().equals("*")) { throw ScriptRuntime.typeError("@* assignment not supported."); } this.node.setAttribute(xmlName.toQname(), ScriptRuntime.toString(value)); } void remove() { this.node.deleteMe(); } void addMatches(XMLList rv, XMLName name) { name.addMatches(rv, this); } XMLList elements(XMLName name) { XMLList rv = newXMLList(); rv.setTargets(this, name.toQname()); // TODO Should have an XMLNode.Filter implementation based on XMLName XmlNode[] elements = this.node.getMatchingChildren(XmlNode.Filter.ELEMENT); for (int i=0; i= 0 && index < this.node.getChildCount()) { result.addToList(getXmlChild(index)); } return result; } XML getXmlChild(int index) { XmlNode child = this.node.getChild(index); if (child.getXml() == null) { child.setXml(newXML(child)); } return child.getXml(); } int childIndex() { return this.node.getChildIndex(); } boolean contains(Object xml) { if (xml instanceof XML) { return equivalentXml(xml); } else { return false; } } // Method overriding XMLObjectImpl boolean equivalentXml(Object target) { boolean result = false; if (target instanceof XML) { // TODO This is a horrifyingly inefficient way to do this so we should make it better. It may also not work. return this.node.toXmlString(getProcessor()).equals( ((XML)target).node.toXmlString(getProcessor()) ); } else if (target instanceof XMLList) { // TODO Is this right? Check the spec ... XMLList otherList = (XMLList) target; if (otherList.length() == 1) { result = equivalentXml(otherList.getXML()); } } else if (hasSimpleContent()) { String otherStr = ScriptRuntime.toString(target); result = toString().equals(otherStr); } return result; } XMLObjectImpl copy() { return newXML( this.node.copy() ); } boolean hasSimpleContent() { if (isComment() || isProcessingInstruction()) return false; if (isText() || this.node.isAttributeType()) return true; return !this.node.hasChildElement(); } boolean hasComplexContent() { return !hasSimpleContent(); } // TODO Cross-reference comment below with spec // Comment is: Length of an XML object is always 1, it's a list of XML objects of size 1. int length() { return 1; } // TODO it is not clear what this method was for ... boolean is(XML other) { return this.node.isSameNode(other.node); } Object nodeKind() { return ecmaClass(); } Object parent() { XmlNode parent = this.node.parent(); if (parent == null) return null; return newXML(this.node.parent()); } boolean propertyIsEnumerable(Object name) { boolean result; if (name instanceof Integer) { result = (((Integer)name).intValue() == 0); } else if (name instanceof Number) { double x = ((Number)name).doubleValue(); // Check that number is positive 0 result = (x == 0.0 && 1.0 / x > 0); } else { result = ScriptRuntime.toString(name).equals("0"); } return result; } Object valueOf() { return this; } // // Selection of children // XMLList comments() { XMLList rv = newXMLList(); this.node.addMatchingChildren(rv, XmlNode.Filter.COMMENT); return rv; } XMLList text() { XMLList rv = newXMLList(); this.node.addMatchingChildren(rv, XmlNode.Filter.TEXT); return rv; } XMLList processingInstructions(XMLName xmlName) { XMLList rv = newXMLList(); this.node.addMatchingChildren(rv, XmlNode.Filter.PROCESSING_INSTRUCTION(xmlName)); return rv; } // // Methods relating to modification of child nodes // // We create all the nodes we are inserting before doing the insert to // avoid nasty cycles caused by mutability of these objects. For example, // what if the toString() method of value modifies the XML object we were // going to insert into? insertAfter might get confused about where to // insert. This actually came up with SpiderMonkey, leading to a (very) // long discussion. See bug #354145. private XmlNode[] getNodesForInsert(Object value) { if (value instanceof XML) { return new XmlNode[] { ((XML)value).node }; } else if (value instanceof XMLList) { XMLList list = (XMLList)value; XmlNode[] rv = new XmlNode[list.length()]; for (int i=0; i 0) { // One exists an that index XML childToReplace = xlChildToReplace.item(0); insertChildAfter(childToReplace, xml); removeChild(index); } return this; } XML prependChild(Object xml) { if (this.node.isParentType()) { this.node.insertChildrenAt(0, getNodesForInsert(xml)); } return this; } XML appendChild(Object xml) { if (this.node.isParentType()) { XmlNode[] nodes = getNodesForInsert(xml); this.node.insertChildrenAt(this.node.getChildCount(), nodes); } return this; } private int getChildIndexOf(XML child) { for (int i=0; i 0) { this.node.removeChild(0); } XmlNode[] toInsert = getNodesForInsert(xml); // append new children this.node.insertChildrenAt(0, toInsert); return this; } // // Name and namespace-related methods // private void addInScopeNamespace(Namespace ns) { if (!isElement()) { return; } // See ECMA357 9.1.1.13 // in this implementation null prefix means ECMA undefined if (ns.prefix() != null) { if (ns.prefix().length() == 0 && ns.uri().length() == 0) { return; } if (node.getQname().getNamespace().getPrefix().equals(ns.prefix())) { node.invalidateNamespacePrefix(); } node.declareNamespace(ns.prefix(), ns.uri()); } else { return; } } Namespace[] inScopeNamespaces() { XmlNode.Namespace[] inScope = this.node.getInScopeNamespaces(); return createNamespaces(inScope); } private XmlNode.Namespace adapt(Namespace ns) { if (ns.prefix() == null) { return XmlNode.Namespace.create(ns.uri()); } else { return XmlNode.Namespace.create(ns.prefix(), ns.uri()); } } XML removeNamespace(Namespace ns) { if (!isElement()) return this; this.node.removeNamespace(adapt(ns)); return this; } XML addNamespace(Namespace ns) { addInScopeNamespace(ns); return this; } QName name() { if (isText() || isComment()) return null; if (isProcessingInstruction()) return newQName("", this.node.getQname().getLocalName(), null); return newQName(node.getQname()); } Namespace[] namespaceDeclarations() { XmlNode.Namespace[] declarations = node.getNamespaceDeclarations(); return createNamespaces(declarations); } Namespace namespace(String prefix) { if (prefix == null) { return createNamespace( this.node.getNamespaceDeclaration() ); } else { return createNamespace( this.node.getNamespaceDeclaration(prefix) ); } } String localName() { if (name() == null) return null; return name().localName(); } void setLocalName(String localName) { // ECMA357 13.4.4.34 if (isText() || isComment()) return; this.node.setLocalName(localName); } void setName(QName name) { // See ECMA357 13.4.4.35 if (isText() || isComment()) return; if (isProcessingInstruction()) { // Spec says set the name URI to empty string and then set the [[Name]] property, but I understand this to do the same // thing, unless we allow colons in processing instruction targets, which I think we do not. this.node.setLocalName(name.localName()); return; } node.renameNode(name.getDelegate()); } void setNamespace(Namespace ns) { // See ECMA357 13.4.4.36 if (isText() || isComment() || isProcessingInstruction()) return; setName(newQName(ns.uri(), localName(), ns.prefix())); } final String ecmaClass() { // See ECMA357 9.1 // TODO See ECMA357 9.1.1 last paragraph for what defaults should be if (node.isTextType()) { return "text"; } else if (node.isAttributeType()) { return "attribute"; } else if (node.isCommentType()) { return "comment"; } else if (node.isProcessingInstructionType()) { return "processing-instruction"; } else if (node.isElementType()) { return "element"; } else { throw new RuntimeException("Unrecognized type: " + node); } } public String getClassName() { // TODO: This appears to confuse the interpreter if we use the "real" class property from ECMA. Otherwise this code // would be: // return ecmaClass(); return "XML"; } private String ecmaValue() { return node.ecmaValue(); } private String ecmaToString() { // See ECMA357 10.1.1 if (isAttribute() || isText()) { return ecmaValue(); } if (this.hasSimpleContent()) { StringBuffer rv = new StringBuffer(); for (int i=0; i < this.node.getChildCount(); i++) { XmlNode child = this.node.getChild(i); if (!child.isProcessingInstructionType() && !child.isCommentType()) { // TODO: Probably inefficient; taking clean non-optimized // solution for now XML x = new XML(getLib(), getParentScope(), (XMLObject)getPrototype(), child); rv.append(x.toString()); } } return rv.toString(); } return toXMLString(); } public String toString() { return ecmaToString(); } String toXMLString() { return this.node.ecmaToXMLString(getProcessor()); } final boolean isAttribute() { return node.isAttributeType(); } final boolean isComment() { return node.isCommentType(); } final boolean isText() { return node.isTextType(); } final boolean isElement() { return node.isElementType(); } final boolean isProcessingInstruction() { return node.isProcessingInstructionType(); } // Support experimental Java interface org.w3c.dom.Node toDomNode() { return node.toDomNode(); } }