View Javadoc

1   /*
2    * Copyright (c) 2004-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.mercury.imap.util;
14  
15  import java.io.IOException;
16  import java.io.InputStream;
17  import java.io.OutputStream;
18  import java.util.ArrayList;
19  import java.util.Calendar;
20  import java.util.GregorianCalendar;
21  import java.util.List;
22  import java.util.TimeZone;
23  import javax.mail.Flags;
24  import org.abstracthorizon.mercury.imap.util.section.HeaderSection;
25  import org.abstracthorizon.mercury.imap.util.section.MimeSection;
26  import org.abstracthorizon.mercury.imap.util.section.MultipartSection;
27  import org.abstracthorizon.mercury.imap.util.section.PointerSection;
28  import org.abstracthorizon.mercury.imap.util.section.TextSection;
29  
30  
31  /**
32   * A class implementing a lexical scanner for an IMAP server.
33   *
34   * @author Daniel Sendula
35   */
36  public class IMAPScanner {
37  
38      /** Input stream */
39      protected InputStream in;
40  
41      /** Output stream */
42      protected OutputStream out;
43  
44      /** Buffer */
45      protected char[] buffer;
46  
47      /** Is literal recognised */
48      protected boolean literal;
49  
50      /** Current pointer */
51      protected int ptr = -1;
52  
53      /** Have we reached EOL */
54      protected boolean eol = false;
55  
56      /**
57       * Constructor
58       * @param in input stream
59       * @param out output stream
60       */
61      public IMAPScanner(InputStream in, OutputStream out) {
62          this.in = in;
63          this.out = out;
64          in.mark(128);
65      }
66  
67      protected boolean atom_char(char c) {
68          if ((c >= 31) &&
69              (c != '(') && (c != ')') && (c != '{') && (c != ' ') &&
70              (c != '%') && (c != '*') && (c != '"') && (c != ']') &&
71              (c != '\\')
72             )
73          {
74              return true;
75          }
76          return false;
77      }
78  
79      protected boolean astring_char(char c) {
80          if (atom_char(c) || (c == ']')) {
81              return true;
82          }
83          return false;
84      }
85  
86      protected boolean text_char(char c) {
87          if ((c != '\r') && (c != '\n')) {
88              return true;
89          }
90          return false;
91      }
92  
93      protected boolean digit_nz(char c) {
94          return (c >= '1') && (c <= '9');
95      }
96  
97      protected boolean digit(char c) {
98          return (c >= '0') && (c <= '9');
99      }
100 
101     protected boolean list_char(char c) {
102         return atom_char(c) || (c == '%') || (c == '*') || (c == ']');
103     }
104 
105 
106     public void skip_line() throws IOException {
107         if (eol) {
108             eol = false;
109             return;
110         }
111         try {
112             int i = in.read();
113             char c = (char)i;
114             while (i >= 0) {
115                 while ((i >= 0) && (c != '\r')) {
116                     i = in.read();
117                     c = (char)i;
118                 } // while
119                 i = in.read();
120                 c = (char)i;
121                 if (c == '\n') {
122                     return;
123                 }
124             } // while
125         } catch (IOException e) {
126         }
127     }
128 
129     public void check_eol() throws IOException, ParserException {
130         try {
131             char c = (char)in.read();
132             if (c != '\r') {
133                 throw new ParserException("<CR>");
134             }
135             c = (char)in.read();
136             if (c != '\n') {
137                 throw new ParserException("<LF>");
138             }
139         } catch (IOException e) {
140         }
141         eol = true;
142     }
143 
144     public boolean is_char(char r) throws IOException {
145         in.mark(1);
146         char c = (char)in.read();
147         if (c == r) {
148             return true;
149         }
150         in.reset();
151         return false;
152     }
153 
154     public boolean peek_char(char r) throws IOException {
155         in.mark(1);
156         char c = (char)in.read();
157         in.reset();
158         if (c == r) {
159             return true;
160         }
161         return false;
162     }
163 
164     public boolean number(Number num) throws IOException {
165         in.mark(10);
166         int len = 0;
167         num.number = 0;
168         char c = (char)in.read();
169         if ((c < '0') && (c > '9')) {
170             in.reset();
171             return false;
172         }
173         num.number = num.number*10 + (c-'0');
174         len = len + 1;
175         c = (char)in.read();
176         while ((c >= '0') && (c <= '9')) {
177             num.number = num.number*10 + (c-'0');
178             len = len + 1;
179             c = (char)in.read();
180         } // while
181         in.reset();
182         in.skip(len);
183         return true;
184     }
185 
186     public boolean nz_number(Number num) throws IOException {
187         in.mark(10);
188         int len = 0;
189         num.number = 0;
190         char c = (char)in.read();
191         if ((c < '1') || (c > '9')) {
192             in.reset();
193             return false;
194         }
195         num.number = num.number*10 + (c-'0');
196         len = len + 1;
197         c = (char)in.read();
198         while ((c >= '0') && (c <= '9')) {
199             num.number = num.number*10 + (c-'0');
200             len = len + 1;
201             c = (char)in.read();
202         } // while
203         in.reset();
204         in.skip(len);
205         return true;
206     }
207 
208     public boolean tag(StringBuffer res) throws IOException {
209         in.mark(128);
210         int i = in.read();
211         char c = (char)i;
212         if ((i != -1) && (c != '+') && astring_char(c)) {
213             res.append(c);
214             i = in.read();
215             c = (char)i;
216             while ((i != -1) && (c != '+') && astring_char(c)) {
217                 res.append(c);
218                 i = in.read();
219                 c = (char)i;
220             }
221             in.reset();
222             in.skip(res.length());
223             return true;
224         }
225         in.reset();
226         res.delete(0, res.length());
227         return false;
228     }
229 
230     public boolean keyword(String keyword) throws IOException {
231         in.mark(keyword.length()+1);
232         ptr = 0;
233         while (ptr < keyword.length()) {
234             char c = (char)in.read();
235             if (Character.toUpperCase(c) != Character.toUpperCase(keyword.charAt(ptr))) {
236                 in.reset();
237 
238                 return false;
239             }
240             ptr = ptr + 1;
241         } // while
242         return true;
243     }
244 
245     public boolean quoted(StringBuffer quoted) throws IOException, ParserException {
246         in.mark(512);
247         char c = (char)in.read();
248         if (c != '"') {
249             in.reset();
250             return false;
251         }
252         c = (char)in.read();
253         while (text_char(c) && (c != '"')) {
254             if (c == '\\') {
255                 c = (char)in.read();
256                 if ((c == '\\') || (c == '"')) {
257                     quoted.append(c);
258                 } else {
259                     in.reset();
260                     throw new ParserException("'"+c+"'");
261                 }
262             } else {
263                 quoted.append(c);
264             }
265             c = (char)in.read();
266         } // while
267         if (c != '"') {
268             in.reset();
269             throw new ParserException("'\"'");
270         }
271         return true;
272     }
273 
274     public boolean literal(StringBuffer literal) throws IOException, ParserException {
275         in.mark(10);
276         char c = (char)in.read();
277         if (c != '{') {
278             in.reset();
279             return false;
280         }
281         c = (char)in.read();
282         if (!digit(c)) {
283             in.reset();
284             throw new ParserException("<digit>");
285         }
286         int len = (c - '0');
287         c = (char)in.read();
288         while (digit(c)) {
289             len = len*10 + (c - '0');
290             c = (char)in.read();
291         } // while
292 
293         if (c != '}') {
294             in.reset();
295             throw new ParserException("'}'");
296         }
297         c = (char)in.read();
298         if (c != '\r') {
299             in.reset();
300             throw new ParserException("<CR>");
301         }
302         c = (char)in.read();
303         if (c != '\n') {
304             in.reset();
305             throw new ParserException("<LF>");
306         }
307         synchronized (out) {
308             out.write("+ Ready for literal data\r\n".getBytes());
309             out.flush();
310         }
311         byte[] buf = new byte[len];
312         int size = 0;
313         size = in.read(buf);
314         while (size < buf.length) {
315             int s = in.read(buf, size, buf.length-size);
316             if (s >= 0) {
317                 size = size + s;
318             } else {
319                 throw new ParserException(false, "Wrong literal - premature EOF reached!");
320             }
321         }
322         literal.append(new String(buf));
323         return true;
324     }
325 
326     public long raw_literal() throws IOException, ParserException {
327         in.mark(10);
328         char c = (char)in.read();
329         if (c != '{') {
330             in.reset();
331             return -1;
332         }
333         c = (char)in.read();
334         if (!digit(c)) {
335             throw new ParserException("<digit>");
336         }
337         long len = (c - '0');
338         c = (char)in.read();
339         while (digit(c)) {
340             len = len*10 + (c - '0');
341             c = (char)in.read();
342         } // while
343 
344         if (c != '}') {
345             throw new ParserException("'}'");
346         }
347         c = (char)in.read();
348         if (c != '\r') {
349             throw new ParserException("<CR>");
350         }
351         c = (char)in.read();
352         if (c != '\n') {
353             throw new ParserException("<LF>");
354         }
355         synchronized (out) {
356             out.write("+ Ready for literal data\r\n".getBytes());
357             out.flush();
358         }
359         return len;
360     }
361 
362 
363     public boolean string(StringBuffer buffer) throws IOException, ParserException {
364         if (quoted(buffer) || literal(buffer)) {
365             return true;
366         }
367         return false;
368     }
369 
370     public boolean atom(StringBuffer buffer) throws IOException, ParserException {
371         in.mark(128);
372         char c = (char)in.read();
373         if (!atom_char(c)) {
374             in.reset();
375             return false;
376         }
377         buffer.append(c);
378         c = (char)in.read();
379         while (atom_char(c)) {
380             buffer.append(c);
381             c = (char)in.read();
382         }
383         in.reset();
384         in.skip(buffer.length());
385         return true;
386     }
387 
388     public boolean astring(StringBuffer buffer) throws IOException, ParserException {
389         if (string(buffer)) {
390             return true;
391         }
392         in.mark(128);
393         char c = (char)in.read();
394         if (!astring_char(c)) {
395             in.reset();
396             return false;
397         }
398         buffer.append(c);
399         c = (char)in.read();
400         while (astring_char(c)) {
401             buffer.append(c);
402             c = (char)in.read();
403         }
404         in.reset();
405         in.skip(buffer.length());
406         return true;
407     }
408 
409     public boolean readBase64Line(StringBuffer line) throws IOException, ParserException {
410         in.mark(128);
411         char c = (char)in.read();
412         while (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) || ((c >= '0') && (c <= '9')) || (c == '+') || (c == '=') || (c == '/')) {
413             line.append(c);
414             c = (char)in.read();
415         }
416         if (c != '\r') {
417             throw new ParserException("<CR>");
418         }
419         c = (char)in.read();
420         if (c != '\n') {
421             throw new ParserException("<LF>");
422         }
423         return true;
424     }
425 
426 
427     public boolean list_mailbox(StringBuffer buffer) throws IOException, ParserException {
428         if (string(buffer)) {
429             return true;
430         }
431         in.mark(128);
432         char c = (char)in.read();
433         if (!list_char(c)) {
434             in.reset();
435             return false;
436         }
437         buffer.append(c);
438         c = (char)in.read();
439         while (list_char(c)) {
440             buffer.append(c);
441             c = (char)in.read();
442         } // while
443         in.reset();
444         in.skip(buffer.length());
445         return true;
446     }
447 
448     public boolean sequence_set(ComposedSequence sequence) throws IOException, ParserException {
449         SimpleSequence seq = new SimpleSequence();
450         if (!seq_range(seq)) {
451             return false;
452         }
453         sequence.add(seq);
454         while (is_char(',')) {
455             seq = new SimpleSequence();
456             if (!seq_range(seq)) {
457                 throw new ParserException("<seq_range>");
458             }
459             sequence.add(seq);
460         }
461         return true;
462     }
463 
464     public boolean seq_range(SimpleSequence sequence) throws IOException, ParserException {
465         Number num = new Number();
466         if (is_char('*')) {
467         } else {
468             if (!number(num)) {
469                 return false;
470             }
471             sequence.setMin(num.number);
472         }
473         if (is_char(':')) {
474             if (is_char('*')) {
475             } else {
476                 if (!number(num)) {
477                     throw new ParserException("<number>");
478                 }
479                 sequence.setMax(num.number);
480             }
481         } else {
482             sequence.setMax(num.number);
483         }
484         return true;
485     }
486 
487     public boolean section(PointerSection section) throws IOException, ParserException {
488         in.mark(1);
489 
490         char c = (char)in.read();
491 
492         if (c != '[') {
493             in.reset();
494             return false;
495         }
496         if (section_msgtext(section)) {
497 
498         } else {
499             int i = section_part(section);
500             if (i == 0) {
501                 //in.reset();
502                 //throw new ParserException("Wrong body section");
503             } else if (i == 2) {
504                 while (section.child != null) {
505                     section = (PointerSection)section.child;
506                 }
507                 if (!section_text(section)) {
508                     throw new ParserException("<section_text>");
509                 }
510             } else {
511                 while (section.child != null) {
512                     section = (PointerSection)section.child;
513                 }
514                 //section.child = new TextSection();
515                 // add TEXT implicitly. This might not be right but Java IMAP acts as it is!
516 
517                 // section.child = new Section();
518 
519             }
520         }
521         c = (char)in.read();
522         if (c != ']') {
523             throw new ParserException("']'");
524         }
525 
526         return true;
527     }
528 
529     public int section_part(PointerSection section) throws IOException, ParserException {
530         Number number = new Number();
531         if (!nz_number(number)) {
532             return 0;
533         }
534         MultipartSection sec = new MultipartSection();
535         section.child = sec;
536         sec.partNo = number.number;
537         section = sec;
538         in.mark(1);
539         char c = (char)in.read();
540         while (c == '.') {
541             if (!nz_number(number)) {
542                 return 2;
543                 //throw new ParserException("Wrong section part - missing non zero number");
544             }
545             sec = new MultipartSection();
546             section.child = sec;
547             sec.partNo = number.number;
548             section = sec;
549             in.mark(1);
550             c = (char)in.read();
551         } // while
552         in.reset();
553         return 1;
554     }
555 
556     public boolean section_text(PointerSection section) throws IOException, ParserException {
557         if (section_msgtext(section)) {
558             return true;
559         } else if (keyword("MIME")) {
560             section.child = new MimeSection();
561             return true;
562         }
563         return false;
564     }
565 
566     public boolean section_msgtext(PointerSection section) throws IOException, ParserException {
567         if (keyword("HEADER")) {
568             HeaderSection sec = new HeaderSection();
569             section.child = sec;
570             if (keyword(".FIELDS")) {
571                 sec.all = false;
572                 if (keyword(".NOT")) {
573                     sec.not = true;
574                 }
575                 if (!is_char(' ')) {
576                     throw new ParserException("<SP>");
577                 }
578                 sec.fields = new ArrayList<String>();
579                 if (!header_list(sec.fields)) {
580                     throw new ParserException("<header_list>");
581                 }
582             }
583             return true;
584         } else if (keyword("TEXT")) {
585             section.child = new TextSection();
586             return true;
587         } else {
588             return false;
589         }
590         //"HEADER" / "HEADER.FIELDS" [".NOT"] SP header-list /
591         //                 "TEXT"        return false;
592     }
593 
594     public boolean header_list(List<String> list) throws IOException, ParserException {
595         in.mark(1);
596         char c = (char)in.read();
597         if (c != '(') {
598             in.reset();
599             return false;
600         }
601         StringBuffer s = new StringBuffer();
602         if (!astring(s)) {
603             throw new ParserException("<string>");
604         }
605         list.add(s.toString());
606         s.delete(0, s.length());
607         while (is_char(' ')) {
608             if (!astring(s)) {
609                 throw new ParserException("<string>");
610             }
611             list.add(s.toString());
612             s.delete(0, s.length());
613         } // while
614         if (!is_char(')')) {
615             throw new ParserException("')'");
616         }
617         return true;
618     }
619 
620     public boolean mailbox(StringBuffer mailbox) throws IOException, ParserException {
621         if (keyword("INBOX")) {
622             mailbox.append("INBOX");
623             return true;
624         } else {
625             return astring(mailbox);
626         }
627     }
628 
629     public boolean flag(Flags flags) throws IOException, ParserException {
630          if (keyword("\\Answered")) {
631              flags.add(Flags.Flag.ANSWERED);
632              return true;
633          } else if (keyword("\\Flagged")) {
634              flags.add(Flags.Flag.FLAGGED);
635              return true;
636          } else if (keyword("\\Deleted")) {
637              flags.add(Flags.Flag.DELETED);
638              return true;
639          } else if (keyword("\\Seen")) {
640              flags.add(Flags.Flag.SEEN);
641              return true;
642          } else if (keyword("\\Draft")) {
643              flags.add(Flags.Flag.DRAFT);
644              return true;
645          } else if (is_char('\\')) {
646              StringBuffer b = new StringBuffer();
647              if (!atom(b)) {
648                  throw new ParserException("<atom>");
649              } else {
650                  flags.add(b.toString());
651              }
652              return true;
653          } else {
654              StringBuffer b = new StringBuffer();
655              if (atom(b)) {
656                  flags.add(b.toString());
657                  return true;
658              }
659          }
660         return false;
661     }
662 
663     public boolean flag_list(Flags fs) throws IOException, ParserException {
664         //flag-list       = "(" [flag *(SP flag)] ")"
665         if (!is_char('(')) {
666             return false;
667         }
668         if (flag(fs)) {
669            while (is_char(' ')) {
670                if (!flag(fs)) {
671                    throw new ParserException("<flag>");
672                }
673            }
674 
675         }
676 
677         if (!is_char(')')) {
678             throw new ParserException("')'");
679         }
680         return true;
681     }
682 
683     public boolean date_day_fixed(Number num) throws IOException, ParserException {
684         if (is_char(' ')) {
685             in.mark(1);
686             char c = (char)in.read();
687             if (digit(c)) {
688                 num.number = c-'0';
689             } else {
690                 throw new ParserException("<digit>");
691             }
692         } else {
693             in.mark(1);
694             char c = (char)in.read();
695             if (digit(c)) {
696                 num.number = c-'0';
697                 c = (char)in.read();
698                 if (digit(c)) {
699                     num.number = num.number*10+c-'0';
700                 } else {
701                     throw new ParserException("<digit>");
702                 }
703             } else {
704                 return false;
705             }
706         }
707         return true;
708     }
709 
710     public boolean date_day(Number num) throws IOException, ParserException {
711         in.mark(1);
712         char c = (char)in.read();
713         if (digit(c)) {
714             num.number = c-'0';
715             c = (char)in.read();
716             if (digit(c)) {
717                 num.number = num.number*10+c-'0';
718             }
719             return true;
720         } else {
721             return false;
722         }
723     }
724 
725 
726     public boolean date_month(Number num) throws IOException, ParserException {
727         //date-month      = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
728         //                  "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
729         if (keyword("Jan")) {
730             num.number = 1;
731         } else if (keyword("Feb")) {
732             num.number = 2;
733         } else if (keyword("Mar")) {
734             num.number = 3;
735         } else if (keyword("Apr")) {
736             num.number = 4;
737         } else if (keyword("May")) {
738             num.number = 5;
739         } else if (keyword("Jun")) {
740             num.number = 6;
741         } else if (keyword("Jul")) {
742             num.number = 7;
743         } else if (keyword("Aug")) {
744             num.number = 8;
745         } else if (keyword("Sep")) {
746             num.number = 9;
747         } else if (keyword("Oct")) {
748             num.number = 10;
749         } else if (keyword("Nov")) {
750             num.number = 11;
751         } else if (keyword("Dec")) {
752             num.number = 12;
753         } else {
754             return false;
755         }
756         return true;
757     }
758 
759     public boolean four_digit(Number num) throws IOException, ParserException {
760         in.mark(1);
761         char c = (char)in.read();
762         if (digit(c)) {
763             num.number = c-'0';
764 
765             for (int i=0; i<3; i++) {
766                 c = (char)in.read();
767                 if (!digit(c)) {
768                     throw new ParserException("<digit>");
769                 }
770                 num.number = num.number*10 + c-'0';
771             } // for
772 
773             return true;
774         } else {
775             in.reset();
776             return false;
777         }
778 
779     }
780 
781     public boolean two_digit(Number num) throws IOException, ParserException {
782         in.mark(1);
783         char c = (char)in.read();
784         if (digit(c)) {
785             num.number = c-'0';
786             c = (char)in.read();
787             if (!digit(c)) {
788                 throw new ParserException("<digit>");
789             }
790             num.number = num.number*10 + c-'0';
791             return true;
792         } else {
793             in.reset();
794             return false;
795         }
796     }
797 
798     public boolean time(Number hour, Number min, Number sec) throws IOException, ParserException {
799 //      time            = 2DIGIT ":" 2DIGIT ":" 2DIGIT
800         if (!two_digit(hour)) {
801             return false;
802         }
803         if (!is_char(':')) {
804             throw new ParserException("':'");
805         }
806         if (!two_digit(min)) {
807             throw new ParserException("<2DIGIT>");
808         }
809         if (!is_char(':')) {
810             throw new ParserException("':'");
811         }
812         if (!two_digit(sec)) {
813             throw new ParserException("<2DIGIT>");
814         }
815         return true;
816     }
817 
818     public boolean zone(StringBuffer zone) throws IOException, ParserException {
819         in.mark(1);
820         char c = (char)in.read();
821         if (c == '+') {
822             zone.append("GMT");
823             zone.append('+');
824         } else if (c == '-') {
825             zone.append("GMT");
826             zone.append('-');
827         } else {
828             in.reset();
829             return false;
830         }
831         c = (char)in.read();
832         if (digit(c)) {
833             zone.append(c);
834 
835             for (int i=0; i<3; i++) {
836                 c = (char)in.read();
837                 if (!digit(c)) {
838                     throw new ParserException("<digit>");
839                 }
840                 if (i == 1) {
841                     zone.append(':');
842                 }
843                 zone.append(c);
844             } // for
845 
846             return true;
847         } else {
848             throw new ParserException("<digit>");
849         }
850     }
851 
852     public boolean date_time(GregorianCalendar calendar) throws IOException, ParserException {
853         //DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
854         if (!is_char('"')) {
855             return false;
856         }
857         Number day = new Number();
858         if (!date_day_fixed(day)) {
859             throw new ParserException("<date_day_fixed>");
860         }
861         if (!is_char('-')) {
862             throw new ParserException("'-'");
863         }
864         Number month = new Number();
865         if (!date_month(month)) {
866             throw new ParserException("<date_month>");
867         }
868         if (!is_char('-')) {
869             throw new ParserException("'-'");
870         }
871         Number year = new Number();
872         if (!four_digit(year)) {
873             throw new ParserException("<date_year>");
874         }
875         if (!is_char(' ')) {
876             throw new ParserException("<SP>");
877         }
878         Number hour = new Number();
879         Number min = new Number();
880         Number sec = new Number();
881         if (!time(hour, min, sec)) {
882             throw new ParserException("<time>");
883         }
884         if (!is_char(' ')) {
885             throw new ParserException("<SP>");
886         }
887         StringBuffer zone = new StringBuffer();
888         if (!zone(zone)) {
889             throw new ParserException("<time>");
890         }
891         TimeZone timeZone = TimeZone.getTimeZone(zone.toString());
892 
893         //GregorianCalendar calendar =
894         //    new GregorianCalendar(year.number, month.number, day.number,
895         //                          hour.number, min.number, sec.number);
896         calendar.set(year.number, month.number, day.number,
897                                   hour.number, min.number);
898         calendar.set(Calendar.SECOND, sec.number);
899         calendar.setTimeZone(timeZone);
900         if (!is_char('"')) {
901             throw new ParserException("'\"'");
902         }
903 
904         return true;
905     }
906 
907     public boolean date_text(GregorianCalendar calendar) throws IOException, ParserException {
908         Number day = new Number();
909         if (!date_day(day)) {
910             throw new ParserException("<date_day_fixed>");
911         }
912         if (!is_char('-')) {
913             throw new ParserException("'-'");
914         }
915         Number month = new Number();
916         if (!date_month(month)) {
917             throw new ParserException("<date_month>");
918         }
919         if (!is_char('-')) {
920             throw new ParserException("'-'");
921         }
922         Number year = new Number();
923         if (!four_digit(year)) {
924             throw new ParserException("<date_year>");
925         }
926         calendar.set(year.number, month.number, day.number);
927         return true;
928     }
929 
930     public boolean date(GregorianCalendar calendar) throws IOException, ParserException {
931         if (is_char('"')) {
932             if (!date_text(calendar)) {
933                 throw new ParserException("<date_text>");
934             }
935             if (!is_char('"')) {
936                 throw new ParserException("'\"'");
937             }
938             return true;
939         } else {
940             return date_text(calendar);
941         }
942     }
943 
944 
945     public static class Number {
946         public int number = 0;
947     }
948 
949 }