001: /*
002: Copyright (C) 2003 Know Gate S.L. All rights reserved.
003: C/Oña, 107 1º2 28050 Madrid (Spain)
004:
005: Redistribution and use in source and binary forms, with or without
006: modification, are permitted provided that the following conditions
007: are met:
008:
009: 1. Redistributions of source code must retain the above copyright
010: notice, this list of conditions and the following disclaimer.
011:
012: 2. The end-user documentation included with the redistribution,
013: if any, must include the following acknowledgment:
014: "This product includes software parts from hipergate
015: (http://www.hipergate.org/)."
016: Alternately, this acknowledgment may appear in the software itself,
017: if and wherever such third-party acknowledgments normally appear.
018:
019: 3. The name hipergate must not be used to endorse or promote products
020: derived from this software without prior written permission.
021: Products derived from this software may not be called hipergate,
022: nor may hipergate appear in their name, without prior written
023: permission.
024:
025: This library is distributed in the hope that it will be useful,
026: but WITHOUT ANY WARRANTY; without even the implied warranty of
027: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
028:
029: You should have received a copy of hipergate License with this code;
030: if not, visit http://www.hipergate.org or mail to info@hipergate.org
031: */
032:
033: package com.knowgate.dataxslt;
034:
035: import java.io.IOException;
036: import java.io.File;
037: import java.io.FileReader;
038: import java.io.UnsupportedEncodingException;
039:
040: import com.knowgate.debug.DebugFile;
041: import com.knowgate.misc.Gadgets;
042: import com.knowgate.dfs.FileSystem;
043:
044: import org.w3c.dom.DOMException;
045:
046: /**
047: * Direct character-level manipulations for XML documents.
048: * This class modifies XML document files by directy seeking substrings inside
049: * its text. Given a well-knonw XML file structure is easier and faster to seek
050: * for <nodes> as substrings instead of parsing the whole documents into a
051: * DOM tree.
052: * @author Sergio Montoro Ten
053: * @version 2.2
054: */
055: public class XMLDocument {
056:
057: private String sXMLDoc;
058: private String sFilePath;
059: private String sEncoding;
060: private FileSystem oFS;
061:
062: public XMLDocument() {
063: oFS = new FileSystem();
064: sEncoding = "UTF-8";
065: }
066:
067: // ----------------------------------------------------------
068:
069: /**
070: * <p>Create XMLDocument and load an XML file into memory.</p>
071: * No node parsing is done, but file is loaded directly into a String.
072: * @param sFile File Path
073: * @throws IOException
074: * @throws OutOfMemoryError
075: */
076: public XMLDocument(String sFile) throws IOException,
077: OutOfMemoryError {
078: oFS = new FileSystem();
079: sEncoding = "UTF-8";
080: load(sFile);
081: }
082:
083: // ----------------------------------------------------------
084:
085: /**
086: * Create XMLDocument and load an XML file into memory.
087: * @param sFile File Path
088: * @param sEnc Character Encoding
089: * @throws IOException
090: * @throws OutOfMemoryError
091: */
092: public XMLDocument(String sFile, String sEnc) throws IOException,
093: OutOfMemoryError {
094: oFS = new FileSystem();
095: sEncoding = sEnc;
096: load(sFile);
097: }
098:
099: // ----------------------------------------------------------
100:
101: public String getCharacterEncoding() {
102: return sEncoding;
103: }
104:
105: // ----------------------------------------------------------
106:
107: public void setCharacterEncoding(String sEnc) {
108: sEnc = sEncoding;
109: }
110:
111: // ----------------------------------------------------------
112:
113: /**
114: * Load an XML file into memory.
115: * No node parsing is done, but file is loaded directly into a String.
116: * @param sFile File Path
117: * @param sEnc Character encoding
118: * @throws IOException
119: * @throws OutOfMemoryError
120: */
121: public void load(String sFile, String sEnc) throws IOException,
122: OutOfMemoryError {
123:
124: if (DebugFile.trace) {
125: DebugFile.writeln("Begin XMLDocument.load(" + sFile + ","
126: + sEnc + ")");
127: DebugFile.incIdent();
128: }
129:
130: sEncoding = sEnc;
131:
132: try {
133: sXMLDoc = oFS.readfilestr(sFile, sEncoding);
134: } catch (com.enterprisedt.net.ftp.FTPException neverthrown) {
135: }
136:
137: sFilePath = sFile;
138:
139: if (DebugFile.trace) {
140: DebugFile.decIdent();
141: DebugFile.writeln("End XMLDocument.load() : "
142: + String.valueOf(sXMLDoc.length()));
143: }
144: } // load
145:
146: // ----------------------------------------------------------
147:
148: /**
149: * <p>Load an XML file into memory.</p>
150: * No node parsing is done, but file is loaded directly into a String.<br>
151: * Character encoding is UTF-8 by default unless changed by calling setEncoding()
152: * @param sFile File Path
153: * @throws IOException
154: * @throws OutOfMemoryError
155: */
156: public void load(String sFile) throws IOException, OutOfMemoryError {
157:
158: if (DebugFile.trace) {
159: DebugFile.writeln("Begin XMLDocument.load(" + sFile + ")");
160: DebugFile.incIdent();
161: }
162:
163: try {
164: sXMLDoc = oFS.readfilestr(sFile, sEncoding);
165: } catch (com.enterprisedt.net.ftp.FTPException neverthrown) {
166: }
167:
168: sFilePath = sFile;
169:
170: if (DebugFile.trace) {
171: DebugFile.decIdent();
172: DebugFile.writeln("End XMLDocument.load() : "
173: + String.valueOf(sXMLDoc.length()));
174: }
175: } // load
176:
177: // ----------------------------------------------------------
178:
179: /**
180: * Save file to disk.
181: * If it already exists it is overwritten.
182: * @param sFile File Path
183: * @throws IOException
184: */
185: public void save(String sFile) throws IOException {
186:
187: if (DebugFile.trace) {
188: DebugFile.writeln("Begin XMLDocument.save(" + sFile + ")");
189: DebugFile.incIdent();
190: }
191:
192: oFS.writefilestr(sFile, sXMLDoc, sEncoding);
193:
194: if (DebugFile.trace) {
195:
196: if (sXMLDoc.length() > 0) {
197: if (Character.isISOControl(sXMLDoc.charAt(0)))
198: DebugFile.writeln("Warning: document " + sFile
199: + " starts with ISO control characters");
200: if (Character.isISOControl(sXMLDoc.charAt(sXMLDoc
201: .length() - 1)))
202: DebugFile.writeln("Warning: document " + sFile
203: + " ends with ISO control characters");
204: }
205:
206: DebugFile.decIdent();
207:
208: File oFileW = new File(sFile);
209: long lFileW = oFileW.length();
210:
211: DebugFile.writeln("End XMLDocument.save() : "
212: + String.valueOf(lFileW));
213: }
214: } // save
215:
216: // ----------------------------------------------------------
217:
218: /**
219: * Save file to disk.
220: * Save to same path used for loading the file.
221: * @throws IOException
222: */
223: public void save() throws IOException {
224: save(sFilePath);
225: }
226:
227: // ----------------------------------------------------------
228:
229: private static boolean isLastSibling(String sXMLDoc,
230: int iFromIndex, String sParent, String sSibling) {
231:
232: int iEndParent = sXMLDoc.indexOf("</" + sParent, iFromIndex);
233: int iNextSibling = sXMLDoc.indexOf("<" + sSibling, iFromIndex);
234:
235: if (iNextSibling == -1)
236: return true;
237: else
238: return iNextSibling > iEndParent;
239: } // isLastSibling
240:
241: // ----------------------------------------------------------
242:
243: private int seekNode(String sXPath) {
244: int iNodeCount;
245: int iNode;
246: int iAttr;
247: int iLeft;
248: int iRight;
249: String sCurrent;
250: String vNodes[]; // Array de nodos parseados
251: String vAttrs[]; // Array de atributos parseados
252: boolean bAttrs[]; // Guarda indicadores booleanos según se van encontrando los atributos
253: // para saber cual es el que no se encuentra en caso de fallo
254:
255: if (DebugFile.trace) {
256: DebugFile.writeln("Begin XMLDocument.seekNode(" + sXPath
257: + ")");
258: DebugFile.incIdent();
259: }
260:
261: // Parsear la cadena de búsqueda XPath y colocar el resultado
262: // en dos arrays, uno para los nodos y otro para los atributos.
263: vNodes = Gadgets.split(sXPath, "/");
264: if (null == vNodes) {
265: iNodeCount = 0;
266: vAttrs = null;
267: bAttrs = null;
268: } else {
269: iNodeCount = vNodes.length;
270: vAttrs = new String[iNodeCount];
271: bAttrs = new boolean[iNodeCount];
272: for (int a = 0; a < iNodeCount; a++)
273: bAttrs[a] = false;
274: }
275:
276: for (int iTok = 0; iTok < iNodeCount; iTok++) {
277:
278: iLeft = vNodes[iTok].indexOf("[");
279: if (iLeft > 0) {
280: iRight = vNodes[iTok].indexOf("]");
281:
282: if (iRight == -1)
283: throw new DOMException(
284: DOMException.INVALID_ACCESS_ERR,
285: "missing right bracket");
286:
287: // Sacar el contenido dentro de los corchetes
288: vAttrs[iTok] = vNodes[iTok]
289: .substring(iLeft + 1, iRight);
290: // Eliminar las arrobas
291: vAttrs[iTok] = vAttrs[iTok].replace('@', ' ');
292: // Cambiar las comillas simples por dobles
293: vAttrs[iTok] = vAttrs[iTok].replace((char) 39,
294: (char) 34);
295: // Eliminar los espacios en blanco
296: vAttrs[iTok] = vAttrs[iTok].trim();
297: // Asignar al nodo el valor quitando el contenido de los corchetes
298: vNodes[iTok] = vNodes[iTok].substring(0, iLeft);
299: } else
300: vAttrs[iTok] = "";
301:
302: if (DebugFile.trace)
303: DebugFile.writeln("Token " + String.valueOf(iTok)
304: + " : node=" + vNodes[iTok] + ", attr="
305: + vAttrs[iTok]);
306: } // next (iTok)
307:
308: // Buscar el nodo
309: iLeft = 0;
310: iNode = 0;
311:
312: while (iNode < iNodeCount) {
313: // Primero recorrer el documento XML para buscar nodos coincidentes
314:
315: iLeft = sXMLDoc.indexOf("<" + vNodes[iNode], iLeft);
316: if (iLeft < 0) {
317: if (DebugFile.trace)
318: DebugFile.writeln("Node " + vNodes[iNode]
319: + " not found");
320: throw new DOMException(DOMException.NOT_FOUND_ERR,
321: "Node " + vNodes[iNode] + " not found");
322: } // fi(iLeft<0)
323:
324: iRight = sXMLDoc.indexOf(">", iLeft + 1);
325: if (iRight < 0) {
326: if (DebugFile.trace)
327: DebugFile.writeln("Unclosed Node " + vNodes[iNode]
328: + " missing >");
329: throw new DOMException(DOMException.SYNTAX_ERR,
330: "Unclosed Node " + vNodes[iNode] + " missing >");
331: }
332:
333: sCurrent = sXMLDoc.substring(iLeft + 1, iLeft
334: + vNodes[iNode].length() + 1);
335:
336: if (vNodes[iNode].equals(sCurrent)) {
337:
338: if (vAttrs[iNode].length() == 0) {
339: // No hay atributos, dar por coincidente el primer nodo que aparezca
340: iNode++;
341: }
342:
343: // Tratar de forma especial la función position() de XPath
344: else if (vAttrs[iNode].startsWith("position()")) {
345:
346: String[] aAttrValue = Gadgets.split2(vAttrs[iNode],
347: '=');
348:
349: if (aAttrValue.length < 2)
350: throw new DOMException(
351: DOMException.NOT_SUPPORTED_ERR,
352: "position() function can only be declared equal to last() function");
353: else {
354: if (aAttrValue[1].equals("last()")) {
355: if (isLastSibling(sXMLDoc, iRight,
356: vNodes[iNode - 1], sCurrent)) {
357: bAttrs[iNode] = true;
358: iNode++;
359: } // fi (isLastSibling)
360: } // fi (aAttrValue[1])
361: else
362: throw new DOMException(
363: DOMException.NOT_SUPPORTED_ERR,
364: "position() function can only be declared equal to last() function");
365: }
366: } else {
367: // Mirar si el valor del atributo del nodo actual coincide con el especificado en XPath
368: iAttr = sXMLDoc.indexOf(vAttrs[iNode], iLeft + 1);
369: if (iAttr > iLeft && iAttr < iRight) {
370: bAttrs[iNode] = true;
371: iNode++;
372: }
373: }
374: } // fi(substring(<...)==vNode[])
375:
376: if (iNode < iNodeCount)
377: iLeft = iRight;
378: } // wend
379:
380: if (0 == iLeft) {
381: for (int b = 0; b < iNodeCount; b++) {
382: if (false == bAttrs[b]) {
383: if (DebugFile.trace)
384: DebugFile.writeln("Attribute " + vAttrs[b]
385: + " of node " + vNodes[b]
386: + " not found");
387: throw new DOMException(DOMException.NOT_FOUND_ERR,
388: "Attribute " + vAttrs[b] + " of node "
389: + vNodes[b] + " not found");
390: } // fi(bAttrs[b])
391: } // next(b)
392: } // fi(iLeft<0)
393:
394: if (DebugFile.trace) {
395: DebugFile.decIdent();
396: DebugFile.writeln("End XMLDocument.seekNode() : "
397: + String.valueOf(iLeft));
398: }
399:
400: return iLeft;
401: } // seekNode
402:
403: // ----------------------------------------------------------
404:
405: /**
406: * Get loaded file as a String
407: */
408: public String toString() {
409: return sXMLDoc;
410: }
411:
412: // ----------------------------------------------------------
413:
414: /**
415: * Add a piece of XML text after a given node identifier by an XPath expression.
416: * @param sAfterXPath Restricted XPath expression for node after witch the next node is to be placed.
417: * For example : <br>
418: * "pageset/pages/page[@guid="123456789012345678901234567890AB"]/blocks/block[@id="003"]" will add sNode text after <block id="003">...</block> substring.<br>
419: * "pageset/pages/page[position()=last()]" will add sNode text after last <page>...</page> substring.
420: * @param sNode XML Text to be added.
421: * @throws DOMException
422: * DOMException Codes:<br>
423: * <table border=1 cellpadding=4>
424: * <tr><td>NOT_FOUND_ERR</td><td>A node or attribute from the XPath expression was not found</td></tr>
425: * <tr><td>INVALID_ACCESS_ERR</td><td>An attribute expression is invalid</td></tr>
426: * <tr><td>NOT_SUPPORTED_ERR</td><td>position() function was used but last() was not specified as value for it</td></tr>
427: * </table>
428: */
429: public void addNode(String sAfterXPath, String sNode)
430: throws DOMException {
431: String sCloseParent;
432: int iOpenParent;
433: int iCloseParent;
434: int iTailParent;
435: int iAngle;
436: int iSpace;
437: char b;
438:
439: if (DebugFile.trace) {
440: DebugFile.writeln("Begin XMLDocument.addNode("
441: + sAfterXPath + "," + sNode + ")");
442: DebugFile.incIdent();
443: }
444:
445: iOpenParent = seekNode(sAfterXPath);
446:
447: if (DebugFile.trace)
448: DebugFile.writeln("iOpenParent="
449: + String.valueOf(iOpenParent));
450:
451: iSpace = sXMLDoc.indexOf(" ", iOpenParent);
452: if (-1 == iSpace)
453: iSpace = 2147483647;
454: iAngle = sXMLDoc.indexOf(">", iOpenParent);
455: if (-1 == iAngle)
456: iSpace = 2147483647;
457:
458: sCloseParent = "</"
459: + sXMLDoc.substring(iOpenParent + 1,
460: iSpace < iAngle ? iSpace : iAngle) + ">";
461:
462: if (DebugFile.trace)
463: DebugFile.writeln("sCloseParent=" + sCloseParent);
464:
465: iCloseParent = sXMLDoc.indexOf(sCloseParent, iOpenParent);
466:
467: if (DebugFile.trace)
468: DebugFile.writeln("iCloseParent="
469: + String.valueOf(iCloseParent));
470:
471: if (iCloseParent <= 0)
472: throw new DOMException(DOMException.NOT_FOUND_ERR, "Node "
473: + sCloseParent + " not found");
474:
475: iSpace = 0;
476: iAngle = iCloseParent - 1;
477: b = sXMLDoc.charAt(iAngle);
478: while (b == ' ' || b == '\t') {
479: iCloseParent--;
480: iSpace++;
481: b = sXMLDoc.charAt(--iAngle);
482: } // wend
483:
484: iCloseParent = iCloseParent + sCloseParent.length() + iSpace;
485: iTailParent = iCloseParent;
486:
487: if (iTailParent < sXMLDoc.length())
488: while (sXMLDoc.charAt(iTailParent) == (char) 13
489: || sXMLDoc.charAt(iTailParent) == (char) 10)
490: iTailParent++;
491:
492: StringBuffer oXMLDoc = new StringBuffer(iCloseParent
493: + sNode.length() + (sXMLDoc.length() - iTailParent) + 4);
494:
495: oXMLDoc.append(sXMLDoc.substring(0, iCloseParent));
496: oXMLDoc.append(sNode);
497: oXMLDoc.append('\n');
498:
499: if (iTailParent < sXMLDoc.length())
500: oXMLDoc.append(sXMLDoc.substring(iTailParent));
501:
502: sXMLDoc = oXMLDoc.toString();
503: oXMLDoc = null;
504:
505: if (DebugFile.trace) {
506: DebugFile.decIdent();
507: DebugFile.writeln("End XMLDocument.addNode()");
508: }
509: } // addNode
510:
511: // ----------------------------------------------------------
512:
513: /**
514: * Add a piece of XML text after a given node and save document.
515: * Document is saved to the same file path where if was loaded.
516: * @param sAfterXPath Restricted XPath expression for node after witch the next node is to be placed.
517: * @param sNode XML Text to be added.
518: * @throws DOMException
519: * @throws IOException
520: */
521: public void addNodeAndSave(String sAfterXPath, String sNode)
522: throws DOMException, IOException {
523: String sCloseParent;
524: int iOpenParent;
525: int iCloseParent;
526: int iTailParent;
527: int iAngle;
528: int iSpace;
529: char b;
530:
531: if (DebugFile.trace) {
532: DebugFile.writeln("Begin XMLDocument.addNodeAndSave("
533: + sAfterXPath + "," + sNode + ")");
534: DebugFile.incIdent();
535: }
536:
537: iOpenParent = seekNode(sAfterXPath);
538:
539: if (DebugFile.trace)
540: DebugFile.writeln("iOpenParent="
541: + String.valueOf(iOpenParent));
542:
543: iSpace = sXMLDoc.indexOf(" ", iOpenParent);
544: if (-1 == iSpace)
545: iSpace = 2147483647;
546: iAngle = sXMLDoc.indexOf(">", iOpenParent);
547: if (-1 == iAngle)
548: iSpace = 2147483647;
549:
550: sCloseParent = "</"
551: + sXMLDoc.substring(iOpenParent + 1,
552: iSpace < iAngle ? iSpace : iAngle) + ">";
553:
554: if (DebugFile.trace)
555: DebugFile.writeln("sCloseParent=" + sCloseParent);
556:
557: iCloseParent = sXMLDoc.indexOf(sCloseParent, iOpenParent);
558:
559: if (DebugFile.trace)
560: DebugFile.writeln("iCloseParent="
561: + String.valueOf(iCloseParent));
562:
563: if (iCloseParent <= 0)
564: throw new DOMException(DOMException.NOT_FOUND_ERR, "Node "
565: + sCloseParent + " not found");
566:
567: iSpace = 0;
568: iAngle = iCloseParent - 1;
569: b = sXMLDoc.charAt(iAngle);
570: while (b == ' ' || b == '\t') {
571: iCloseParent--;
572: iSpace++;
573: b = sXMLDoc.charAt(--iAngle);
574: } // wend
575:
576: iCloseParent = iCloseParent + sCloseParent.length() + iSpace;
577: iTailParent = iCloseParent;
578:
579: if (iTailParent < sXMLDoc.length())
580: while (sXMLDoc.charAt(iTailParent) == (char) 13
581: || sXMLDoc.charAt(iTailParent) == (char) 10)
582: iTailParent++;
583:
584: StringBuffer oXMLDoc = new StringBuffer(iCloseParent
585: + sNode.length() + (sXMLDoc.length() - iTailParent) + 4);
586:
587: oXMLDoc.append(sXMLDoc.substring(0, iCloseParent));
588: oXMLDoc.append(sNode);
589: oXMLDoc.append('\n');
590:
591: if (iTailParent < sXMLDoc.length())
592: oXMLDoc.append(sXMLDoc.substring(iTailParent));
593:
594: sXMLDoc = oXMLDoc.toString();
595: oXMLDoc = null;
596:
597: if (DebugFile.trace) {
598: if (sXMLDoc.length() > 0) {
599: if (Character.isISOControl(sXMLDoc.charAt(0)))
600: DebugFile.writeln("Warning: document " + sFilePath
601: + " starts with ISO control characters");
602: if (Character.isISOControl(sXMLDoc.charAt(sXMLDoc
603: .length() - 1)))
604: DebugFile.writeln("Warning: document " + sFilePath
605: + " ends with ISO control characters");
606: }
607: }
608:
609: oFS.writefilestr(sFilePath, sXMLDoc, sEncoding);
610:
611: if (DebugFile.trace) {
612: DebugFile.decIdent();
613:
614: File oFileW = new File(sFilePath);
615: long lFileW = oFileW.length();
616:
617: DebugFile.writeln("End XMLDocument.addNodeAndSave() : "
618: + String.valueOf(lFileW));
619: }
620: } // addNodeAndSave
621:
622: // ----------------------------------------------------------
623:
624: /**
625: * Remove a node.
626: * @param sXPath Restricted XPath expression for node to remove.
627: * For example: "pageset/pages/page[@guid="123456789012345678901234567890AB"]/blocks/block[@id="003"]"
628: * will remove <block id="003">...</block> substring.
629: * @throws DOMException
630: */
631: public void removeNode(String sXPath) throws DOMException {
632: int iStartParent;
633: int iEndParent;
634: int iBracket;
635: String sNodeName;
636: String vNodes[];
637:
638: if (DebugFile.trace) {
639: DebugFile.writeln("Begin XMLDocument.removeNode(" + sXPath
640: + ")");
641: DebugFile.incIdent();
642: }
643:
644: iStartParent = seekNode(sXPath);
645:
646: vNodes = Gadgets.split(sXPath, "/");
647:
648: iBracket = vNodes[vNodes.length - 1].indexOf("[");
649:
650: if (iBracket > 0)
651: sNodeName = vNodes[vNodes.length - 1]
652: .substring(0, iBracket);
653: else
654: sNodeName = vNodes[vNodes.length - 1];
655:
656: iEndParent = sXMLDoc.indexOf("</" + sNodeName + ">",
657: iStartParent)
658: + sNodeName.length() + 3;
659:
660: if (iEndParent == 0)
661: throw new DOMException(DOMException.NOT_FOUND_ERR, "Node "
662: + "</" + sNodeName + ">" + " not found");
663:
664: // Quitar los espacios por delante del nodo
665: for (char b = sXMLDoc.charAt(iStartParent); (b == ' ')
666: && iStartParent > 0; b = sXMLDoc.charAt(--iStartParent))
667: ;
668:
669: // Quitar los saltos de línea y retornos de carro por detrás del nodo
670: for (char c = sXMLDoc.charAt(iEndParent); (c == '\r' || c == '\n')
671: && iEndParent < sXMLDoc.length(); c = sXMLDoc
672: .charAt(++iEndParent))
673: ;
674:
675: sXMLDoc = sXMLDoc.substring(0, iStartParent)
676: + sXMLDoc.substring(iEndParent);
677:
678: if (DebugFile.trace) {
679: DebugFile.decIdent();
680: DebugFile.writeln("End XMLDocument.removeNode()");
681: }
682: } // removeNode
683:
684: // ----------------------------------------------------------
685:
686: /**
687: * Remove a node and save document.
688: * Document is saved to the same file path where if was loaded.
689: * @param sXPath XPath expression for node to remove.
690: * @throws DOMException
691: * @throws IOException
692: */
693: public void removeNodeAndSave(String sXPath) throws DOMException,
694: IOException {
695:
696: if (DebugFile.trace) {
697: DebugFile.writeln("Begin XMLDocument.removeNodeAndSave("
698: + sXPath + ")");
699: DebugFile.incIdent();
700: }
701:
702: removeNode(sXPath);
703:
704: oFS.writefilestr(sFilePath, sXMLDoc, sEncoding);
705:
706: if (DebugFile.trace) {
707: DebugFile.decIdent();
708: DebugFile.writeln("End XMLDocument.removeNodeAndSave()");
709: }
710: } // removeNodeAndSave
711:
712: // ----------------------------------------------------------
713: } // XMLDocument
|