KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > apache > james > imapserver > commands > CommandParser


1 /***********************************************************************
2  * Copyright (c) 2000-2004 The Apache Software Foundation. *
3  * All rights reserved. *
4  * ------------------------------------------------------------------- *
5  * Licensed under the Apache License, Version 2.0 (the "License"); you *
6  * may not use this file except in compliance with the License. You *
7  * may obtain a copy of the License at: *
8  * *
9  * http://www.apache.org/licenses/LICENSE-2.0 *
10  * *
11  * Unless required by applicable law or agreed to in writing, software *
12  * distributed under the License is distributed on an "AS IS" BASIS, *
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or *
14  * implied. See the License for the specific language governing *
15  * permissions and limitations under the License. *
16  ***********************************************************************/

17
18 package org.apache.james.imapserver.commands;
19
20 import org.apache.james.imapserver.ProtocolException;
21 import org.apache.james.imapserver.ImapRequestLineReader;
22 import org.apache.james.imapserver.ImapConstants;
23 import org.apache.james.imapserver.store.MessageFlags;
24 import org.apache.james.util.Assert;
25
26 import java.util.Date JavaDoc;
27 import java.util.TimeZone JavaDoc;
28 import java.util.List JavaDoc;
29 import java.util.ArrayList JavaDoc;
30 import java.text.DateFormat JavaDoc;
31 import java.text.SimpleDateFormat JavaDoc;
32 import java.text.ParseException JavaDoc;
33
34 /**
35  *
36  *
37  * @version $Revision: 1.5.2.3 $
38  */

39 public class CommandParser
40 {
41     private static final char[] EMPTY_CHAR_ARRAY = new char[0];
42
43     /**
44      * Reads an argument of type "atom" from the request.
45      */

46     public String JavaDoc atom( ImapRequestLineReader request ) throws ProtocolException
47     {
48         return consumeWord( request, new ATOM_CHARValidator() );
49     }
50
51     /**
52      * Reads a command "tag" from the request.
53      */

54     public String JavaDoc tag(ImapRequestLineReader request) throws ProtocolException
55     {
56         CharacterValidator validator = new TagCharValidator();
57         return consumeWord( request, validator );
58     }
59
60     /**
61      * Reads an argument of type "astring" from the request.
62      */

63     public String JavaDoc astring(ImapRequestLineReader request) throws ProtocolException
64     {
65         char next = request.nextWordChar();
66         switch ( next ) {
67             case '"':
68                 return consumeQuoted( request );
69             case '{':
70                 return consumeLiteral( request );
71             default:
72                 return atom( request );
73         }
74     }
75
76     /**
77      * Reads an argument of type "nstring" from the request.
78      */

79     public String JavaDoc nstring( ImapRequestLineReader request ) throws ProtocolException
80     {
81         char next = request.nextWordChar();
82         switch ( next ) {
83             case '"':
84                 return consumeQuoted( request );
85             case '{':
86                 return consumeLiteral( request );
87             default:
88                 String JavaDoc value = atom( request );
89                 if ( "NIL".equals( value ) ) {
90                     return null;
91                 }
92                 else {
93                     throw new ProtocolException( "Invalid nstring value: valid values are '\"...\"', '{12} CRLF *CHAR8', and 'NIL'." );
94                 }
95         }
96     }
97
98     /**
99      * Reads a "mailbox" argument from the request. Not implemented *exactly* as per spec,
100      * since a quoted or literal "inbox" still yeilds "INBOX"
101      * (ie still case-insensitive if quoted or literal). I think this makes sense.
102      *
103      * mailbox ::= "INBOX" / astring
104      * ;; INBOX is case-insensitive. All case variants of
105      * ;; INBOX (e.g. "iNbOx") MUST be interpreted as INBOX
106      * ;; not as an astring.
107      */

108     public String JavaDoc mailbox( ImapRequestLineReader request ) throws ProtocolException
109     {
110         String JavaDoc mailbox = astring( request );
111         if ( mailbox.equalsIgnoreCase( ImapConstants.INBOX_NAME ) ) {
112             return ImapConstants.INBOX_NAME;
113         }
114         else {
115             return mailbox;
116         }
117     }
118
119     /**
120      * Reads a "date-time" argument from the request.
121      * TODO handle timezones properly
122      */

123     public Date JavaDoc dateTime( ImapRequestLineReader request ) throws ProtocolException
124     {
125         char next = request.nextWordChar();
126         String JavaDoc dateString;
127         if ( next == '"' ) {
128             dateString = consumeQuoted( request );
129         }
130         else {
131             throw new ProtocolException( "DateTime values must be quoted." );
132         }
133
134         DateFormat JavaDoc dateFormat = new SimpleDateFormat JavaDoc( "dd-MMM-yyyy hh:mm:ss zzzz" );
135         try {
136             return dateFormat.parse( dateString );
137         }
138         catch ( ParseException JavaDoc e ) {
139             throw new ProtocolException( "Invalid date format." );
140         }
141     }
142
143     /**
144      * Reads a "date" argument from the request.
145      * TODO handle timezones properly
146      */

147     public Date JavaDoc date( ImapRequestLineReader request ) throws ProtocolException
148     {
149         char next = request.nextWordChar();
150         String JavaDoc dateString;
151         if ( next == '"' ) {
152             dateString = consumeQuoted( request );
153         }
154         else {
155             dateString = atom( request );
156         }
157
158         DateFormat JavaDoc dateFormat = new SimpleDateFormat JavaDoc( "dd-MMM-yyyy" );
159         try {
160             return dateFormat.parse( dateString );
161         }
162         catch ( ParseException JavaDoc e ) {
163             throw new ProtocolException( "Invalid date format." );
164         }
165     }
166
167     /**
168      * Reads the next "word from the request, comprising all characters up to the next SPACE.
169      * Characters are tested by the supplied CharacterValidator, and an exception is thrown
170      * if invalid characters are encountered.
171      */

172     protected String JavaDoc consumeWord( ImapRequestLineReader request,
173                                   CharacterValidator validator )
174             throws ProtocolException
175     {
176         StringBuffer JavaDoc atom = new StringBuffer JavaDoc();
177
178         char next = request.nextWordChar();
179         while( ! isWhitespace( next ) ) {
180             if ( validator.isValid( next ) )
181             {
182                 atom.append( next );
183                 request.consume();
184             }
185             else {
186                 throw new ProtocolException( "Invalid character: '" + next + "'" );
187             }
188             next = request.nextChar();
189         }
190         return atom.toString();
191     }
192
193     private boolean isWhitespace( char next )
194     {
195         return ( next == ' ' || next == '\n' || next == '\r' || next == '\t' );
196     }
197
198     /**
199      * Reads an argument of type "literal" from the request, in the format:
200      * "{" charCount "}" CRLF *CHAR8
201      * Note before calling, the request should be positioned so that nextChar
202      * is '{'. Leading whitespace is not skipped in this method.
203      */

204     protected String JavaDoc consumeLiteral( ImapRequestLineReader request )
205             throws ProtocolException
206     {
207         // The 1st character must be '{'
208
consumeChar( request, '{' );
209
210         StringBuffer JavaDoc digits = new StringBuffer JavaDoc();
211         char next = request.nextChar();
212         while ( next != '}' && next != '+' )
213         {
214             digits.append( next );
215             request.consume();
216             next = request.nextChar();
217         }
218
219         // If the number is *not* suffixed with a '+', we *are* using a synchronized literal,
220
// and we need to send command continuation request before reading data.
221
boolean synchronizedLiteral = true;
222         // '+' indicates a non-synchronized literal (no command continuation request)
223
if ( next == '+' ) {
224             synchronizedLiteral = false;
225             consumeChar(request, '+' );
226         }
227
228         // Consume the '}' and the newline
229
consumeChar( request, '}' );
230         consumeCRLF( request );
231
232         if ( synchronizedLiteral ) {
233             request.commandContinuationRequest();
234         }
235
236         int size = Integer.parseInt( digits.toString() );
237         byte[] buffer = new byte[size];
238         request.read( buffer );
239
240         return new String JavaDoc( buffer );
241     }
242
243     /**
244      * Consumes a CRLF from the request.
245      * TODO we're being liberal, the spec insists on \r\n for new lines.
246      * @param request
247      * @throws ProtocolException
248      */

249     private void consumeCRLF( ImapRequestLineReader request )
250             throws ProtocolException
251     {
252         char next = request.nextChar();
253         if ( next != '\n' ) {
254             consumeChar( request, '\r' );
255         }
256         consumeChar( request, '\n' );
257     }
258
259     /**
260      * Consumes the next character in the request, checking that it matches the
261      * expected one. This method should be used when the
262      */

263     protected void consumeChar( ImapRequestLineReader request, char expected )
264             throws ProtocolException
265     {
266         char consumed = request.consume();
267         if ( consumed != expected ) {
268             throw new ProtocolException( "Expected:'" + expected + "' found:'" + consumed + "'" );
269         }
270     }
271
272     /**
273      * Reads a quoted string value from the request.
274      */

275     protected String JavaDoc consumeQuoted( ImapRequestLineReader request )
276             throws ProtocolException
277     {
278         // The 1st character must be '"'
279
consumeChar(request, '"' );
280
281         StringBuffer JavaDoc quoted = new StringBuffer JavaDoc();
282         char next = request.nextChar();
283         while( next != '"' ) {
284             if ( next == '\\' ) {
285                 request.consume();
286                 next = request.nextChar();
287                 if ( ! isQuotedSpecial( next ) ) {
288                     throw new ProtocolException( "Invalid escaped character in quote: '" +
289                                                  next + "'" );
290                 }
291             }
292             quoted.append( next );
293             request.consume();
294             next = request.nextChar();
295         }
296
297         consumeChar( request, '"' );
298
299         return quoted.toString();
300     }
301
302     /**
303      * Reads a base64 argument from the request.
304      */

305     public byte[] base64( ImapRequestLineReader request ) throws ProtocolException
306     {
307         return null;
308     }
309
310     /**
311      * Reads a "flags" argument from the request.
312      */

313     public MessageFlags flagList( ImapRequestLineReader request ) throws ProtocolException
314     {
315         MessageFlags flags = new MessageFlags();
316         request.nextWordChar();
317         consumeChar( request, '(' );
318         CharacterValidator validator = new NoopCharValidator();
319         String JavaDoc nextWord = consumeWord( request, validator );
320         while ( ! nextWord.endsWith(")" ) ) {
321             setFlag( nextWord, flags );
322             nextWord = consumeWord( request, validator );
323         }
324         // Got the closing ")", may be attached to a word.
325
if ( nextWord.length() > 1 ) {
326             setFlag( nextWord.substring(0, nextWord.length() - 1 ), flags );
327         }
328
329         return flags;
330         }
331
332     public void setFlag( String JavaDoc flagString, MessageFlags flags ) throws ProtocolException
333     {
334         if ( flagString.equalsIgnoreCase( MessageFlags.ANSWERED ) ) {
335             flags.setAnswered( true );
336         }
337         else if ( flagString.equalsIgnoreCase( MessageFlags.DELETED ) ) {
338             flags.setDeleted( true );
339         }
340         else if ( flagString.equalsIgnoreCase( MessageFlags.DRAFT ) ) {
341             flags.setDraft( true );
342         }
343         else if ( flagString.equalsIgnoreCase( MessageFlags.FLAGGED ) ) {
344             flags.setFlagged( true );
345         }
346         else if ( flagString.equalsIgnoreCase( MessageFlags.SEEN ) ) {
347             flags.setSeen( true );
348         }
349         else {
350             throw new ProtocolException( "Invalid flag string." );
351         }
352     }
353
354      /**
355      * Reads an argument of type "number" from the request.
356      */

357     public long number( ImapRequestLineReader request ) throws ProtocolException
358     {
359         String JavaDoc digits = consumeWord( request, new DigitCharValidator() );
360         return Long.parseLong( digits );
361     }
362
363     /**
364      * Reads an argument of type "nznumber" (a non-zero number)
365      * (NOTE this isn't strictly as per the spec, since the spec disallows
366      * numbers such as "0123" as nzNumbers (although it's ok as a "number".
367      * I think the spec is a bit shonky.)
368      */

369     public long nzNumber( ImapRequestLineReader request ) throws ProtocolException
370     {
371         long number = number( request );
372         if ( number == 0 ) {
373             throw new ProtocolException( "Zero value not permitted." );
374         }
375         return number;
376     }
377
378     private boolean isCHAR( char chr )
379     {
380         return ( chr >= 0x01 && chr <= 0x7f );
381     }
382
383     private boolean isCHAR8( char chr )
384     {
385         return ( chr >= 0x01 && chr <= 0xff );
386     }
387
388     protected boolean isListWildcard( char chr )
389     {
390         return ( chr == '*' || chr == '%' );
391     }
392
393     private boolean isQuotedSpecial( char chr )
394     {
395         return ( chr == '"' || chr == '\\' );
396     }
397
398     /**
399      * Consumes the request up to and including the eno-of-line.
400      * @param request The request
401      * @throws ProtocolException If characters are encountered before the endLine.
402      */

403     public void endLine( ImapRequestLineReader request ) throws ProtocolException
404     {
405         request.eol();
406     }
407
408     /**
409      * Reads a "message set" argument, and parses into an IdSet.
410      * Currently only supports a single range of values.
411      */

412     public IdSet set( ImapRequestLineReader request )
413             throws ProtocolException
414     {
415         CharacterValidator validator = new MessageSetCharValidator();
416         String JavaDoc nextWord = consumeWord( request, validator );
417
418         int commaPos = nextWord.indexOf( ',' );
419         if ( commaPos == -1 ) {
420             return singleRangeSet( nextWord );
421         }
422
423         CompoundIdSet compoundSet = new CompoundIdSet();
424         int pos = 0;
425         while ( commaPos != -1 ) {
426             String JavaDoc range = nextWord.substring( pos, commaPos );
427             IdSet set = singleRangeSet( range );
428             compoundSet.addIdSet( set );
429
430             pos = commaPos + 1;
431             commaPos = nextWord.indexOf( ',', pos );
432         }
433         String JavaDoc range = nextWord.substring( pos );
434         compoundSet.addIdSet( singleRangeSet( range ) );
435         return compoundSet;
436     }
437
438     private IdSet singleRangeSet( String JavaDoc range ) throws ProtocolException
439     {
440         int pos = range.indexOf( ':' );
441         try {
442             if ( pos == -1 ) {
443                 long value = parseLong( range );
444                 return new HighLowIdSet( value, value );
445             }
446             else {
447                 long lowVal = parseLong( range.substring(0, pos ) );
448                 long highVal = parseLong( range.substring( pos + 1 ) );
449                 return new HighLowIdSet( lowVal, highVal );
450             }
451         }
452         catch ( NumberFormatException JavaDoc e ) {
453             throw new ProtocolException( "Invalid message set.");
454         }
455     }
456
457     private long parseLong( String JavaDoc value ) {
458         if ( value.length() == 1 && value.charAt(0) == '*' ) {
459             return Long.MAX_VALUE;
460         }
461         return Long.parseLong( value );
462     }
463     /**
464      * Provides the ability to ensure characters are part of a permitted set.
465      */

466     protected interface CharacterValidator
467     {
468         /**
469          * Validates the supplied character.
470          * @param chr The character to validate.
471          * @return <code>true</code> if chr is valid, <code>false</code> if not.
472          */

473         boolean isValid( char chr );
474     }
475
476     protected class NoopCharValidator implements CharacterValidator
477     {
478         public boolean isValid( char chr )
479         {
480             return true;
481         }
482     }
483
484     protected class ATOM_CHARValidator implements CharacterValidator
485     {
486         public boolean isValid( char chr )
487         {
488             return ( isCHAR( chr ) && !isAtomSpecial( chr ) &&
489                      !isListWildcard( chr ) && !isQuotedSpecial( chr ) );
490         }
491
492         private boolean isAtomSpecial( char chr )
493         {
494             return ( chr == '(' ||
495                     chr == ')' ||
496                     chr == '{' ||
497                     chr == ' ' ||
498                     chr == Character.CONTROL );
499         }
500     }
501
502     protected class DigitCharValidator implements CharacterValidator
503     {
504         public boolean isValid( char chr )
505         {
506             return ( ( chr >= '0' && chr <= '9' ) ||
507                      chr == '*' );
508         }
509     }
510
511     private class TagCharValidator extends ATOM_CHARValidator
512     {
513         public boolean isValid( char chr )
514         {
515             if ( chr == '+' ) return false;
516             return super.isValid( chr );
517         }
518     }
519
520     private class MessageSetCharValidator implements CharacterValidator
521     {
522         public boolean isValid( char chr )
523         {
524             return ( isDigit( chr ) ||
525                     chr == ':' ||
526                     chr == '*' ||
527                     chr == ',' );
528         }
529
530         private boolean isDigit( char chr )
531         {
532             return '0' <= chr && chr <= '9';
533         }
534     }
535
536     private class HighLowIdSet implements IdSet
537     {
538         private long lowVal;
539         private long highVal;
540
541         public HighLowIdSet( long lowVal, long highVal )
542         {
543             this.lowVal = lowVal;
544             this.highVal = highVal;
545         }
546
547         public boolean includes( long value ) {
548             return ( lowVal <= value ) && ( value <= highVal );
549         }
550     }
551
552     private class CompoundIdSet implements IdSet
553     {
554         private List JavaDoc idSets = new ArrayList JavaDoc();
555
556         void addIdSet( IdSet set ) {
557             idSets.add( set );
558         }
559
560         public boolean includes( long value )
561         {
562             for ( int i = 0; i < idSets.size(); i++ ) {
563                 IdSet idSet = ( IdSet ) idSets.get( i );
564                 if ( idSet.includes( value ) ) {
565                     return true;
566                 }
567             }
568             return false;
569         }
570     }
571
572
573 }
574
Popular Tags