/* This file is part of NanoXML. * * $Revision: 1.21 $ * $Date: 2001/05/04 16:26:58 $ * $Name: RELEASE_1_6_8 $ * * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved. * * This software is provided 'as-is', without any express or implied warranty. * In no event will the authors be held liable for any damages arising from the * use of this software. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely, subject to the following restrictions: * * 1. The origin of this software must not be misrepresented; you must not * claim that you wrote the original software. If you use this software in * a product, an acknowledgment in the product documentation would be * appreciated but is not required. * * 2. Altered source versions must be plainly marked as such, and must not be * misrepresented as being the original software. * * 3. This notice may not be removed or altered from any source distribution. */ package tests.nanoxml; import catg.CATG; import java.io.*; import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; import java.util.Vector; /** * XMLElement is a representation of an XML object. The object is able to parse * XML code. * <P> * Note that NanoXML is not 100% XML 1.0 compliant: * <UL><LI>The parser is non-validating. * <LI>The DTD is fully ignored, including <CODE><!ENTITY...></CODE>. * <LI>There is no support for mixed content (elements containing both * subelements and CDATA elements) * </UL> * <P> * You can opt to use a SAX compatible API, by including both * <CODE>nanoxml.jar</CODE> and <CODE>nanoxml-sax.jar</CODE> in your classpath * and setting the property <CODE>org.xml.sax.parser</CODE> to * <CODE>nanoxml.sax.SAXParser</CODE> * <P> * $Revision: 1.21 $<BR> * $Date: 2001/05/04 16:26:58 $<P> * * @see tests.nanoxml.XMLParseException * * @author Marc De Scheemaecker * <<A HREF="mailto:Marc.DeScheemaecker@advalvas.be" * >Marc.DeScheemaecker@advalvas.be</A>> * @version 1.6 */ public class XMLElement implements Serializable { /** * Serialization serial version ID. */ static final long serialVersionUID = 6685035139346394777L; /** * Major version of NanoXML. */ public static final int NANOXML_MAJOR_VERSION = 1; /** * Minor version of NanoXML. */ public static final int NANOXML_MINOR_VERSION = 6; /** * The attributes given to the object. */ private Properties attributes; /** * Subobjects of the object. The subobjects are of class XMLElement * themselves. */ private Vector children; /** * The class of the object (the name indicated in the tag). */ private String tagName; /** * The #PCDATA content of the object. If there is no such content, this * field is null. */ private String contents; /** * Conversion table for &...; tags. */ private Properties conversionTable; /** * Whether to skip leading whitespace in CDATA. */ private boolean skipLeadingWhitespace; /** * The line number where the element starts. */ private int lineNr; /** * Whether the parsing is case sensitive. */ private boolean ignoreCase; /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD>Minimal XML conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE></DD> * <DT>Skip whitespace in contents</DT> * <DD><CODE>false</CODE></DD> * <DT>Ignore Case</DT> * <DD><CODE>true</CODE></DD> * </DL> * * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties) * @see tests.nanoxml.XMLElement#XMLElement(boolean) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties,boolean) */ public XMLElement() { this(new Properties(), false, true, true); } /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD><I>conversionTable</I> combined with the minimal XML * conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE></DD> * <DT>Skip whitespace in contents</DT> * <DD><CODE>false</CODE></DD> * <DT>Ignore Case</DT> * <DD><CODE>true</CODE></DD> * </DL> * * @see tests.nanoxml.XMLElement#XMLElement() * @see tests.nanoxml.XMLElement#XMLElement(boolean) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties,boolean) */ public XMLElement(Properties conversionTable) { this(conversionTable, false, true, true); } /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD>Minimal XML conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE></DD> * <DT>Skip whitespace in contents</DT> * <DD><I>skipLeadingWhitespace</I></DD> * <DT>Ignore Case</DT> * <DD><CODE>true</CODE></DD> * </DL> * * @see tests.nanoxml.XMLElement#XMLElement() * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties,boolean) */ public XMLElement(boolean skipLeadingWhitespace) { this(new Properties(), skipLeadingWhitespace, true, true); } /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD><I>conversionTable</I> combined with the minimal XML * conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE></DD> * <DT>Skip whitespace in contents</DT> * <DD><I>skipLeadingWhitespace</I></DD> * <DT>Ignore Case</DT> * <DD><CODE>true</CODE></DD> * </DL> * * @see tests.nanoxml.XMLElement#XMLElement() * @see tests.nanoxml.XMLElement#XMLElement(boolean) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties) */ public XMLElement(Properties conversionTable, boolean skipLeadingWhitespace) { this(conversionTable, skipLeadingWhitespace, true, true); } /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD><I>conversionTable</I>, eventually combined with the minimal XML * conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE> * (depending on <I>fillBasicConversionTable</I>)</DD> * <DT>Skip whitespace in contents</DT> * <DD><I>skipLeadingWhitespace</I></DD> * <DT>Ignore Case</DT> * <DD><I>ignoreCase</I></DD> * </DL> * <P> * This constructor should <I>only</I> be called from XMLElement itself * to create child elements. * * @see tests.nanoxml.XMLElement#XMLElement() * @see tests.nanoxml.XMLElement#XMLElement(boolean) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties,boolean) */ public XMLElement(Properties conversionTable, boolean skipLeadingWhitespace, boolean ignoreCase) { this(conversionTable, skipLeadingWhitespace, true, ignoreCase); } /** * Creates a new XML element. The following settings are used: * <DL><DT>Conversion table</DT> * <DD><I>conversionTable</I>, eventually combined with the minimal XML * conversions: <CODE>&amp; &lt; &gt; * &apos; &quot;</CODE> * (depending on <I>fillBasicConversionTable</I>)</DD> * <DT>Skip whitespace in contents</DT> * <DD><I>skipLeadingWhitespace</I></DD> * <DT>Ignore Case</DT> * <DD><I>ignoreCase</I></DD> * </DL> * <P> * This constructor should <I>only</I> be called from XMLElement itself * to create child elements. * * @see tests.nanoxml.XMLElement#XMLElement() * @see tests.nanoxml.XMLElement#XMLElement(boolean) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties) * @see tests.nanoxml.XMLElement#XMLElement(java.util.Properties,boolean) */ protected XMLElement(Properties conversionTable, boolean skipLeadingWhitespace, boolean fillBasicConversionTable, boolean ignoreCase) { this.ignoreCase = ignoreCase; this.skipLeadingWhitespace = skipLeadingWhitespace; this.tagName = null; this.contents = ""; this.attributes = new Properties(); this.children = new Vector(); this.conversionTable = conversionTable; this.lineNr = 0; if (fillBasicConversionTable) { this.conversionTable.put("lt", "<"); this.conversionTable.put("gt", ">"); this.conversionTable.put("quot", "\""); this.conversionTable.put("apos", "'"); this.conversionTable.put("amp", "&"); } } /** * Adds a subobject. */ public void addChild(XMLElement child) { this.children.addElement(child); } /** * Adds a property. * If the element is case insensitive, the property name is capitalized. */ public void addProperty(String key, Object value) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.put(key, value.toString()); } /** * Adds a property. * If the element is case insensitive, the property name is capitalized. */ public void addProperty(String key, int value) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.put(key, Integer.toString(value)); } /** * Adds a property. * If the element is case insensitive, the property name is capitalized. */ public void addProperty(String key, double value) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.put(key, Double.toString(value)); } /** * Returns the number of subobjects of the object. */ public int countChildren() { return this.children.size(); } /** * Enumerates the attribute names. */ public Enumeration enumeratePropertyNames() { return this.attributes.keys(); } /** * Enumerates the subobjects of the object. */ public Enumeration enumerateChildren() { return this.children.elements(); } /** * Returns the subobjects of the object. */ public Vector getChildren() { return this.children; } /** * Returns the #PCDATA content of the object. If there is no such content, * <CODE>null</CODE> is returned. */ public String getContents() { return this.contents; } /** * Returns the line nr on which the element is found. */ public int getLineNr() { return this.lineNr; } /** * Returns a property by looking up a key in a hashtable. * If the property doesn't exist, the value corresponding to defaultValue * is returned. */ public int getIntProperty(String key, Hashtable valueSet, String defaultValue) { String val = this.attributes.getProperty(key); Integer result; if (this.ignoreCase) { key = key.toUpperCase(); } if (val == null) { val = defaultValue; } try { result = (Integer)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result.intValue(); } /** * Returns a property of the object. If there is no such property, this * method returns <CODE>null</CODE>. */ public String getProperty(String key) { if (this.ignoreCase) { key = key.toUpperCase(); } return this.attributes.getProperty(key); } /** * Returns a property of the object. * If the property doesn't exist, <I>defaultValue</I> is returned. */ public String getProperty(String key, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } return this.attributes.getProperty(key, defaultValue); } /** * Returns an integer property of the object. * If the property doesn't exist, <I>defaultValue</I> is returned. */ public int getProperty(String key, int defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { return defaultValue; } else { try { return Integer.parseInt(val); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } } /** * Returns a floating point property of the object. * If the property doesn't exist, <I>defaultValue</I> is returned. */ public double getProperty(String key, double defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { return defaultValue; } else { try { return Double.valueOf(val).doubleValue(); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } } /** * Returns a boolean property of the object. If the property is missing, * <I>defaultValue</I> is returned. */ public boolean getProperty(String key, String trueValue, String falseValue, boolean defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { return defaultValue; } else if (val.equals(trueValue)) { return true; } else if (val.equals(falseValue)) { return false; } else { throw this.invalidValue(key, val, this.lineNr); } } /** * Returns a property by looking up a key in the hashtable <I>valueSet</I> * If the property doesn't exist, the value corresponding to * <I>defaultValue</I> is returned. */ public Object getProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); if (val == null) { val = defaultValue; } Object result = valueSet.get(val); if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result; } /** * Returns a property by looking up a key in the hashtable <I>valueSet</I>. * If the property doesn't exist, the value corresponding to * <I>defaultValue</I> is returned. */ public String getStringProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); String result; if (val == null) { val = defaultValue; } try { result = (String)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { throw this.invalidValue(key, val, this.lineNr); } return result; } /** * Returns a property by looking up a key in the hashtable <I>valueSet</I>. * If the value is not defined in the hashtable, the value is considered to * be an integer. * If the property doesn't exist, the value corresponding to * <I>defaultValue</I> is returned. */ public int getSpecialIntProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); Integer result; if (val == null) { val = defaultValue; } try { result = (Integer)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { try { return Integer.parseInt(val); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } return result.intValue(); } /** * Returns a property by looking up a key in the hashtable <I>valueSet</I>. * If the value is not defined in the hashtable, the value is considered to * be a floating point number. * If the property doesn't exist, the value corresponding to * <I>defaultValue</I> is returned. */ public double getSpecialDoubleProperty(String key, Hashtable valueSet, String defaultValue) { if (this.ignoreCase) { key = key.toUpperCase(); } String val = this.attributes.getProperty(key); Double result; if (val == null) { val = defaultValue; } try { result = (Double)(valueSet.get(val)); } catch (ClassCastException e) { throw this.invalidValueSet(key); } if (result == null) { try { result = Double.valueOf(val); } catch (NumberFormatException e) { throw this.invalidValue(key, val, this.lineNr); } } return result.doubleValue(); } /** * Returns the class (i.e. the name indicated in the tag) of the object. */ public String getTagName() { return this.tagName; } /** * Checks whether a character may be part of an identifier. */ private boolean isIdentifierChar(char ch) { return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0)); } /** * Reads an XML definition from a java.io.Reader and parses it. * * @exception java.io.IOException * if an error occured while reading the input * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the read data */ public void parseFromReader(Reader reader) throws IOException, XMLParseException { this.parseFromReader(reader, 1); } /** * Reads an XML definition from a java.io.Reader and parses it. * * @exception java.io.IOException * if an error occured while reading the input * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the read data */ public void parseFromReader(Reader reader, int startingLineNr) throws IOException, XMLParseException { int blockSize = 4096; char[] input = null; int size = 0; for (;;) { if (input == null) { input = new char[blockSize]; } else { char[] oldInput = input; input = new char[input.length + blockSize]; System.arraycopy(oldInput, 0, input, 0, oldInput.length); } int charsRead = reader.read(input, size, blockSize); if (charsRead < 0) { break; } size += charsRead; } this.parseCharArray(input, 0, size, startingLineNr); } /** * Parses an XML definition. * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the string */ public void parseString(String string) throws XMLParseException { this.parseCharArray(string.toCharArray(), 0, string.length(), 1); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the string following the XML data * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, string.length(), 1); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the string following the XML data (<= end) * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset, int end) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, end, 1); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the string following the XML data (<= end) * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the string */ public int parseString(String string, int offset, int end, int startingLineNr) throws XMLParseException { return this.parseCharArray(string.toCharArray(), offset, end, startingLineNr); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the array following the XML data (<= end) * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ public int parseCharArray(char[] input, int offset, int end) throws XMLParseException { return this.parseCharArray(input, offset, end, 1); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the array following the XML data (<= end) * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ public int parseCharArray(char[] input, int offset, int end, int startingLineNr) throws XMLParseException { int[] lineNr = new int[1]; lineNr[0] = startingLineNr; return this.parseCharArray(input, offset, end, lineNr); } /** * Parses an XML definition starting at <I>offset</I>. * * @return the offset of the array following the XML data (<= end) * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int parseCharArray(char[] input, int offset, int end, int[] currentLineNr) throws XMLParseException { this.lineNr = currentLineNr[0]; this.tagName = null; this.contents = null; this.attributes = new Properties(); this.children = new Vector(); try { offset = this.skipWhitespace(input, offset, end, currentLineNr); } catch (XMLParseException e) { return offset; } offset = this.skipPreamble(input, offset, end, currentLineNr); offset = this.scanTagName(input, offset, end, currentLineNr); this.lineNr = currentLineNr[0]; offset = this.scanAttributes(input, offset, end, currentLineNr); int[] contentOffset = new int[1]; int[] contentSize = new int[1]; int contentLineNr = currentLineNr[0]; offset = this.scanContent(input, offset, end, contentOffset, contentSize, currentLineNr); if (contentSize[0] > 0) { this.scanChildren(input, contentOffset[0], contentSize[0], contentLineNr); if (this.children.size() > 0) { this.contents = null; } else { this.processContents(input, contentOffset[0], contentSize[0], contentLineNr); for (int i = 0; i < this.contents.length(); i++) { if (this.contents.charAt(i) > ' ') { return offset; } } this.contents = null; } } return offset; } /** * Decodes the entities in the contents and, if skipLeadingWhitespace is * <CODE>true</CODE>, removes extraneous whitespaces after newlines and * convert those newlines into spaces. * * @see tests.nanoxml.XMLElement#decodeString * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private void processContents(char[] input, int contentOffset, int contentSize, int contentLineNr) throws XMLParseException { int[] lineNr = new int[1]; lineNr[0] = contentLineNr; if (! this.skipLeadingWhitespace) { String str = new String(input, contentOffset, contentSize); this.contents = this.decodeString(str, lineNr[0]); return; } StringBuffer result = new StringBuffer(contentSize); int end = contentSize + contentOffset; for (int i = contentOffset; i < end; i++) { char ch = input[i]; // The end of the contents is always a < character, so there's // no danger for bounds violation while ((ch == '\r') || (ch == '\n')) { lineNr[0]++; result.append(ch); i++; ch = input[i]; if (ch != '\n') { result.append(ch); } do { i++; ch = input[i]; } while ((ch == ' ') || (ch == '\t')); } if (i < end) { result.append(input[i]); } } this.contents = this.decodeString(result.toString(), lineNr[0]); } /** * Removes a child object. If the object is not a child, nothing happens. */ public void removeChild(XMLElement child) { this.children.removeElement(child); } /** * Removes an attribute. */ public void removeChild(String key) { if (this.ignoreCase) { key = key.toUpperCase(); } this.attributes.remove(key); } /** * Scans the attributes of the object. * * @return the offset in the string following the attributes, so that * input[offset] in { '/', '>' } * * @see tests.nanoxml.XMLElement#scanOneAttribute * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanAttributes(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { String key, value; for (;;) { offset = this.skipWhitespace(input, offset, end, lineNr); char ch = input[offset]; if ((ch == '/') || (ch == '>')) { break; } offset = this.scanOneAttribute(input, offset, end, lineNr); } return offset; } /**!!! * Searches the content for child objects. If such objects exist, the * content is reduced to <CODE>null</CODE>. * * @see tests.nanoxml.XMLElement#parseCharArray * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ protected void scanChildren(char[] input, int contentOffset, int contentSize, int contentLineNr) throws XMLParseException { int end = contentOffset + contentSize; int offset = contentOffset; int lineNr[] = new int[1]; lineNr[0] = contentLineNr; while (offset < end) { try { offset = this.skipWhitespace(input, offset, end, lineNr); } catch (XMLParseException e) { return; } if ((input[offset] != '<') || ((input[offset + 1] == '!') && (input[offset + 2] == '['))) { return; } XMLElement child = this.createAnotherElement(); offset = child.parseCharArray(input, offset, end, lineNr); this.children.addElement(child); } } /** * Creates a new XML element. */ protected XMLElement createAnotherElement() { return new XMLElement(this.conversionTable, this.skipLeadingWhitespace, false, this.ignoreCase); } /** * Scans the content of the object. * * @return the offset after the XML element; contentOffset points to the * start of the content section; contentSize is the size of the * content section * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanContent(char[] input, int offset, int end, int[] contentOffset, int[] contentSize, int[] lineNr) throws XMLParseException { if (input[offset] == '/') { contentSize[0] = 0; if (input[offset + 1] != '>') { throw this.expectedInput("'>'", lineNr[0]); } return offset + 2; } if (input[offset] != '>') { throw this.expectedInput("'>'", lineNr[0]); } if (this.skipLeadingWhitespace) { offset = this.skipWhitespace(input, offset + 1, end, lineNr); } else { offset++; } int begin = offset; contentOffset[0] = offset; int level = 0; char[] tag = this.tagName.toCharArray(); end -= (tag.length + 2); while ((offset < end) && (level >= 0)) { if (input[offset] == '<') { boolean ok = true; if ((offset < (end - 3)) && (input[offset + 1] == '!') && (input[offset + 2] == '-') && (input[offset + 3] == '-')) { offset += 3; while ((offset < end) && ((input[offset - 2] != '-') || (input[offset - 1] != '-') || (input[offset - 0] != '>'))) { offset++; } offset++; continue; } if ((offset < (end - 1)) && (input[offset + 1] == '!') && (input[offset + 2] == '[')) { offset++; continue; } for (int i = 0; ok && (i < tag.length); i++) { ok &= (input[offset + (i + 1)] == tag[i]); } ok &= ! this.isIdentifierChar(input[offset+tag.length+1]); if (ok) { while ((offset < end) && (input[offset] != '>')) { offset++; } if (input[offset - 1] != '/') { level++; } continue; } else if (input[offset + 1] == '/') { ok = true; for (int i = 0; ok && (i < tag.length); i++) { ok &= (input[offset + (i + 2)] == tag[i]); } if (ok) { contentSize[0] = offset - contentOffset[0]; offset += tag.length + 2; try { offset = this.skipWhitespace(input, offset, end + tag.length + 2, lineNr); } catch (XMLParseException e) { // ignore } if (input[offset] == '>') { level--; offset++; } continue; } } } if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (level >= 0) { throw this.unexpectedEndOfData(lineNr[0]); } if (this.skipLeadingWhitespace) { int i = contentOffset[0] + contentSize[0] - 1; while ((contentSize[0] >= 0) && (input[i] <= ' ')) { i--; contentSize[0]--; } } CATG.equivalent("test1", "loc1", offset); return offset; } /** * Scans an identifier. * * @return the identifier, or <CODE>null</CODE> if offset doesn't point * to an identifier */ private String scanIdentifier(char[] input, int offset, int end) { int begin = offset; while ((offset < end) && (this.isIdentifierChar(input[offset]))) { offset++; } if ((offset == end) || (offset == begin)) { return null; } else { return new String(input, begin, offset - begin); } } /** * Scans one attribute of an object. * * @return the offset after the attribute * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanOneAttribute(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { String key, value; key = this.scanIdentifier(input, offset, end); if (key == null) { throw this.syntaxError("an attribute key", lineNr[0]); } offset = this.skipWhitespace(input, offset + key.length(), end, lineNr); if (this.ignoreCase) { key = key.toUpperCase(); } if (input[offset] != '=') { throw this.valueMissingForAttribute(key, lineNr[0]); } offset = this.skipWhitespace(input, offset + 1, end, lineNr); value = this.scanString(input, offset, end, lineNr); if (value == null) { throw this.syntaxError("an attribute value", lineNr[0]); } if ((value.charAt(0) == '"') || (value.charAt(0) == '\'')) { value = value.substring(1, (value.length() - 1)); offset += 2; } this.attributes.put(key, this.decodeString(value, lineNr[0])); return offset + value.length(); } /** * Scans a string. Strings are either identifiers, or text delimited by * double quotes. * * @return the string found, without delimiting double quotes; or null * if offset didn't point to a valid string * * @see tests.nanoxml.XMLElement#scanIdentifier * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private String scanString(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { char delim = input[offset]; if ((delim == '"') || (delim == '\'')) { int begin = offset; offset++; while ((offset < end) && (input[offset] != delim)) { if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } offset++; } if (offset == end) { return null; } else { return new String(input, begin, offset - begin + 1); } } else { return this.scanIdentifier(input, offset, end); } } /** * Scans the class (tag) name of the object. * * @return the position after the tag name * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int scanTagName(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { this.tagName = this.scanIdentifier(input, offset, end); if (this.tagName == null) { throw this.syntaxError("a tag name", lineNr[0]); } offset = offset + this.tagName.length(); CATG.equivalent("test1", "loc3", offset); return offset; } /** * Changes the content string. * * @param content The new content string. */ public void setContent(String content) { this.contents = content; } /** * Changes the tag name. * * @param tagName The new tag name. */ public void setTagName(String tagName) { this.tagName = tagName; } /** * Skips a tag that don't contain any useful data: <?...?>, * <!...> and comments. * * @return the position after the tag * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ protected int skipBogusTag(char[] input, int offset, int end, int[] lineNr) { int level = 1; while (offset < end) { char ch = input[offset++]; switch (ch) { case '\r': if ((offset < end) && (input[offset] == '\n')) { offset++; } lineNr[0]++; break; case '\n': lineNr[0]++; break; case '<': level++; break; case '>': level--; if (level == 0) { return offset; } break; default: } } throw this.unexpectedEndOfData(lineNr[0]); } /** * Skips a tag that don't contain any useful data: <?...?>, * <!...> and comments. * * @return the position after the tag * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int skipPreamble(char[] input, int offset, int end, int[] lineNr) throws XMLParseException { char ch; do { offset = this.skipWhitespace(input, offset, end, lineNr); if (input[offset] != '<') { this.expectedInput("'<'", lineNr[0]); } offset++; if (offset >= end) { throw this.unexpectedEndOfData(lineNr[0]); } ch = input[offset]; if ((ch == '!') || (ch == '?')) { offset = this.skipBogusTag(input, offset, end, lineNr); } } while (! isIdentifierChar(ch)); CATG.equivalent("test1", "loc2", offset); return offset; } /** * Skips whitespace characters. * * @return the position after the whitespace * * @exception tests.nanoxml.XMLParseException * if an error occured while parsing the array */ private int skipWhitespace(char[] input, int offset, int end, int[] lineNr) { int startLine = lineNr[0]; while (offset < end) { if (((offset + 6) < end) && (input[offset + 3] == '-') && (input[offset + 2] == '-')&& (input[offset + 1] == '!') && (input[offset] == '<')) { offset += 4; while ((input[offset] != '-') || (input[offset + 1] != '-') || (input[offset + 2] != '>')) { if ((offset + 2) >= end) { throw this.unexpectedEndOfData(startLine); } offset++; } offset += 2; } else if (input[offset] == '\r') { lineNr[0]++; if ((offset != end) && (input[offset + 1] == '\n')) { offset++; } } else if (input[offset] == '\n') { lineNr[0]++; } else if (input[offset] > ' ') { break; } offset++; } if (offset == end) { throw this.unexpectedEndOfData(startLine); } return offset; } /** * Converts &...; sequences to "normal" chars. */ protected String decodeString(String s, int lineNr) { StringBuffer result = new StringBuffer(s.length()); int index = 0; while (index < s.length()) { int index2 = (s + '&').indexOf('&', index); int index3 = (s + "<![CDATA[").indexOf("<![CDATA[", index); int index4 = (s + "<!--").indexOf("<!--", index); if ((index2 <= index3) && (index2 <= index4)) { result.append(s.substring(index, index2)); if (index2 == s.length()) { break; } index = s.indexOf(';', index2); if (index < 0) { result.append(s.substring(index2)); break; } String key = s.substring(index2 + 1, index); if (key.charAt(0) == '#') { if (key.charAt(1) == 'x') { result.append((char)(Integer. parseInt(key.substring(2), 16))); } else { result.append((char)(Integer. parseInt(key.substring(1), 10))); } } else { result.append(this.conversionTable .getProperty(key, "&" + key + ';')); } } else if (index3 <= index4) { int end = (s + "]]>").indexOf("]]>", index3 + 9); result.append(s.substring(index, index3)); result.append(s.substring(index3 + 9, end)); index = end + 2; } else { result.append(s.substring(index, index4)); index = (s + "-->").indexOf("-->", index4) + 2; } index++; } return result.toString(); } /** * Writes the XML element to a string. */ public String toString() { StringWriter writer = new StringWriter(); this.write(writer); return writer.toString(); } /** * Writes the XML element to a writer. */ public void write(Writer writer) { this.write(writer, 0); } /** * Writes the XML element to a writer. */ public void write(Writer writer, int indent) { PrintWriter out = new PrintWriter(writer); for (int i = 0; i < indent; i++) { out.print(' '); } if (this.tagName == null) { this.writeEncoded(out, this.contents); return; } out.print('<'); out.print(this.tagName); if (! this.attributes.isEmpty()) { Enumeration enum2 = this.attributes.keys(); while (enum2.hasMoreElements()) { out.print(' '); String key = (String)(enum2.nextElement()); String value = (String)(this.attributes.get(key)); out.print(key); out.print("=\""); this.writeEncoded(out, value); out.print('"'); } } if ((this.contents != null) && (this.contents.length() > 0)) { if (this.skipLeadingWhitespace) { out.println('>'); for (int i = 0; i < indent + 4; i++) { out.print(' '); } out.println(this.contents); for (int i = 0; i < indent; i++) { out.print(' '); } } else { out.print('>'); this.writeEncoded(out, this.contents); } out.print("</"); out.print(this.tagName); out.println('>'); } else if (this.children.isEmpty()) { out.println("/>"); } else { out.println('>'); Enumeration enum2 = this.enumerateChildren(); while (enum2.hasMoreElements()) { XMLElement child = (XMLElement)(enum2.nextElement()); child.write(writer, indent + 4); } for (int i = 0; i < indent; i++) { out.print(' '); } out.print("</"); out.print(this.tagName); out.println('>'); } } /** * Writes a string encoded to a writer. */ protected void writeEncoded(PrintWriter out, String str) { for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); switch (ch) { case '<': out.write("<"); break; case '>': out.write(">"); break; case '&': out.write("&"); break; case '"': out.write("""); break; case '\'': out.write("'"); break; case '\r': case '\n': out.write(ch); break; default: if (((int)ch < 32) || ((int)ch > 126)) { out.write("&#x"); out.write(Integer.toString((int)ch, 16)); out.write(';'); } else { out.write(ch); } } } } /** * Creates a parse exception for when an invalid valueset is given to * a method. */ private XMLParseException invalidValueSet(String key) { String msg = "Invalid value set (key = \"" + key + "\")"; return new XMLParseException(this.getTagName(), msg); } /** * Creates a parse exception for when an invalid value is given to a * method. */ private XMLParseException invalidValue(String key, String value, int lineNr) { String msg = "Attribute \"" + key + "\" does not contain a valid " + "value (\"" + value + "\")"; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * The end of the data input has been reached. */ private XMLParseException unexpectedEndOfData(int lineNr) { String msg = "Unexpected end of data reached"; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A syntax error occured. */ private XMLParseException syntaxError(String context, int lineNr) { String msg = "Syntax error while parsing " + context; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A character has been expected. */ private XMLParseException expectedInput(String charSet, int lineNr) { String msg = "Expected: " + charSet; return new XMLParseException(this.getTagName(), lineNr, msg); } /** * A value is missing for an attribute. */ private XMLParseException valueMissingForAttribute(String key, int lineNr) { String msg = "Value missing for attribute with key \"" + key + "\""; return new XMLParseException(this.getTagName(), lineNr, msg); } }