org.mozilla.javascript.xmlimpl.XML Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rhino Show documentation
Show all versions of rhino Show documentation
Rhino is an open-source implementation of JavaScript written entirely in Java. It is typically
embedded into Java applications to provide scripting to end users.
The newest version!
/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.javascript.xmlimpl;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.Undefined;
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);
}
@Override
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) {
this.node.replaceWith(value.node);
} else {
this.initialize(value.node);
}
}
/* TODO: needs encapsulation. */
XML makeXmlFromString(XMLName name, String value) {
try {
return newTextElementXML(this.node, name.toQname(), value);
} catch (Exception e) {
throw ScriptRuntime.typeError(e.getMessage());
}
}
/* TODO: 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
@Override
public Object get(int index, Scriptable start) {
if (index == 0) {
return this;
} else {
return Scriptable.NOT_FOUND;
}
}
@Override
public boolean has(int index, Scriptable start) {
return (index == 0);
}
@Override
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");
}
@Override
public Object[] getIds() {
if (isPrototype()) {
return new Object[0];
} else {
return new Object[] {Integer.valueOf(0)};
}
}
// TODO This is how I found it but I am not sure it makes sense
@Override
public void delete(int index) {
if (index == 0) {
this.remove();
}
}
//
// Methods from XMLObjectImpl
//
@Override
boolean hasXMLProperty(XMLName xmlName) {
return (getPropertyList(xmlName).length() > 0);
}
@Override
Object getXMLProperty(XMLName xmlName) {
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 < rv.length; i++) {
rv[i] = toXML(children[i]);
}
return rv;
}
XML[] getAttributes() {
XmlNode[] attributes = this.node.getAttributes();
XML[] rv = new XML[attributes.length];
for (int i = 0; i < rv.length; i++) {
rv[i] = toXML(attributes[i]);
}
return rv;
}
// Used only by XML, XMLList
XMLList getPropertyList(XMLName name) {
return name.getMyValueOn(this);
}
@Override
void deleteXMLProperty(XMLName name) {
XMLList list = getPropertyList(name);
for (int i = 0; i < list.length(); i++) {
list.item(i).node.deleteMe();
}
}
@Override
void putXMLProperty(XMLName xmlName, Object value) {
if (isPrototype()) {
// TODO Is this really a no-op? Check the spec to be sure
} else {
xmlName.setMyValueOn(this, value);
}
}
@Override
boolean hasOwnProperty(XMLName xmlName) {
boolean hasProperty = false;
if (isPrototype()) {
String property = xmlName.localName();
hasProperty = (0 != findPrototypeId(property));
} else {
hasProperty = (getPropertyList(xmlName).length() > 0);
}
return hasProperty;
}
@Override
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.
@SuppressWarnings("deprecation")
@Override
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);
}
@Override
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();
}
@Override
void addMatches(XMLList rv, XMLName name) {
name.addMatches(rv, this);
}
@Override
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 < elements.length; i++) {
if (name.matches(toXML(elements[i]))) {
rv.addToList(toXML(elements[i]));
}
}
return rv;
}
@Override
XMLList child(XMLName xmlName) {
// TODO Right now I think this method would allow child( "@xxx" ) to return the xxx
// attribute, which is wrong
XMLList rv = newXMLList();
// TODO Should this also match processing instructions? If so, we have to change the
// filter and also the XMLName
// class to add an acceptsProcessingInstruction() method
XmlNode[] elements = this.node.getMatchingChildren(XmlNode.Filter.ELEMENT);
for (int i = 0; i < elements.length; i++) {
if (xmlName.matchesElement(elements[i].getQname())) {
rv.addToList(toXML(elements[i]));
}
}
rv.setTargets(this, xmlName.toQname());
return rv;
}
XML replace(XMLName xmlName, Object xml) {
putXMLProperty(xmlName, xml);
return this;
}
@Override
XMLList children() {
XMLList rv = newXMLList();
XMLName all = XMLName.formStar();
rv.setTargets(this, all.toQname());
XmlNode[] children = this.node.getMatchingChildren(XmlNode.Filter.TRUE);
for (int i = 0; i < children.length; i++) {
rv.addToList(toXML(children[i]));
}
return rv;
}
@Override
XMLList child(int index) {
// ECMA357 13.4.4.6 (numeric case)
XMLList result = newXMLList();
result.setTargets(this, null);
if (index >= 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();
}
/* Return the last added element */
XML getLastXmlChild() {
int pos = this.node.getChildCount() - 1;
if (pos < 0) return null;
return getXmlChild(pos);
}
int childIndex() {
return this.node.getChildIndex();
}
@Override
boolean contains(Object xml) {
if (xml instanceof XML) {
return equivalentXml(xml);
} else {
return false;
}
}
// Method overriding XMLObjectImpl
@Override
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;
}
@Override
XMLObjectImpl copy() {
return newXML(this.node.copy());
}
@Override
boolean hasSimpleContent() {
if (isComment() || isProcessingInstruction()) return false;
if (isText() || this.node.isAttributeType()) return true;
return !this.node.hasChildElement();
}
@Override
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.
@Override
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();
}
@Override
Object parent() {
XmlNode parent = this.node.parent();
if (parent == null) return null;
return newXML(this.node.parent());
}
@Override
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;
}
@Override
Object valueOf() {
return this;
}
//
// Selection of children
//
@Override
XMLList comments() {
XMLList rv = newXMLList();
this.node.addMatchingChildren(rv, XmlNode.Filter.COMMENT);
return rv;
}
@Override
XMLList text() {
XMLList rv = newXMLList();
this.node.addMatchingChildren(rv, XmlNode.Filter.TEXT);
return rv;
}
@Override
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 < list.length(); i++) {
rv[i] = list.item(i).node;
}
return rv;
} else {
return new XmlNode[] {
XmlNode.createText(getProcessor(), ScriptRuntime.toString(value))
};
}
}
XML replace(int index, Object xml) {
XMLList xlChildToReplace = child(index);
if (xlChildToReplace.length() > 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 < this.node.getChildCount(); i++) {
if (this.node.getChild(i).isSameNode(child.node)) {
return i;
}
}
return -1;
}
XML insertChildBefore(XML child, Object xml) {
if (child == null) {
// Spec says inserting before nothing is the same as appending
appendChild(xml);
} else {
XmlNode[] toInsert = getNodesForInsert(xml);
int index = getChildIndexOf(child);
if (index != -1) {
this.node.insertChildrenAt(index, toInsert);
}
}
return this;
}
XML insertChildAfter(XML child, Object xml) {
if (child == null) {
// Spec says inserting after nothing is the same as prepending
prependChild(xml);
} else {
XmlNode[] toInsert = getNodesForInsert(xml);
int index = getChildIndexOf(child);
if (index != -1) {
this.node.insertChildrenAt(index + 1, toInsert);
}
}
return this;
}
XML setChildren(Object xml) {
// TODO Have not carefully considered the spec but it seems to call for this
if (!isElement()) return this;
while (this.node.getChildCount() > 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);
}
}
@Override
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()) {
StringBuilder rv = new StringBuilder();
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();
}
@Override
public String toString() {
return ecmaToString();
}
@Override
String toSource(int indent) {
return toXMLString();
}
@Override
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();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy