1. /* This file is part of NanoXML.
  2. *
  3. * $Revision: 1.1 $
  4. * $Date: 2007/05/09 15:54:52 $
  5. * $Name: $
  6. *
  7. * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved.
  8. *
  9. * This software is provided 'as-is', without any express or implied warranty.
  10. * In no event will the authors be held liable for any damages arising from the
  11. * use of this software.
  12. *
  13. * Permission is granted to anyone to use this software for any purpose,
  14. * including commercial applications, and to alter it and redistribute it
  15. * freely, subject to the following restrictions:
  16. *
  17. * 1. The origin of this software must not be misrepresented; you must not
  18. * claim that you wrote the original software. If you use this software in
  19. * a product, an acknowledgment in the product documentation would be
  20. * appreciated but is not required.
  21. *
  22. * 2. Altered source versions must be plainly marked as such, and must not be
  23. * misrepresented as being the original software.
  24. *
  25. * 3. This notice may not be removed or altered from any source distribution.
  26. */
  27. package nanoxml;
  28. import java.util.Hashtable;
  29. /**
  30. * This class is modified from nanoxml.XMLElement.<br>
  31. * The reason to modify the original class is small memory of embeded devices.<br>
  32. *
  33. * <br>
  34. *
  35. * <br>
  36. * @author peaklau <br>
  37. * email:<A HREF="mailto:peaklau@hotmail.com">peaklau@hotmail.com</A> <br>
  38. * <a href="http://www.peaklau.com/fund/english/">HomePage</a>
  39. * @version $Revision: 1.1 $ $Date: 2007/05/09 15:54:52 $
  40. */
  41. public class XMLElement {
  42. public void startElement(String name) throws XMLParseException {
  43. // System.out.println("StartElement"+name);
  44. }
  45. public void content(String value) throws XMLParseException {
  46. // System.out.println("content"+value);
  47. }
  48. public void attribute(String key, String value) throws XMLParseException {
  49. // System.out.println("attribute key="+key+" value="+value);
  50. }
  51. public void endElement(String name) throws XMLParseException {
  52. // System.out.println("EndElement"+name);
  53. }
  54. public XMLElement createInstance(){
  55. return new XMLElement();
  56. }
  57. public static final int NANOXML_MAJOR_VERSION = 1;
  58. public static final int NANOXML_MINOR_VERSION = 6;
  59. /**
  60. * The class of the object (the name indicated in the tag).
  61. */
  62. private String tagName;
  63. /**
  64. * The #PCDATA content of the object. If there is no such content, this
  65. * field is null.
  66. */
  67. private String contents;
  68. /**
  69. * Conversion table for &...; tags.
  70. */
  71. private static FakeProperties conversionTable;
  72. /**
  73. * Whether to skip leading whitespace in CDATA.
  74. */
  75. private boolean skipLeadingWhitespace = false;
  76. /**
  77. * The line number where the element starts.
  78. */
  79. private int lineNr;
  80. private boolean ignoreCase = true;
  81. public XMLElement() {
  82. conversionTable = new FakeProperties();
  83. this.tagName = null;
  84. this.contents = "";
  85. this.lineNr = 0;
  86. conversionTable.put("lt", "<");
  87. conversionTable.put("gt", ">");
  88. conversionTable.put("quot", "\"");
  89. conversionTable.put("apos", "'");
  90. conversionTable.put("amp", "&");
  91. }
  92. /**
  93. * Returns the #PCDATA content of the object. If there is no such content,
  94. * <CODE>null</CODE> is returned.
  95. */
  96. public String getContents() {
  97. return this.contents;
  98. }
  99. /**
  100. * Returns the line nr on which the element is found.
  101. */
  102. public int getLineNr() {
  103. return this.lineNr;
  104. }
  105. /**
  106. * Returns the class (i.e. the name indicated in the tag) of the object.
  107. */
  108. public String getTagName() {
  109. return this.tagName;
  110. }
  111. /**
  112. * Checks whether a character may be part of an identifier.
  113. */
  114. private boolean isIdentifierChar(byte ch) {
  115. return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z'))
  116. || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0));
  117. }
  118. /**
  119. * Parses an XML definition starting at <I>offset</I>.
  120. *
  121. * @return the offset of the array following the XML data (<= end)
  122. *
  123. * @exception nanoxml.XMLParseException
  124. * if an error occured while parsing the array
  125. */
  126. public int parseArray(byte[] input, int offset, int end)
  127. throws XMLParseException {
  128. return this.parseArray(input, offset, end, 1);
  129. }
  130. /**
  131. * Parses an XML definition starting at <I>offset</I>.
  132. *
  133. * @return the offset of the array following the XML data (<= end)
  134. *
  135. * @exception nanoxml.XMLParseException
  136. * if an error occured while parsing the array
  137. */
  138. public int parseArray(byte[] input, int offset, int end, int startingLineNr)
  139. throws XMLParseException {
  140. int[] lineNr = new int[1];
  141. lineNr[0] = startingLineNr;
  142. return this.parseArray(input, offset, end, lineNr);
  143. }
  144. /**
  145. * Parses an XML definition starting at <I>offset</I>.
  146. *
  147. * @return the offset of the array following the XML data (<= end)
  148. *
  149. * @exception nanoxml.XMLParseException
  150. * if an error occured while parsing the array
  151. */
  152. private int parseArray(byte[] input, int offset, int end,
  153. int[] currentLineNr) throws XMLParseException {
  154. this.lineNr = currentLineNr[0];
  155. this.tagName = null;
  156. this.contents = "";
  157. try {
  158. offset = this.skipWhitespace(input, offset, end, currentLineNr);
  159. } catch (XMLParseException e) {
  160. return offset;
  161. }
  162. offset = this.skipPreamble(input, offset, end, currentLineNr);
  163. offset = this.scanTagName(input, offset, end, currentLineNr);
  164. startElement(tagName);
  165. this.lineNr = currentLineNr[0];
  166. offset = this.scanAttributes(input, offset, end, currentLineNr);
  167. int[] contentOffset = new int[1];
  168. int[] contentSize = new int[1];
  169. int contentLineNr = currentLineNr[0];
  170. offset = this.scanContent(input, offset, end, contentOffset,
  171. contentSize, currentLineNr);
  172. if (contentSize[0] > 0) {
  173. if (scanChildren(input, contentOffset[0], contentSize[0],
  174. contentLineNr) > 0) {
  175. this.contents = null;
  176. } else {
  177. this.processContents(input, contentOffset[0], contentSize[0],
  178. contentLineNr);
  179. content(contents);
  180. }
  181. }
  182. endElement(tagName);
  183. return offset;
  184. }
  185. /**
  186. * Decodes the entities in the contents and, if skipLeadingWhitespace is
  187. * <CODE>true</CODE>, removes extraneous whitespaces after newlines and
  188. * convert those newlines into spaces.
  189. *
  190. * @see XMLElement#decodeString
  191. *
  192. * @exception nanoxml.XMLParseException
  193. * if an error occured while parsing the array
  194. */
  195. private void processContents(byte[] input, int contentOffset,
  196. int contentSize, int contentLineNr) throws XMLParseException {
  197. int[] lineNr = new int[1];
  198. lineNr[0] = contentLineNr;
  199. if (!this.skipLeadingWhitespace) {
  200. String str = new String(input, contentOffset, contentSize);
  201. this.contents = this.decodeString(str, lineNr[0]);
  202. return;
  203. }
  204. StringBuffer result = new StringBuffer(contentSize);
  205. int end = contentSize + contentOffset;
  206. for (int i = contentOffset; i < end; i++) {
  207. byte ch = input[i];
  208. // The end of the contents is always a < character, so there's
  209. // no danger for bounds violation
  210. while ((ch == '\r') || (ch == '\n')) {
  211. lineNr[0]++;
  212. result.append(ch);
  213. i++;
  214. ch = input[i];
  215. if (ch != '\n') {
  216. result.append(ch);
  217. }
  218. do {
  219. i++;
  220. ch = input[i];
  221. } while ((ch == ' ') || (ch == '\t'));
  222. }
  223. if (i < end) {
  224. result.append(ch);
  225. }
  226. }
  227. this.contents = this.decodeString(result.toString(), lineNr[0]);
  228. }
  229. /**
  230. * Scans the attributes of the object.
  231. *
  232. * @return the offset in the string following the attributes, so that
  233. * input[offset] in { '/', '>' }
  234. *
  235. * @see XMLElement#scanOneAttribute
  236. *
  237. * @exception nanoxml.XMLParseException
  238. * if an error occured while parsing the array
  239. */
  240. private int scanAttributes(byte[] input, int offset, int end, int[] lineNr)
  241. throws XMLParseException {
  242. for (;;) {
  243. offset = this.skipWhitespace(input, offset, end, lineNr);
  244. byte ch = input[offset];
  245. if ((ch == '/') || (ch == '>')) {
  246. break;
  247. }
  248. offset = this.scanOneAttribute(input, offset, end, lineNr);
  249. }
  250. return offset;
  251. }
  252. /**
  253. * !!! Searches the content for child objects. If such objects exist, the
  254. * content is reduced to <CODE>null</CODE>.
  255. *
  256. * @see XMLElement#parseCharArray
  257. *
  258. * @exception nanoxml.XMLParseException
  259. * if an error occured while parsing the array
  260. */
  261. protected int scanChildren(byte[] input, int contentOffset,
  262. int contentSize, int contentLineNr) throws XMLParseException {
  263. int childCount = 0;
  264. int end = contentOffset + contentSize;
  265. int offset = contentOffset;
  266. int lineNr[] = new int[1];
  267. lineNr[0] = contentLineNr;
  268. while (offset < end) {
  269. try {
  270. offset = this.skipWhitespace(input, offset, end, lineNr);
  271. } catch (XMLParseException e) {
  272. break;
  273. }
  274. if ((input[offset] != '<')
  275. || ((input[offset + 1] == '!') && (input[offset + 2] == '['))) {
  276. break;
  277. }
  278. XMLElement child = this.createInstance();
  279. offset = child.parseArray(input, offset, end, lineNr);
  280. childCount++;
  281. }
  282. return childCount;
  283. }
  284. /**
  285. * Scans the content of the object.
  286. *
  287. * @return the offset after the XML element; contentOffset points to the
  288. * start of the content section; contentSize is the size of the
  289. * content section
  290. *
  291. * @exception nanoxml.XMLParseException
  292. * if an error occured while parsing the array
  293. */
  294. private int scanContent(byte[] input, int offset, int end,
  295. int[] contentOffset, int[] contentSize, int[] lineNr)
  296. throws XMLParseException {
  297. if (input[offset] == '/') {
  298. contentSize[0] = 0;
  299. if (input[offset + 1] != '>') {
  300. throw this.expectedInput("'>'", lineNr[0]);
  301. }
  302. return offset + 2;
  303. }
  304. if (input[offset] != '>') {
  305. throw this.expectedInput("'>'", lineNr[0]);
  306. }
  307. if (this.skipLeadingWhitespace) {
  308. offset = this.skipWhitespace(input, offset + 1, end, lineNr);
  309. } else {
  310. offset++;
  311. }
  312. int begin = offset;
  313. contentOffset[0] = offset;
  314. int level = 0;
  315. char[] tag = this.tagName.toCharArray();
  316. end -= (tag.length + 2);
  317. while ((offset < end) && (level >= 0)) {
  318. if (input[offset] == '<') {
  319. boolean ok = true;
  320. if ((offset < (end - 1)) && (input[offset + 1] == '!')) {
  321. offset++;
  322. continue;
  323. }
  324. for (int i = 0; ok && (i < tag.length); i++) {
  325. ok &= (input[offset + (i + 1)] == tag[i]);
  326. }
  327. ok &= !this.isIdentifierChar(input[offset + tag.length + 1]);
  328. if (ok) {
  329. while ((offset < end) && (input[offset] != '>')) {
  330. offset++;
  331. }
  332. if (input[offset - 1] != '/') {
  333. level++;
  334. }
  335. continue;
  336. } else if (input[offset + 1] == '/') {
  337. ok = true;
  338. for (int i = 0; ok && (i < tag.length); i++) {
  339. ok &= (input[offset + (i + 2)] == tag[i]);
  340. }
  341. if (ok) {
  342. contentSize[0] = offset - contentOffset[0];
  343. offset += tag.length + 2;
  344. offset = this
  345. .skipWhitespace(input, offset, end, lineNr);
  346. if (input[offset] == '>') {
  347. level--;
  348. offset++;
  349. }
  350. continue;
  351. }
  352. }
  353. }
  354. if (input[offset] == '\r') {
  355. lineNr[0]++;
  356. if ((offset != end) && (input[offset + 1] == '\n')) {
  357. offset++;
  358. }
  359. } else if (input[offset] == '\n') {
  360. lineNr[0]++;
  361. }
  362. offset++;
  363. }
  364. if (level >= 0) {
  365. throw this.unexpectedEndOfData(lineNr[0]);
  366. }
  367. if (this.skipLeadingWhitespace) {
  368. int i = contentOffset[0] + contentSize[0] - 1;
  369. while ((contentSize[0] >= 0) && (input[i] <= ' ')) {
  370. i--;
  371. contentSize[0]--;
  372. }
  373. }
  374. return offset;
  375. }
  376. /**
  377. * Scans an identifier.
  378. *
  379. * @return the identifier, or <CODE>null</CODE> if offset doesn't point to
  380. * an identifier
  381. */
  382. private String scanIdentifier(byte[] input, int offset, int end) {
  383. int begin = offset;
  384. while ((offset < end) && (this.isIdentifierChar(input[offset]))) {
  385. offset++;
  386. }
  387. if ((offset == end) || (offset == begin)) {
  388. return null;
  389. } else {
  390. return new String(input, begin, offset - begin);
  391. }
  392. }
  393. /**
  394. * Scans one attribute of an object.
  395. *
  396. * @return the offset after the attribute
  397. *
  398. * @exception nanoxml.XMLParseException
  399. * if an error occured while parsing the array
  400. */
  401. private int scanOneAttribute(byte[] input, int offset, int end, int[] lineNr)
  402. throws XMLParseException {
  403. String key, value;
  404. key = this.scanIdentifier(input, offset, end);
  405. if (key == null) {
  406. throw this.syntaxError("an attribute key", lineNr[0]);
  407. }
  408. offset = this.skipWhitespace(input, offset + key.length(), end, lineNr);
  409. if (this.ignoreCase) {
  410. key = key.toUpperCase();
  411. }
  412. if (input[offset] != '=') {
  413. throw this.valueMissingForAttribute(key, lineNr[0]);
  414. }
  415. offset = this.skipWhitespace(input, offset + 1, end, lineNr);
  416. value = this.scanString(input, offset, end, lineNr);
  417. if (value == null) {
  418. throw this.syntaxError("an attribute value", lineNr[0]);
  419. }
  420. if ((value.charAt(0) == '"') || (value.charAt(0) == '\'')) {
  421. value = value.substring(1, (value.length() - 1));
  422. offset += 2;
  423. }
  424. attribute(key, this.decodeString(value, lineNr[0]));
  425. // this.attributes.put(key, this.decodeString(value, lineNr[0]));
  426. return offset + value.length();
  427. }
  428. /**
  429. * Scans a string. Strings are either identifiers, or text delimited by
  430. * double quotes.
  431. *
  432. * @return the string found, without delimiting double quotes; or null if
  433. * offset didn't point to a valid string
  434. *
  435. * @see XMLElement#scanIdentifier
  436. *
  437. * @exception nanoxml.XMLParseException
  438. * if an error occured while parsing the array
  439. */
  440. private String scanString(byte[] input, int offset, int end, int[] lineNr)
  441. throws XMLParseException {
  442. byte delim = input[offset];
  443. if ((delim == '"') || (delim == '\'')) {
  444. int begin = offset;
  445. offset++;
  446. while ((offset < end) && (input[offset] != delim)) {
  447. if (input[offset] == '\r') {
  448. lineNr[0]++;
  449. if ((offset != end) && (input[offset + 1] == '\n')) {
  450. offset++;
  451. }
  452. } else if (input[offset] == '\n') {
  453. lineNr[0]++;
  454. }
  455. offset++;
  456. }
  457. if (offset == end) {
  458. return null;
  459. } else {
  460. return new String(input, begin, offset - begin + 1);
  461. }
  462. } else {
  463. return this.scanIdentifier(input, offset, end);
  464. }
  465. }
  466. /**
  467. * Scans the class (tag) name of the object.
  468. *
  469. * @return the position after the tag name
  470. *
  471. * @exception nanoxml.XMLParseException
  472. * if an error occured while parsing the array
  473. */
  474. private int scanTagName(byte[] input, int offset, int end, int[] lineNr)
  475. throws XMLParseException {
  476. this.tagName = this.scanIdentifier(input, offset, end);
  477. if (this.tagName == null) {
  478. throw this.syntaxError("a tag name", lineNr[0]);
  479. }
  480. return offset + this.tagName.length();
  481. }
  482. /**
  483. * Changes the content string.
  484. *
  485. * @param content
  486. * The new content string.
  487. */
  488. public void setContent(String content) {
  489. this.contents = content;
  490. }
  491. /**
  492. * Changes the tag name.
  493. *
  494. * @param tagName
  495. * The new tag name.
  496. */
  497. public void setTagName(String tagName) {
  498. this.tagName = tagName;
  499. }
  500. /**
  501. * Skips a tag that don't contain any useful data: <?...?>,
  502. * <!...> and comments.
  503. *
  504. * @return the position after the tag
  505. *
  506. * @exception nanoxml.XMLParseException
  507. * if an error occured while parsing the array
  508. */
  509. protected int skipBogusTag(byte[] input, int offset, int end, int[] lineNr) {
  510. if ((input[offset + 1] == '-') && (input[offset + 2] == '-')) {
  511. while ((offset < end)
  512. && ((input[offset] != '-') || (input[offset + 1] != '-') || (input[offset + 2] != '>'))) {
  513. if (input[offset] == '\r') {
  514. lineNr[0]++;
  515. if ((offset != end) && (input[offset + 1] == '\n')) {
  516. offset++;
  517. }
  518. } else if (input[offset] == '\n') {
  519. lineNr[0]++;
  520. }
  521. offset++;
  522. }
  523. if (offset == end) {
  524. throw unexpectedEndOfData(lineNr[0]);
  525. } else {
  526. return offset + 3;
  527. }
  528. }
  529. int level = 1;
  530. while (offset < end) {
  531. byte ch = input[offset++];
  532. switch (ch) {
  533. case '\r':
  534. if ((offset < end) && (input[offset] == '\n')) {
  535. offset++;
  536. }
  537. lineNr[0]++;
  538. break;
  539. case '\n':
  540. lineNr[0]++;
  541. break;
  542. case '<':
  543. level++;
  544. break;
  545. case '>':
  546. level--;
  547. if (level == 0) {
  548. return offset;
  549. }
  550. break;
  551. default:
  552. }
  553. }
  554. throw this.unexpectedEndOfData(lineNr[0]);
  555. }
  556. /**
  557. * Skips a tag that don't contain any useful data: <?...?>,
  558. * <!...> and comments.
  559. *
  560. * @return the position after the tag
  561. *
  562. * @exception nanoxml.XMLParseException
  563. * if an error occured while parsing the array
  564. */
  565. private int skipPreamble(byte[] input, int offset, int end, int[] lineNr)
  566. throws XMLParseException {
  567. byte ch;
  568. do {
  569. offset = this.skipWhitespace(input, offset, end, lineNr);
  570. if (input[offset] != '<') {
  571. this.expectedInput("'<'", lineNr[0]);
  572. }
  573. offset++;
  574. if (offset >= end) {
  575. throw this.unexpectedEndOfData(lineNr[0]);
  576. }
  577. ch = input[offset];
  578. if ((ch == '!') || (ch == '?')) {
  579. offset = this.skipBogusTag(input, offset, end, lineNr);
  580. }
  581. } while (!isIdentifierChar(ch));
  582. return offset;
  583. }
  584. /**
  585. * Skips whitespace characters.
  586. *
  587. * @return the position after the whitespace
  588. *
  589. * @exception nanoxml.XMLParseException
  590. * if an error occured while parsing the array
  591. */
  592. private int skipWhitespace(byte[] input, int offset, int end, int[] lineNr) {
  593. while ((offset < end) && (input[offset] <= ' ')) {
  594. if (input[offset] == '\r') {
  595. lineNr[0]++;
  596. if ((offset != end) && (input[offset + 1] == '\n')) {
  597. offset++;
  598. }
  599. } else if (input[offset] == '\n') {
  600. lineNr[0]++;
  601. }
  602. offset++;
  603. }
  604. if (offset == end) {
  605. throw this.unexpectedEndOfData(lineNr[0]);
  606. }
  607. return offset;
  608. }
  609. /**
  610. * Converts &...; sequences to "normal" chars.
  611. */
  612. protected String decodeString(String s, int lineNr) {
  613. StringBuffer result = new StringBuffer(s.length());
  614. int index = 0;
  615. while (index < s.length()) {
  616. int index2 = (s + '&').indexOf('&', index);
  617. int index3 = (s + "<![CDATA[").indexOf("<![CDATA[", index);
  618. if (index2 <= index3) {
  619. result.append(s.substring(index, index2));
  620. if (index2 == s.length()) {
  621. break;
  622. }
  623. index = s.indexOf(';', index2);
  624. if (index < 0) {
  625. result.append(s.substring(index2));
  626. break;
  627. }
  628. String key = s.substring(index2 + 1, index);
  629. if (key.charAt(0) == '#') {
  630. if (key.charAt(1) == 'x') {
  631. result.append((char) (Integer.parseInt(
  632. key.substring(2), 16)));
  633. } else {
  634. result.append((char) (Integer.parseInt(
  635. key.substring(1), 10)));
  636. }
  637. } else {
  638. result.append(this.conversionTable.getProperty(key, "&"
  639. + key + ';'));
  640. }
  641. } else {
  642. int index4 = (s + "]]>").indexOf("]]>", index3 + 9);
  643. result.append(s.substring(index, index3));
  644. result.append(s.substring(index3 + 9, index4));
  645. index = index4 + 2;
  646. }
  647. index++;
  648. }
  649. return result.toString();
  650. }
  651. /**
  652. * Creates a parse exception for when an invalid valueset is given to a
  653. * method.
  654. */
  655. private XMLParseException invalidValueSet(String key) {
  656. String msg = "Invalid value set (key = \"" + key + "\")";
  657. return new XMLParseException(this.getTagName(), msg);
  658. }
  659. /**
  660. * Creates a parse exception for when an invalid value is given to a method.
  661. */
  662. private XMLParseException invalidValue(String key, String value, int lineNr) {
  663. String msg = "Attribute \"" + key + "\" does not contain a valid "
  664. + "value (\"" + value + "\")";
  665. return new XMLParseException(this.getTagName(), lineNr, msg);
  666. }
  667. /**
  668. * The end of the data input has been reached.
  669. */
  670. private XMLParseException unexpectedEndOfData(int lineNr) {
  671. String msg = "Unexpected end of data reached";
  672. return new XMLParseException(this.getTagName(), lineNr, msg);
  673. }
  674. /**
  675. * A syntax error occured.
  676. */
  677. private XMLParseException syntaxError(String context, int lineNr) {
  678. String msg = "Syntax error while parsing " + context;
  679. return new XMLParseException(this.getTagName(), lineNr, msg);
  680. }
  681. /**
  682. * A character has been expected.
  683. */
  684. private XMLParseException expectedInput(String charSet, int lineNr) {
  685. String msg = "Expected: " + charSet;
  686. return new XMLParseException(this.getTagName(), lineNr, msg);
  687. }
  688. /**
  689. * A value is missing for an attribute.
  690. */
  691. private XMLParseException valueMissingForAttribute(String key, int lineNr) {
  692. String msg = "Value missing for attribute with key \"" + key + "\"";
  693. return new XMLParseException(this.getTagName(), lineNr, msg);
  694. }
  695. // ------------------------------------------------------------------
  696. public static class FakeProperties extends Hashtable {
  697. /**
  698. */
  699. public FakeProperties() {
  700. }
  701. /**
  702. */
  703. public String getProperty(String key) {
  704. return (String) super.get(key);
  705. }
  706. /**
  707. */
  708. public String getProperty(String key, String defstr) {
  709. String val = getProperty(key);
  710. return ((val != null) ? val : defstr);
  711. }
  712. }
  713. }