001: /* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
002: *
003: * ***** BEGIN LICENSE BLOCK *****
004: * Version: MPL 1.1/GPL 2.0
005: *
006: * The contents of this file are subject to the Mozilla Public License Version
007: * 1.1 (the "License"); you may not use this file except in compliance with
008: * the License. You may obtain a copy of the License at
009: * http://www.mozilla.org/MPL/
010: *
011: * Software distributed under the License is distributed on an "AS IS" basis,
012: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
013: * for the specific language governing rights and limitations under the
014: * License.
015: *
016: * The Original Code is Rhino code, released
017: * May 6, 1999.
018: *
019: * The Initial Developer of the Original Code is
020: * Netscape Communications Corporation.
021: * Portions created by the Initial Developer are Copyright (C) 1997-2000
022: * the Initial Developer. All Rights Reserved.
023: *
024: * Contributor(s):
025: * Ethan Hugg
026: * Terry Lucas
027: * Milen Nankov
028: * David P. Caldwell <inonit@inonit.com>
029: *
030: * Alternatively, the contents of this file may be used under the terms of
031: * the GNU General Public License Version 2 or later (the "GPL"), in which
032: * case the provisions of the GPL are applicable instead of those above. If
033: * you wish to allow use of your version of this file only under the terms of
034: * the GPL and not to allow others to use your version of this file under the
035: * MPL, indicate your decision by deleting the provisions above and replacing
036: * them with the notice and other provisions required by the GPL. If you do
037: * not delete the provisions above, a recipient may use your version of this
038: * file under either the MPL or the GPL.
039: *
040: * ***** END LICENSE BLOCK ***** */
041:
042: package org.mozilla.javascript.xmlimpl;
043:
044: import org.mozilla.javascript.*;
045: import org.mozilla.javascript.xml.XMLObject;
046:
047: class XML extends XMLObjectImpl {
048: static final long serialVersionUID = -630969919086449092L;
049:
050: private XmlNode node;
051:
052: XML(XMLLibImpl lib, Scriptable scope, XMLObject prototype,
053: XmlNode node) {
054: super (lib, scope, prototype);
055: initialize(node);
056: }
057:
058: void initialize(XmlNode node) {
059: this .node = node;
060: this .node.setXml(this );
061: }
062:
063: final XML getXML() {
064: return this ;
065: }
066:
067: void replaceWith(XML value) {
068: // We use the underlying document structure if the node is not
069: // "standalone," but we need to just replace the XmlNode instance
070: // otherwise
071: if (this .node.parent() != null || false) {
072: this .node.replaceWith(value.node);
073: } else {
074: this .initialize(value.node);
075: }
076: }
077:
078: /** @deprecated I would love to encapsulate this somehow. */
079: XML makeXmlFromString(XMLName name, String value) {
080: try {
081: return newTextElementXML(this .node, name.toQname(), value
082: .toString());
083: } catch (Exception e) {
084: throw ScriptRuntime.typeError(e.getMessage());
085: }
086: }
087:
088: /** @deprecated Rename this, at the very least. But it's not clear it's even necessary */
089: XmlNode getAnnotation() {
090: return node;
091: }
092:
093: //
094: // Methods from ScriptableObject
095: //
096:
097: // TODO Either cross-reference this next comment with the specification or delete it and change the behavior
098: // The comment: XML[0] should return this, all other indexes are Undefined
099: public Object get(int index, Scriptable start) {
100: if (index == 0) {
101: return this ;
102: } else {
103: return Scriptable.NOT_FOUND;
104: }
105: }
106:
107: public boolean has(int index, Scriptable start) {
108: return (index == 0);
109: }
110:
111: public void put(int index, Scriptable start, Object value) {
112: // TODO Clarify the following comment and add a reference to the spec
113: // The comment: Spec says assignment to indexed XML object should return type error
114: throw ScriptRuntime
115: .typeError("Assignment to indexed XML is not allowed");
116: }
117:
118: public Object[] getIds() {
119: if (isPrototype()) {
120: return new Object[0];
121: } else {
122: return new Object[] { new Integer(0) };
123: }
124: }
125:
126: // TODO This is how I found it but I am not sure it makes sense
127: public void delete(int index) {
128: if (index == 0) {
129: this .remove();
130: }
131: }
132:
133: //
134: // Methods from XMLObjectImpl
135: //
136:
137: boolean hasXMLProperty(XMLName xmlName) {
138: if (isPrototype()) {
139: return getMethod(xmlName.localName()) != NOT_FOUND;
140: } else {
141: return (getPropertyList(xmlName).length() > 0)
142: || (getMethod(xmlName.localName()) != NOT_FOUND);
143: }
144: }
145:
146: Object getXMLProperty(XMLName xmlName) {
147: if (isPrototype()) {
148: return getMethod(xmlName.localName());
149: } else {
150: return getPropertyList(xmlName);
151: }
152: }
153:
154: //
155: //
156: // Methods that merit further review
157: //
158: //
159:
160: XmlNode.QName getNodeQname() {
161: return this .node.getQname();
162: }
163:
164: XML[] getChildren() {
165: if (!isElement())
166: return null;
167: XmlNode[] children = this .node
168: .getMatchingChildren(XmlNode.Filter.TRUE);
169: XML[] rv = new XML[children.length];
170: for (int i = 0; i < rv.length; i++) {
171: rv[i] = toXML(children[i]);
172: }
173: return rv;
174: }
175:
176: XML[] getAttributes() {
177: XmlNode[] attributes = this .node.getAttributes();
178: XML[] rv = new XML[attributes.length];
179: for (int i = 0; i < rv.length; i++) {
180: rv[i] = toXML(attributes[i]);
181: }
182: return rv;
183: }
184:
185: // Used only by XML, XMLList
186: XMLList getPropertyList(XMLName name) {
187: return name.getMyValueOn(this );
188: }
189:
190: void deleteXMLProperty(XMLName name) {
191: XMLList list = getPropertyList(name);
192: for (int i = 0; i < list.length(); i++) {
193: list.item(i).node.deleteMe();
194: }
195: }
196:
197: void putXMLProperty(XMLName xmlName, Object value) {
198: if (isPrototype()) {
199: // TODO Is this really a no-op? Check the spec to be sure
200: } else {
201: xmlName.setMyValueOn(this , value);
202: }
203: }
204:
205: boolean hasOwnProperty(XMLName xmlName) {
206: boolean hasProperty = false;
207:
208: if (isPrototype()) {
209: String property = xmlName.localName();
210: hasProperty = (0 != findPrototypeId(property));
211: } else {
212: hasProperty = (getPropertyList(xmlName).length() > 0);
213: }
214:
215: return hasProperty;
216: }
217:
218: protected Object jsConstructor(Context cx, boolean inNewExpr,
219: Object[] args) {
220: if (args.length == 0 || args[0] == null
221: || args[0] == Undefined.instance) {
222: args = new Object[] { "" };
223: }
224: // ECMA 13.4.2 does not appear to specify what to do if multiple arguments are sent.
225: XML toXml = ecmaToXml(args[0]);
226: if (inNewExpr) {
227: return toXml.copy();
228: } else {
229: return toXml;
230: }
231: }
232:
233: // See ECMA 357, 11_2_2_1, Semantics, 3_f.
234: public Scriptable getExtraMethodSource(Context cx) {
235: if (hasSimpleContent()) {
236: String src = toString();
237: return ScriptRuntime.toObjectOrNull(cx, src);
238: }
239: return null;
240: }
241:
242: //
243: // TODO Miscellaneous methods not yet grouped
244: //
245:
246: void removeChild(int index) {
247: this .node.removeChild(index);
248: }
249:
250: void normalize() {
251: this .node.normalize();
252: }
253:
254: private XML toXML(XmlNode node) {
255: if (node.getXml() == null) {
256: node.setXml(newXML(node));
257: }
258: return node.getXml();
259: }
260:
261: void setAttribute(XMLName xmlName, Object value) {
262: if (!isElement())
263: throw new IllegalStateException(
264: "Can only set attributes on elements.");
265: // TODO Is this legal, but just not "supported"? If so, support it.
266: if (xmlName.uri() == null && xmlName.localName().equals("*")) {
267: throw ScriptRuntime
268: .typeError("@* assignment not supported.");
269: }
270: this .node.setAttribute(xmlName.toQname(), ScriptRuntime
271: .toString(value));
272: }
273:
274: void remove() {
275: this .node.deleteMe();
276: }
277:
278: void addMatches(XMLList rv, XMLName name) {
279: name.addMatches(rv, this );
280: }
281:
282: XMLList elements(XMLName name) {
283: XMLList rv = newXMLList();
284: rv.setTargets(this , name.toQname());
285: // TODO Should have an XMLNode.Filter implementation based on XMLName
286: XmlNode[] elements = this .node
287: .getMatchingChildren(XmlNode.Filter.ELEMENT);
288: for (int i = 0; i < elements.length; i++) {
289: if (name.matches(toXML(elements[i]))) {
290: rv.addToList(toXML(elements[i]));
291: }
292: }
293: return rv;
294: }
295:
296: XMLList child(XMLName xmlName) {
297: // TODO Right now I think this method would allow child( "@xxx" ) to return the xxx attribute, which is wrong
298:
299: XMLList rv = newXMLList();
300:
301: // TODO Should this also match processing instructions? If so, we have to change the filter and also the XMLName
302: // class to add an acceptsProcessingInstruction() method
303:
304: XmlNode[] elements = this .node
305: .getMatchingChildren(XmlNode.Filter.ELEMENT);
306: for (int i = 0; i < elements.length; i++) {
307: if (xmlName.matchesElement(elements[i].getQname())) {
308: rv.addToList(toXML(elements[i]));
309: }
310: }
311: rv.setTargets(this , xmlName.toQname());
312: return rv;
313: }
314:
315: XML replace(XMLName xmlName, Object xml) {
316: putXMLProperty(xmlName, xml);
317: return this ;
318: }
319:
320: XMLList children() {
321: XMLList rv = newXMLList();
322: XMLName all = XMLName.formStar();
323: rv.setTargets(this , all.toQname());
324: XmlNode[] children = this .node
325: .getMatchingChildren(XmlNode.Filter.TRUE);
326: for (int i = 0; i < children.length; i++) {
327: rv.addToList(toXML(children[i]));
328: }
329: return rv;
330: }
331:
332: XMLList child(int index) {
333: // ECMA357 13.4.4.6 (numeric case)
334: XMLList result = newXMLList();
335: result.setTargets(this , null);
336: if (index >= 0 && index < this .node.getChildCount()) {
337: result.addToList(getXmlChild(index));
338: }
339: return result;
340: }
341:
342: XML getXmlChild(int index) {
343: XmlNode child = this .node.getChild(index);
344: if (child.getXml() == null) {
345: child.setXml(newXML(child));
346: }
347: return child.getXml();
348: }
349:
350: int childIndex() {
351: return this .node.getChildIndex();
352: }
353:
354: boolean contains(Object xml) {
355: if (xml instanceof XML) {
356: return equivalentXml(xml);
357: } else {
358: return false;
359: }
360: }
361:
362: // Method overriding XMLObjectImpl
363: boolean equivalentXml(Object target) {
364: boolean result = false;
365:
366: if (target instanceof XML) {
367: // TODO This is a horrifyingly inefficient way to do this so we should make it better. It may also not work.
368: return this .node.toXmlString(getProcessor()).equals(
369: ((XML) target).node.toXmlString(getProcessor()));
370: } else if (target instanceof XMLList) {
371: // TODO Is this right? Check the spec ...
372: XMLList otherList = (XMLList) target;
373:
374: if (otherList.length() == 1) {
375: result = equivalentXml(otherList.getXML());
376: }
377: } else if (hasSimpleContent()) {
378: String otherStr = ScriptRuntime.toString(target);
379:
380: result = toString().equals(otherStr);
381: }
382:
383: return result;
384: }
385:
386: XMLObjectImpl copy() {
387: return newXML(this .node.copy());
388: }
389:
390: boolean hasSimpleContent() {
391: if (isComment() || isProcessingInstruction())
392: return false;
393: if (isText() || this .node.isAttributeType())
394: return true;
395: return !this .node.hasChildElement();
396: }
397:
398: boolean hasComplexContent() {
399: return !hasSimpleContent();
400: }
401:
402: // TODO Cross-reference comment below with spec
403: // Comment is: Length of an XML object is always 1, it's a list of XML objects of size 1.
404: int length() {
405: return 1;
406: }
407:
408: // TODO it is not clear what this method was for ...
409: boolean is(XML other) {
410: return this .node.isSameNode(other.node);
411: }
412:
413: Object nodeKind() {
414: return ecmaClass();
415: }
416:
417: Object parent() {
418: XmlNode parent = this .node.parent();
419: if (parent == null)
420: return null;
421: return newXML(this .node.parent());
422: }
423:
424: boolean propertyIsEnumerable(Object name) {
425: boolean result;
426: if (name instanceof Integer) {
427: result = (((Integer) name).intValue() == 0);
428: } else if (name instanceof Number) {
429: double x = ((Number) name).doubleValue();
430: // Check that number is positive 0
431: result = (x == 0.0 && 1.0 / x > 0);
432: } else {
433: result = ScriptRuntime.toString(name).equals("0");
434: }
435: return result;
436: }
437:
438: Object valueOf() {
439: return this ;
440: }
441:
442: //
443: // Selection of children
444: //
445:
446: XMLList comments() {
447: XMLList rv = newXMLList();
448: this .node.addMatchingChildren(rv, XmlNode.Filter.COMMENT);
449: return rv;
450: }
451:
452: XMLList text() {
453: XMLList rv = newXMLList();
454: this .node.addMatchingChildren(rv, XmlNode.Filter.TEXT);
455: return rv;
456: }
457:
458: XMLList processingInstructions(XMLName xmlName) {
459: XMLList rv = newXMLList();
460: this .node.addMatchingChildren(rv, XmlNode.Filter
461: .PROCESSING_INSTRUCTION(xmlName));
462: return rv;
463: }
464:
465: //
466: // Methods relating to modification of child nodes
467: //
468:
469: // We create all the nodes we are inserting before doing the insert to
470: // avoid nasty cycles caused by mutability of these objects. For example,
471: // what if the toString() method of value modifies the XML object we were
472: // going to insert into? insertAfter might get confused about where to
473: // insert. This actually came up with SpiderMonkey, leading to a (very)
474: // long discussion. See bug #354145.
475: private XmlNode[] getNodesForInsert(Object value) {
476: if (value instanceof XML) {
477: return new XmlNode[] { ((XML) value).node };
478: } else if (value instanceof XMLList) {
479: XMLList list = (XMLList) value;
480: XmlNode[] rv = new XmlNode[list.length()];
481: for (int i = 0; i < list.length(); i++) {
482: rv[i] = list.item(i).node;
483: }
484: return rv;
485: } else {
486: return new XmlNode[] { XmlNode.createText(getProcessor(),
487: ScriptRuntime.toString(value)) };
488: }
489: }
490:
491: XML replace(int index, Object xml) {
492: XMLList xlChildToReplace = child(index);
493: if (xlChildToReplace.length() > 0) {
494: // One exists an that index
495: XML childToReplace = xlChildToReplace.item(0);
496: insertChildAfter(childToReplace, xml);
497: removeChild(index);
498: }
499: return this ;
500: }
501:
502: XML prependChild(Object xml) {
503: if (this .node.isParentType()) {
504: this .node.insertChildrenAt(0, getNodesForInsert(xml));
505: }
506: return this ;
507: }
508:
509: XML appendChild(Object xml) {
510: if (this .node.isParentType()) {
511: XmlNode[] nodes = getNodesForInsert(xml);
512: this .node
513: .insertChildrenAt(this .node.getChildCount(), nodes);
514: }
515: return this ;
516: }
517:
518: private int getChildIndexOf(XML child) {
519: for (int i = 0; i < this .node.getChildCount(); i++) {
520: if (this .node.getChild(i).isSameNode(child.node)) {
521: return i;
522: }
523: }
524: return -1;
525: }
526:
527: XML insertChildBefore(XML child, Object xml) {
528: if (child == null) {
529: // Spec says inserting before nothing is the same as appending
530: appendChild(xml);
531: } else {
532: XmlNode[] toInsert = getNodesForInsert(xml);
533: int index = getChildIndexOf(child);
534: if (index != -1) {
535: this .node.insertChildrenAt(index, toInsert);
536: }
537: }
538:
539: return this ;
540: }
541:
542: XML insertChildAfter(XML child, Object xml) {
543: if (child == null) {
544: // Spec says inserting after nothing is the same as prepending
545: prependChild(xml);
546: } else {
547: XmlNode[] toInsert = getNodesForInsert(xml);
548: int index = getChildIndexOf(child);
549: if (index != -1) {
550: this .node.insertChildrenAt(index + 1, toInsert);
551: }
552: }
553:
554: return this ;
555: }
556:
557: XML setChildren(Object xml) {
558: // TODO Have not carefully considered the spec but it seems to call for this
559: if (!isElement())
560: return this ;
561:
562: while (this .node.getChildCount() > 0) {
563: this .node.removeChild(0);
564: }
565: XmlNode[] toInsert = getNodesForInsert(xml);
566: // append new children
567: this .node.insertChildrenAt(0, toInsert);
568:
569: return this ;
570: }
571:
572: //
573: // Name and namespace-related methods
574: //
575:
576: private void addInScopeNamespace(Namespace ns) {
577: if (!isElement()) {
578: return;
579: }
580: // See ECMA357 9.1.1.13
581: // in this implementation null prefix means ECMA undefined
582: if (ns.prefix() != null) {
583: if (ns.prefix().length() == 0 && ns.uri().length() == 0) {
584: return;
585: }
586: if (node.getQname().getNamespace().getPrefix().equals(
587: ns.prefix())) {
588: node.invalidateNamespacePrefix();
589: }
590: node.declareNamespace(ns.prefix(), ns.uri());
591: } else {
592: return;
593: }
594: }
595:
596: Namespace[] inScopeNamespaces() {
597: XmlNode.Namespace[] inScope = this .node.getInScopeNamespaces();
598: return createNamespaces(inScope);
599: }
600:
601: private XmlNode.Namespace adapt(Namespace ns) {
602: if (ns.prefix() == null) {
603: return XmlNode.Namespace.create(ns.uri());
604: } else {
605: return XmlNode.Namespace.create(ns.prefix(), ns.uri());
606: }
607: }
608:
609: XML removeNamespace(Namespace ns) {
610: if (!isElement())
611: return this ;
612: this .node.removeNamespace(adapt(ns));
613: return this ;
614: }
615:
616: XML addNamespace(Namespace ns) {
617: addInScopeNamespace(ns);
618: return this ;
619: }
620:
621: QName name() {
622: if (isText() || isComment())
623: return null;
624: if (isProcessingInstruction())
625: return newQName("", this .node.getQname().getLocalName(),
626: null);
627: return newQName(node.getQname());
628: }
629:
630: Namespace[] namespaceDeclarations() {
631: XmlNode.Namespace[] declarations = node
632: .getNamespaceDeclarations();
633: return createNamespaces(declarations);
634: }
635:
636: Namespace namespace(String prefix) {
637: if (prefix == null) {
638: return createNamespace(this .node.getNamespaceDeclaration());
639: } else {
640: return createNamespace(this .node
641: .getNamespaceDeclaration(prefix));
642: }
643: }
644:
645: String localName() {
646: if (name() == null)
647: return null;
648: return name().localName();
649: }
650:
651: void setLocalName(String localName) {
652: // ECMA357 13.4.4.34
653: if (isText() || isComment())
654: return;
655: this .node.setLocalName(localName);
656: }
657:
658: void setName(QName name) {
659: // See ECMA357 13.4.4.35
660: if (isText() || isComment())
661: return;
662: if (isProcessingInstruction()) {
663: // Spec says set the name URI to empty string and then set the [[Name]] property, but I understand this to do the same
664: // thing, unless we allow colons in processing instruction targets, which I think we do not.
665: this .node.setLocalName(name.localName());
666: return;
667: }
668: node.renameNode(name.getDelegate());
669: }
670:
671: void setNamespace(Namespace ns) {
672: // See ECMA357 13.4.4.36
673: if (isText() || isComment() || isProcessingInstruction())
674: return;
675: setName(newQName(ns.uri(), localName(), ns.prefix()));
676: }
677:
678: final String ecmaClass() {
679: // See ECMA357 9.1
680:
681: // TODO See ECMA357 9.1.1 last paragraph for what defaults should be
682:
683: if (node.isTextType()) {
684: return "text";
685: } else if (node.isAttributeType()) {
686: return "attribute";
687: } else if (node.isCommentType()) {
688: return "comment";
689: } else if (node.isProcessingInstructionType()) {
690: return "processing-instruction";
691: } else if (node.isElementType()) {
692: return "element";
693: } else {
694: throw new RuntimeException("Unrecognized type: " + node);
695: }
696: }
697:
698: public String getClassName() {
699: // TODO: This appears to confuse the interpreter if we use the "real" class property from ECMA. Otherwise this code
700: // would be:
701: // return ecmaClass();
702: return "XML";
703: }
704:
705: private String ecmaValue() {
706: return node.ecmaValue();
707: }
708:
709: private String ecmaToString() {
710: // See ECMA357 10.1.1
711: if (isAttribute() || isText()) {
712: return ecmaValue();
713: }
714: if (this .hasSimpleContent()) {
715: StringBuffer rv = new StringBuffer();
716: for (int i = 0; i < this .node.getChildCount(); i++) {
717: XmlNode child = this .node.getChild(i);
718: if (!child.isProcessingInstructionType()
719: && !child.isCommentType()) {
720: // TODO: Probably inefficient; taking clean non-optimized
721: // solution for now
722: XML x = new XML(getLib(), getParentScope(),
723: (XMLObject) getPrototype(), child);
724: rv.append(x.toString());
725: }
726: }
727: return rv.toString();
728: }
729: return toXMLString();
730: }
731:
732: public String toString() {
733: return ecmaToString();
734: }
735:
736: String toXMLString() {
737: return this .node.ecmaToXMLString(getProcessor());
738: }
739:
740: final boolean isAttribute() {
741: return node.isAttributeType();
742: }
743:
744: final boolean isComment() {
745: return node.isCommentType();
746: }
747:
748: final boolean isText() {
749: return node.isTextType();
750: }
751:
752: final boolean isElement() {
753: return node.isElementType();
754: }
755:
756: final boolean isProcessingInstruction() {
757: return node.isProcessingInstructionType();
758: }
759:
760: // Support experimental Java interface
761: org.w3c.dom.Node toDomNode() {
762: return node.toDomNode();
763: }
764: }
|