comparison src/com/go/trove/util/PropertyParser.java @ 0:3dc0c5604566

Initial checkin of blitz 2.0 fcs - no installer yet.
author Dan Creswell <dan.creswell@gmail.com>
date Sat, 21 Mar 2009 11:00:06 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:3dc0c5604566
1 /* ====================================================================
2 * Trove - Copyright (c) 1997-2000 Walt Disney Internet Group
3 * ====================================================================
4 * The Tea Software License, Version 1.1
5 *
6 * Copyright (c) 2000 Walt Disney Internet Group. All rights reserved.
7 *
8 * Redistribution and use in source and binary forms, with or without
9 * modification, are permitted provided that the following conditions
10 * are met:
11 *
12 * 1. Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
14 *
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in
17 * the documentation and/or other materials provided with the
18 * distribution.
19 *
20 * 3. The end-user documentation included with the redistribution,
21 * if any, must include the following acknowledgment:
22 * "This product includes software developed by the
23 * Walt Disney Internet Group (http://opensource.go.com/)."
24 * Alternately, this acknowledgment may appear in the software itself,
25 * if and wherever such third-party acknowledgments normally appear.
26 *
27 * 4. The names "Tea", "TeaServlet", "Kettle", "Trove" and "BeanDoc" must
28 * not be used to endorse or promote products derived from this
29 * software without prior written permission. For written
30 * permission, please contact opensource@dig.com.
31 *
32 * 5. Products derived from this software may not be called "Tea",
33 * "TeaServlet", "Kettle" or "Trove", nor may "Tea", "TeaServlet",
34 * "Kettle", "Trove" or "BeanDoc" appear in their name, without prior
35 * written permission of the Walt Disney Internet Group.
36 *
37 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
38 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
39 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
40 * DISCLAIMED. IN NO EVENT SHALL THE WALT DISNEY INTERNET GROUP OR ITS
41 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
42 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
43 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
44 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
45 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
46 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
47 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48 * ====================================================================
49 *
50 * For more information about Tea, please see http://opensource.go.com/.
51 */
52
53 package com.go.trove.util;
54
55 import java.util.*;
56 import java.io.InputStream;
57 import java.io.Reader;
58 import java.io.IOException;
59 import java.io.Serializable;
60 import com.go.trove.io.SourceInfo;
61 import com.go.trove.io.SourceReader;
62
63 /******************************************************************************
64 * Parses a properties file similar to how {@link java.util.Properties} does,
65 * except:
66 *
67 * <ul>
68 * <li>Values have trailing whitespace trimmed.
69 * <li>Quotation marks ( " or ' ) can be used to define keys and values that
70 * have embedded spaces.
71 * <li>Quotation marks can also be used to define multi-line keys and values
72 * without having to use continuation characters.
73 * <li>Properties may be nested using braces '{' and '}'.
74 * </ul>
75 *
76 * Just like Properties, comment lines start with optional whitespace followed
77 * by a '#' or '!'. Keys and values may have an optional '=' or ':' as a
78 * separator, unicode escapes are supported as well as other common escapes.
79 * A line may end in a backslash so that it continues to the next line.
80 * Escapes for brace characters '{' and '}' are also supported.
81 *
82 * Example:
83 *
84 * <pre>
85 * # Properties file
86 *
87 * foo = bar
88 * foo.sub = blink
89 * empty
90 *
91 * block {
92 * inner {
93 * foo = bar
94 * item
95 * }
96 * next.item = "true"
97 * }
98 *
99 * section = test {
100 * level = 4
101 * message = "Message: "
102 * }
103 * </pre>
104 *
105 * is equivalent to
106 *
107 * <pre>
108 * # Properties file
109 *
110 * foo = bar
111 * foo.sub = blink
112 * empty
113 *
114 * block.inner.foo = bar
115 * block.inner.item
116 * block.next.item = true
117 *
118 * section = test
119 * section.level = 4
120 * section.message = Message:
121 * </pre>
122 *
123 * @author Brian S O'Neill
124 * @version
125 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $-->
126 */
127 public class PropertyParser {
128 // Parsed grammer (EBNF) is:
129 //
130 // Properties ::= { PropertyList }
131 // PropertyList ::= { Property | COMMENT }
132 // Property ::= KEY [ VALUE ] [ Block ]
133 // Block ::= LBRACE PropertyList RBRACE
134
135 private Map mMap;
136
137 private Vector mListeners = new Vector(1);
138 private int mErrorCount = 0;
139
140 private Scanner mScanner;
141
142 /**
143 * @param map Map to receive properties
144 */
145 public PropertyParser(Map map) {
146 mMap = map;
147 }
148
149 public void addErrorListener(ErrorListener listener) {
150 mListeners.addElement(listener);
151 }
152
153 public void removeErrorListener(ErrorListener listener) {
154 mListeners.removeElement(listener);
155 }
156
157 private void dispatchParseError(ErrorEvent e) {
158 mErrorCount++;
159
160 synchronized (mListeners) {
161 for (int i = 0; i < mListeners.size(); i++) {
162 ((ErrorListener)mListeners.elementAt(i)).parseError(e);
163 }
164 }
165 }
166
167 private void error(String str, SourceInfo info) {
168 dispatchParseError(new ErrorEvent(this, str, info));
169 }
170
171 private void error(String str, Token token) {
172 error(str, token.getSourceInfo());
173 }
174
175 /**
176 * Parses properties from the given reader and stores them in the Map. To
177 * capture any parsing errors, call addErrorListener prior to parsing.
178 */
179 public void parse(Reader reader) throws IOException {
180 mScanner = new Scanner(reader);
181
182 mScanner.addErrorListener(new ErrorListener() {
183 public void parseError(ErrorEvent e) {
184 dispatchParseError(e);
185 }
186 });
187
188 try {
189 parseProperties();
190 }
191 finally {
192 mScanner.close();
193 }
194 }
195
196 private void parseProperties() throws IOException {
197 Token token;
198 while ((token = peek()).getId() != Token.EOF) {
199 switch (token.getId()) {
200
201 case Token.KEY:
202 case Token.LBRACE:
203 case Token.COMMENT:
204 parsePropertyList(null);
205 break;
206
207 case Token.RBRACE:
208 token = read();
209 error("No matching left brace", token);
210 break;
211
212 default:
213 token = read();
214 error("Unexpected token: " + token.getValue(), token);
215 break;
216 }
217 }
218 }
219
220 private void parsePropertyList(String keyPrefix) throws IOException {
221 Token token;
222
223 loop:
224 while ((token = peek()).getId() != Token.EOF) {
225 switch (token.getId()) {
226
227 case Token.KEY:
228 token = read();
229 parseProperty(keyPrefix, token);
230 break;
231
232 case Token.COMMENT:
233 read();
234 break;
235
236 case Token.LBRACE:
237 read();
238 error("Nested properties must have a base name", token);
239 parseBlock(keyPrefix);
240 break;
241
242 default:
243 break loop;
244 }
245 }
246 }
247
248 private void parseProperty(String keyPrefix, Token token)
249 throws IOException {
250
251 String key = token.getValue();
252 if (keyPrefix != null) {
253 key = keyPrefix + key;
254 }
255
256 String value = null;
257
258 if (peek().getId() == Token.VALUE) {
259 token = read();
260 value = token.getValue();
261 }
262
263 if (peek().getId() == Token.LBRACE) {
264 read();
265 parseBlock(key + '.');
266 }
267 else if (value == null) {
268 value = "";
269 }
270
271 if (value != null) {
272 putProperty(key, value, token);
273 }
274 }
275
276 // When this is called, the LBRACE token has already been read.
277 private void parseBlock(String keyPrefix) throws IOException {
278 parsePropertyList(keyPrefix);
279
280 Token token;
281 if ((token = peek()).getId() == Token.RBRACE) {
282 read();
283 }
284 else {
285 error("Right brace expected", token);
286 }
287 }
288
289 private void putProperty(String key, String value, Token token) {
290 if (mMap.containsKey(key)) {
291 error("Property \"" + key + "\" already defined", token);
292 }
293 mMap.put(key, value);
294 }
295
296 /**
297 * Total number of errors accumulated by this PropertyParser instance.
298 */
299 public int getErrorCount() {
300 return mErrorCount;
301 }
302
303 private Token read() throws IOException {
304 return mScanner.readToken();
305 }
306
307 private Token peek() throws IOException {
308 return mScanner.peekToken();
309 }
310
311 private void unread(Token token) throws IOException {
312 mScanner.unreadToken(token);
313 }
314
315 /**************************************************************************
316 *
317 * @author Brian S O'Neill
318 * @version
319 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $-->
320 */
321 public static interface ErrorListener extends java.util.EventListener {
322 public void parseError(ErrorEvent e);
323 }
324
325 /**************************************************************************
326 *
327 * @author Brian S O'Neill
328 * @version
329 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $-->
330 */
331 public static class ErrorEvent extends java.util.EventObject {
332 private String mErrorMsg;
333 private SourceInfo mInfo;
334
335 ErrorEvent(Object source, String errorMsg, SourceInfo info) {
336 super(source);
337 mErrorMsg = errorMsg;
338 mInfo = info;
339 }
340
341 public String getErrorMessage() {
342 return mErrorMsg;
343 }
344
345 /**
346 * Returns the error message prepended with source file information.
347 */
348 public String getDetailedErrorMessage() {
349 String prepend = getSourceInfoMessage();
350 if (prepend == null || prepend.length() == 0) {
351 return mErrorMsg;
352 }
353 else {
354 return prepend + ": " + mErrorMsg;
355 }
356 }
357
358 public String getSourceInfoMessage() {
359 if (mInfo == null) {
360 return "";
361 }
362 else {
363 return String.valueOf(mInfo.getLine());
364 }
365 }
366
367 /**
368 * This method reports on where in the source code an error was found.
369 *
370 * @return Source information on this error or null if not known.
371 */
372 public SourceInfo getSourceInfo() {
373 return mInfo;
374 }
375 }
376
377 /**************************************************************************
378 *
379 * @author Brian S O'Neill
380 * @version
381 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $-->
382 */
383 private static class Token implements java.io.Serializable {
384 public final static int UNKNOWN = 0;
385 public final static int EOF = 1;
386
387 public final static int COMMENT = 2;
388 public final static int KEY = 3;
389 public final static int VALUE = 4;
390
391 public final static int LBRACE = 5;
392 public final static int RBRACE = 6;
393
394 private final static int LAST_ID = 6;
395
396 private int mTokenId;
397 private String mValue;
398 private SourceInfo mInfo;
399
400 Token(int sourceLine,
401 int sourceStartPos,
402 int sourceEndPos,
403 int tokenId,
404 String value) {
405
406 mTokenId = tokenId;
407 mValue = value;
408
409 if (tokenId > LAST_ID) {
410 throw new IllegalArgumentException("Token Id out of range: " +
411 tokenId);
412 }
413
414 mInfo = new SourceInfo(sourceLine, sourceStartPos, sourceEndPos);
415
416 if (sourceStartPos > sourceEndPos) {
417 // This is an internal error.
418 throw new IllegalArgumentException
419 ("Token start position greater than " +
420 "end position at line: " + sourceLine);
421 }
422 }
423
424 public Token(SourceInfo info, int tokenId, String value) {
425 mTokenId = tokenId;
426
427 if (tokenId > LAST_ID) {
428 throw new IllegalArgumentException("Token Id out of range: " +
429 tokenId);
430 }
431
432 mInfo = info;
433 }
434
435 public final int getId() {
436 return mTokenId;
437 }
438
439 /**
440 * Token code is non-null, and is exactly the same as the name for
441 * its Id.
442 */
443 public String getCode() {
444 return Code.TOKEN_CODES[mTokenId];
445 }
446
447 public final SourceInfo getSourceInfo() {
448 return mInfo;
449 }
450
451 public String getValue() {
452 return mValue;
453 }
454
455 public String toString() {
456 StringBuffer buf = new StringBuffer(10);
457
458 String image = getCode();
459
460 if (image != null) {
461 buf.append(image);
462 }
463
464 String str = getValue();
465
466 if (str != null) {
467 if (image != null) {
468 buf.append(' ');
469 }
470 buf.append('"');
471 buf.append(str);
472 buf.append('"');
473 }
474
475 return buf.toString();
476 }
477
478 private static class Code {
479 public static final String[] TOKEN_CODES =
480 {
481 "UNKNOWN",
482 "EOF",
483
484 "COMMENT",
485 "KEY",
486 "VALUE",
487
488 "LBRACE",
489 "RBRACE",
490 };
491 }
492 }
493
494 /**************************************************************************
495 *
496 * @author Brian S O'Neill
497 * @version
498 * <!--$$Revision: 1.1 $-->, <!--$$JustDate:--> 12/11/00 <!-- $-->
499 */
500 private static class Scanner {
501 private SourceReader mSource;
502
503 /** The scanner supports any amount of lookahead. */
504 private Stack mLookahead = new Stack();
505
506 private boolean mScanKey = true;
507 private Token mEOFToken;
508
509 private Vector mListeners = new Vector(1);
510 private int mErrorCount = 0;
511
512 public Scanner(Reader in) {
513 mSource = new SourceReader(in, null, null);
514 }
515
516 public void addErrorListener(ErrorListener listener) {
517 mListeners.addElement(listener);
518 }
519
520 public void removeErrorListener(ErrorListener listener) {
521 mListeners.removeElement(listener);
522 }
523
524 private void dispatchParseError(ErrorEvent e) {
525 mErrorCount++;
526
527 synchronized (mListeners) {
528 for (int i = 0; i < mListeners.size(); i++) {
529 ((ErrorListener)mListeners.elementAt(i)).parseError(e);
530 }
531 }
532 }
533
534 private void error(String str, SourceInfo info) {
535 dispatchParseError(new ErrorEvent(this, str, info));
536 }
537
538 private void error(String str) {
539 error(str, new SourceInfo(mSource.getLineNumber(),
540 mSource.getStartPosition(),
541 mSource.getEndPosition()));
542 }
543
544 /**
545 * Returns EOF as the last token.
546 */
547 public synchronized Token readToken() throws IOException {
548 if (mLookahead.empty()) {
549 return scanToken();
550 }
551 else {
552 return (Token)mLookahead.pop();
553 }
554 }
555
556 /**
557 * Returns EOF as the last token.
558 */
559 public synchronized Token peekToken() throws IOException {
560 if (mLookahead.empty()) {
561 return (Token)mLookahead.push(scanToken());
562 }
563 else {
564 return (Token)mLookahead.peek();
565 }
566 }
567
568 public synchronized void unreadToken(Token token) throws IOException {
569 mLookahead.push(token);
570 }
571
572 public void close() throws IOException {
573 mSource.close();
574 }
575
576 public int getErrorCount() {
577 return mErrorCount;
578 }
579
580 private Token scanToken() throws IOException {
581 if (mSource.isClosed()) {
582 if (mEOFToken == null) {
583 mEOFToken = makeToken(Token.EOF, null);
584 }
585
586 return mEOFToken;
587 }
588
589 int c;
590 int peek;
591
592 int startPos;
593
594 while ( (c = mSource.read()) != -1 ) {
595 switch (c) {
596
597 case SourceReader.ENTER_CODE:
598 case SourceReader.ENTER_TEXT:
599 continue;
600
601 case '#':
602 case '!':
603 mScanKey = true;
604 return scanComment();
605
606 case '{':
607 mScanKey = true;
608 return makeToken(Token.LBRACE, "{");
609 case '}':
610 mScanKey = true;
611 return makeToken(Token.RBRACE, "}");
612
613 case '0': case '1': case '2': case '3': case '4':
614 case '5': case '6': case '7': case '8': case '9':
615 case 'a': case 'b': case 'c': case 'd': case 'e':
616 case 'f': case 'g': case 'h': case 'i': case 'j':
617 case 'k': case 'l': case 'm': case 'n': case 'o':
618 case 'p': case 'q': case 'r': case 's': case 't':
619 case 'u': case 'v': case 'w': case 'x': case 'y':
620 case 'z': case '.':
621 case 'A': case 'B': case 'C': case 'D': case 'E':
622 case 'F': case 'G': case 'H': case 'I': case 'J':
623 case 'K': case 'L': case 'M': case 'N': case 'O':
624 case 'P': case 'Q': case 'R': case 'S': case 'T':
625 case 'U': case 'V': case 'W': case 'X': case 'Y':
626 case 'Z': case '_':
627 mSource.unread();
628 return scanKeyOrValue();
629
630 case '\n':
631 mScanKey = true;
632 // fall through
633 case ' ':
634 case '\0':
635 case '\t':
636 continue;
637
638 default:
639 if (Character.isWhitespace((char)c)) {
640 continue;
641 }
642 else {
643 mSource.unread();
644 return scanKeyOrValue();
645 }
646 }
647 }
648
649 if (mEOFToken == null) {
650 mEOFToken = makeToken(Token.EOF, null);
651 }
652
653 return mEOFToken;
654 }
655
656 private Token scanKeyOrValue() throws IOException {
657 StringBuffer buf = new StringBuffer(40);
658 boolean trim = true;
659
660 int startLine = mSource.getLineNumber();
661 int startPos = mSource.getStartPosition();
662 int endPos = mSource.getEndPosition();
663
664 boolean skipWhitespace = true;
665 boolean skipSeparator = true;
666
667 int c;
668 loop:
669 while ( (c = mSource.read()) != -1 ) {
670 switch (c) {
671
672 case '\n':
673 mSource.unread();
674 break loop;
675
676 case '\\':
677 int next = mSource.read();
678 if (next == -1 || next == '\n') {
679 // line continuation
680 skipWhitespace = true;
681 continue;
682 }
683
684 c = processEscape(c, next);
685 skipWhitespace = false;
686 break;
687
688 case '{':
689 case '}':
690 mSource.unread();
691 break loop;
692
693 case '=':
694 case ':':
695 if (mScanKey) {
696 mSource.unread();
697 break loop;
698 }
699 else if (skipSeparator) {
700 skipSeparator = false;
701 continue;
702 }
703 skipWhitespace = false;
704 break;
705
706 case '\'':
707 case '"':
708 if (buf.length() == 0) {
709 scanStringLiteral(c, buf);
710 endPos = mSource.getEndPosition();
711 trim = false;
712 break loop;
713 }
714 // fall through
715 case '0': case '1': case '2': case '3': case '4':
716 case '5': case '6': case '7': case '8': case '9':
717 case 'a': case 'b': case 'c': case 'd': case 'e':
718 case 'f': case 'g': case 'h': case 'i': case 'j':
719 case 'k': case 'l': case 'm': case 'n': case 'o':
720 case 'p': case 'q': case 'r': case 's': case 't':
721 case 'u': case 'v': case 'w': case 'x': case 'y':
722 case 'z': case '.':
723 case 'A': case 'B': case 'C': case 'D': case 'E':
724 case 'F': case 'G': case 'H': case 'I': case 'J':
725 case 'K': case 'L': case 'M': case 'N': case 'O':
726 case 'P': case 'Q': case 'R': case 'S': case 'T':
727 case 'U': case 'V': case 'W': case 'X': case 'Y':
728 case 'Z': case '_':
729 skipWhitespace = false;
730 break;
731
732 case ' ':
733 case '\0':
734 case '\t':
735 if (skipWhitespace) {
736 continue;
737 }
738 if (mScanKey) {
739 break loop;
740 }
741 break;
742
743 default:
744 if (Character.isWhitespace((char)c)) {
745 if (skipWhitespace) {
746 continue;
747 }
748 if (mScanKey) {
749 break loop;
750 }
751 }
752 else {
753 skipWhitespace = false;
754 }
755 break;
756 }
757
758 buf.append((char)c);
759 endPos = mSource.getEndPosition();
760 skipSeparator = false;
761 }
762
763 int tokenId;
764 if (mScanKey) {
765 tokenId = Token.KEY;
766 mScanKey = false;
767 }
768 else {
769 tokenId = Token.VALUE;
770 mScanKey = true;
771 }
772
773 String value = buf.toString();
774
775 if (trim) {
776 value = value.trim();
777 }
778
779 return new Token(startLine, startPos, endPos, tokenId, value);
780 }
781
782 private Token scanComment() throws IOException {
783 StringBuffer buf = new StringBuffer(40);
784
785 int startLine = mSource.getLineNumber();
786 int startPos = mSource.getStartPosition();
787 int endPos = mSource.getEndPosition();
788
789 int c;
790 while ( (c = mSource.peek()) != -1 ) {
791 if (c == '\n') {
792 break;
793 }
794
795 mSource.read();
796 buf.append((char)c);
797
798 endPos = mSource.getEndPosition();
799 }
800
801 return new Token(startLine, startPos, endPos,
802 Token.COMMENT, buf.toString());
803 }
804
805 private void scanStringLiteral(int quote, StringBuffer buf)
806 throws IOException {
807
808 int c;
809 while ( (c = mSource.read()) != -1 ) {
810 if (c == quote) {
811 return;
812 }
813
814 if (c == '\\') {
815 int next = mSource.read();
816 if (next == -1 || next == '\n') {
817 // line continuation
818 continue;
819 }
820 c = processEscape(c, next);
821 }
822
823 buf.append((char)c);
824 }
825 }
826
827 private int processEscape(int c, int next) {
828 switch (next) {
829 case '0':
830 return '\0';
831 case 't':
832 return '\t';
833 case 'n':
834 return '\n';
835 case 'f':
836 return '\f';
837 case 'r':
838 return '\r';
839
840 case '\\':
841 case '\'':
842 case '\"':
843 case '=':
844 case ':':
845 case '{':
846 case '}':
847 return next;
848
849 default:
850 error("Invalid escape code: \\" + (char)next);
851 return next;
852 }
853 }
854
855 private Token makeToken(int Id, String value) {
856 return new Token(mSource.getLineNumber(),
857 mSource.getStartPosition(),
858 mSource.getEndPosition(),
859 Id, value);
860 }
861 }
862 }