KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > go > trove > util > PropertyParser


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 JavaDoc;
57 import java.io.Reader JavaDoc;
58 import java.io.IOException JavaDoc;
59 import java.io.Serializable JavaDoc;
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 JavaDoc str, SourceInfo info) {
168         dispatchParseError(new ErrorEvent(this, str, info));
169     }
170
171     private void error(String JavaDoc 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 JavaDoc reader) throws IOException JavaDoc {
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 JavaDoc {
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 JavaDoc keyPrefix) throws IOException JavaDoc {
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 JavaDoc keyPrefix, Token token)
249         throws IOException JavaDoc {
250
251         String JavaDoc key = token.getValue();
252         if (keyPrefix != null) {
253             key = keyPrefix + key;
254         }
255
256         String JavaDoc 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 JavaDoc keyPrefix) throws IOException JavaDoc {
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 JavaDoc key, String JavaDoc 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 JavaDoc {
304         return mScanner.readToken();
305     }
306
307     private Token peek() throws IOException JavaDoc {
308         return mScanner.peekToken();
309     }
310
311     private void unread(Token token) throws IOException JavaDoc {
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 JavaDoc {
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 JavaDoc {
332         private String JavaDoc mErrorMsg;
333         private SourceInfo mInfo;
334
335         ErrorEvent(Object JavaDoc source, String JavaDoc errorMsg, SourceInfo info) {
336             super(source);
337             mErrorMsg = errorMsg;
338             mInfo = info;
339         }
340         
341         public String JavaDoc getErrorMessage() {
342             return mErrorMsg;
343         }
344         
345         /**
346          * Returns the error message prepended with source file information.
347          */

348         public String JavaDoc getDetailedErrorMessage() {
349             String JavaDoc 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 JavaDoc 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 JavaDoc {
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 JavaDoc mValue;
398         private SourceInfo mInfo;
399
400         Token(int sourceLine,
401               int sourceStartPos,
402               int sourceEndPos,
403               int tokenId,
404               String JavaDoc value) {
405             
406             mTokenId = tokenId;
407             mValue = value;
408             
409             if (tokenId > LAST_ID) {
410                 throw new IllegalArgumentException JavaDoc("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 JavaDoc
419                     ("Token start position greater than " +
420                      "end position at line: " + sourceLine);
421             }
422         }
423     
424         public Token(SourceInfo info, int tokenId, String JavaDoc value) {
425             mTokenId = tokenId;
426         
427             if (tokenId > LAST_ID) {
428                 throw new IllegalArgumentException JavaDoc("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 JavaDoc getCode() {
444             return Code.TOKEN_CODES[mTokenId];
445         }
446
447         public final SourceInfo getSourceInfo() {
448             return mInfo;
449         }
450         
451         public String JavaDoc getValue() {
452             return mValue;
453         }
454
455         public String JavaDoc toString() {
456             StringBuffer JavaDoc buf = new StringBuffer JavaDoc(10);
457
458             String JavaDoc image = getCode();
459             
460             if (image != null) {
461                 buf.append(image);
462             }
463             
464             String JavaDoc 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 JavaDoc[] 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 JavaDoc 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 JavaDoc str, SourceInfo info) {
535             dispatchParseError(new ErrorEvent(this, str, info));
536         }
537         
538         private void error(String JavaDoc 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 JavaDoc {
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 JavaDoc {
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 JavaDoc {
569             mLookahead.push(token);
570         }
571         
572         public void close() throws IOException JavaDoc {
573             mSource.close();
574         }
575
576         public int getErrorCount() {
577             return mErrorCount;
578         }
579         
580         private Token scanToken() throws IOException JavaDoc {
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 JavaDoc {
657             StringBuffer JavaDoc buf = new StringBuffer JavaDoc(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 JavaDoc 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 JavaDoc {
783             StringBuffer JavaDoc buf = new StringBuffer JavaDoc(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 JavaDoc buf)
806             throws IOException JavaDoc {
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 JavaDoc value) {
856             return new Token(mSource.getLineNumber(),
857                              mSource.getStartPosition(),
858                              mSource.getEndPosition(),
859                              Id, value);
860         }
861     }
862 }
863
Popular Tags