Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/go/trove/util/PropertyParser.java


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:--> 8 <!-- $-->, <!--$$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:--> 8 <!-- $-->, <!--$$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:--> 8 <!-- $-->, <!--$$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:--> 8 <!-- $-->, <!--$$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:--> 8 <!-- $-->, <!--$$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 }