Source for gnu.inet.imap.IMAPResponseTokenizer

   1: /*
   2:  * IMAPResponseTokenizer.java
   3:  * Copyright (C) 2003 The Free Software Foundation
   4:  * 
   5:  * This file is part of GNU inetlib, a library.
   6:  * 
   7:  * GNU inetlib is free software; you can redistribute it and/or modify
   8:  * it under the terms of the GNU General Public License as published by
   9:  * the Free Software Foundation; either version 2 of the License, or
  10:  * (at your option) any later version.
  11:  * 
  12:  * GNU inetlib is distributed in the hope that it will be useful,
  13:  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14:  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15:  * GNU General Public License for more details.
  16:  * 
  17:  * You should have received a copy of the GNU General Public License
  18:  * along with this library; if not, write to the Free Software
  19:  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  20:  * 
  21:  * Linking this library statically or dynamically with other modules is
  22:  * making a combined work based on this library.  Thus, the terms and
  23:  * conditions of the GNU General Public License cover the whole
  24:  * combination.
  25:  *
  26:  * As a special exception, the copyright holders of this library give you
  27:  * permission to link this library with independent modules to produce an
  28:  * executable, regardless of the license terms of these independent
  29:  * modules, and to copy and distribute the resulting executable under
  30:  * terms of your choice, provided that you also meet, for each linked
  31:  * independent module, the terms and conditions of the license of that
  32:  * module.  An independent module is a module which is not derived from
  33:  * or based on this library.  If you modify this library, you may extend
  34:  * this exception to your version of the library, but you are not
  35:  * obliged to do so.  If you do not wish to do so, delete this
  36:  * exception statement from your version.
  37:  */
  38: 
  39: package gnu.inet.imap;
  40: 
  41: import java.io.ByteArrayOutputStream;
  42: import java.io.InputStream;
  43: import java.io.IOException;
  44: import java.net.ProtocolException;
  45: import java.util.ArrayList;
  46: import java.util.List;
  47: import java.util.Stack;
  48: 
  49: /**
  50:  * An object that can parse an underlying socket stream containing IMAP
  51:  * protocol server responses into IMAPResponse tokens.
  52:  *
  53:  * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
  54:  */
  55: public class IMAPResponseTokenizer
  56:   implements IMAPConstants
  57: {
  58: 
  59:   /**
  60:    * The server stream.
  61:    */
  62:   protected InputStream in;
  63: 
  64:   private byte[] buffer = null;
  65: 
  66:   private static final int BUFFER_SIZE = 4096; // Seems to be a good value
  67:   private static final String DEFAULT_ENCODING = "US-ASCII";
  68: 
  69:   private static final int STATE_TAG = 0;
  70:   private static final int STATE_COUNT = 1;
  71:   private static final int STATE_ID = 2;
  72:   private static final int STATE_MAYBE_CODE = 3;
  73:   private static final int STATE_CODE = 4;
  74:   private static final int STATE_LITERAL_LENGTH = 5;
  75:   private static final int STATE_LITERAL = 6;
  76:   private static final int STATE_TEXT = 7;
  77:   private static final int STATE_STATUS = 8;
  78: 
  79:   /**
  80:    * Constructor.
  81:    * @param in the server socket input stream
  82:    */
  83:   public IMAPResponseTokenizer(InputStream in)
  84:   {
  85:     this.in = in;
  86:   }
  87: 
  88:   /*
  89:    * Reads bytes, from the underlying stream if necessary, or from the
  90:    * cache. If moreNeeded is specified, always performs a read to append
  91:    * more to the cache.
  92:    */
  93:   byte[] read(boolean moreNeeded)
  94:     throws IOException
  95:   {
  96:     if (buffer != null && !moreNeeded && buffer.length > 0)
  97:       {
  98:         return buffer;
  99:       }
 100:     int max = in.available();
 101:     if (max < 1)
 102:       {
 103:         max = BUFFER_SIZE;
 104:       }
 105:     byte[] tmp = new byte[max];
 106:     int len = 0;
 107:     while (len == 0)
 108:       {
 109:         len = in.read(tmp, 0, max);
 110:       }
 111:     if (len == -1)
 112:       {
 113:         return null;              // EOF
 114:       }
 115:     int blen =(buffer == null) ? 0 : buffer.length;
 116:     byte[] uni = new byte[blen + len];
 117:     if (blen != 0)
 118:       {
 119:         System.arraycopy(buffer, 0, uni, 0, blen);
 120:       }
 121:     System.arraycopy(tmp, 0, uni, blen, len);
 122:     buffer = uni;
 123:     return buffer;
 124:   }
 125: 
 126:   /*
 127:    * Invalidates the byte cache up to the specified index.
 128:    */
 129:   void mark(int index)
 130:   {
 131:     int len = buffer.length;
 132:     int start = index + 1;
 133:     if (start < len)
 134:       {
 135:         int n =(len - start);
 136:         byte[] tmp = new byte[n];
 137:         System.arraycopy(buffer, start, tmp, 0, n);
 138:         buffer = tmp;
 139:       }
 140:     else
 141:       {
 142:         buffer = null;
 143:       }
 144:   }
 145: 
 146:   /**
 147:    * Returns the next IMAPResponse.
 148:    */
 149:   public IMAPResponse next()
 150:     throws IOException
 151:   {
 152:     // Perform read
 153:     byte[] buf = read(false);
 154:     if (buf == null)
 155:       {
 156:         return null;              // pass EOF back up the chain
 157:       }
 158:     int len = buf.length;
 159:     
 160:     IMAPResponse response = new IMAPResponse();
 161:     ByteArrayOutputStream genericSink = new ByteArrayOutputStream();
 162:     ByteArrayOutputStream literalSink = null;
 163:     int literalCount = 0, literalLength = -1;
 164:     Stack context = new Stack();
 165:     int state = STATE_TAG;
 166:     boolean inQuote = false;
 167:     boolean inContent = false;
 168:     for (int i = 0; i < len; i++)
 169:       {
 170:         byte b = buf[i];
 171:         switch (state)
 172:           {
 173:           case STATE_TAG:          // expect tag
 174:             if (i == 0 && b == 0x2a)        // untagged
 175:               {
 176:                 response.tag = IMAPResponse.UNTAGGED;
 177:               }
 178:             else if (i == 0 && b == 0x2b)   // continuation
 179:               {
 180:                 response.tag = IMAPResponse.CONTINUATION;
 181:               }
 182:             else if (b == 0x20)     // delimiter
 183:               {
 184:                 if (response.tag == null)
 185:                   {
 186:                     byte[] tb = genericSink.toByteArray();
 187:                     response.tag = new String(tb, DEFAULT_ENCODING);
 188:                   }
 189:                 genericSink.reset();
 190:                 if (response.isContinuation())
 191:                   {
 192:                     state = STATE_TEXT;
 193:                   }
 194:                 else
 195:                   {
 196:                     state = STATE_COUNT;
 197:                   }
 198:               }
 199:             else                    // tag literal
 200:               {
 201:                 genericSink.write(b);
 202:               }
 203:             break;
 204:           case STATE_COUNT:        // expect count or id
 205:             if (b < 0x30 || b > 0x39)
 206:               {
 207:                 state = STATE_ID;
 208:               }
 209:             if (b == 0x20)          // delimiter
 210:               {
 211:                 byte[] cb = genericSink.toByteArray();
 212:                 genericSink.reset();
 213:                 String cs = new String(cb, DEFAULT_ENCODING);
 214:                 try
 215:                   {
 216:                     response.count = Integer.parseInt(cs);
 217:                   }
 218:                 catch (NumberFormatException e)
 219:                   {
 220:                     throw new ProtocolException("Expecting number: " + cs);
 221:                   }
 222:                 state = STATE_ID;
 223:               }
 224:             else
 225:               {
 226:                 genericSink.write(b);
 227:               }
 228:             break;
 229:           case STATE_ID:           // expect id
 230:             if (b == 0x20)          // delimiter
 231:               {
 232:                 byte[] ib = genericSink.toByteArray();
 233:                 genericSink.reset();
 234:                 response.id = new String(ib, DEFAULT_ENCODING).intern();
 235:                 state = STATE_MAYBE_CODE;
 236:               }
 237:             else if (b == 0x0a)     // EOL
 238:               {
 239:                 byte[] ib = genericSink.toByteArray();
 240:                 genericSink.reset();
 241:                 response.id = new String(ib, DEFAULT_ENCODING).intern();
 242:                 state = STATE_TAG;
 243:                 // mark bytes read
 244:                 mark(i);
 245:                 return response;
 246:               }
 247:             else if (b != 0x0d)     // id literal
 248:               {
 249:                 genericSink.write(b);
 250:               }
 251:             break;
 252:           case STATE_MAYBE_CODE:   // expect code or text
 253:             if (b == 0x28 || b == 0x5b)
 254:               {
 255:                 List top = new ArrayList();
 256:                 response.code = top;
 257:                 context.push(top);
 258:                 state = STATE_CODE;
 259:               }
 260:             else
 261:               {
 262:                 if (response.id == FETCH)
 263:                   {
 264:                     // We can't have text here so it must be the beginning of
 265:                     // FETCH FLAGS. Go back to ID state.
 266:                     genericSink.reset();
 267:                     byte[] fetchBytes =
 268:                       new byte[] { 0x46, 0x45, 0x54, 0x43, 0x48, 0x20};
 269:                     genericSink.write(fetchBytes);
 270:                     genericSink.write(b);
 271:                     state = STATE_ID;
 272:                   }
 273:                 else if (response.id == STATUS)
 274:                   {
 275:                     // We are in the mailbox name part of the STATUS response
 276:                     genericSink.write(b);
 277:                     state = STATE_STATUS;
 278:                   }
 279:                 else
 280:                   {
 281:                     genericSink.write(b);
 282:                     state = STATE_TEXT;
 283:                   }
 284:               }
 285:             break;
 286:           case STATE_STATUS:
 287:             if (b == 0x20)          // delimiter
 288:               {
 289:                 response.mailbox = genericSink.toString();
 290:                 genericSink.reset();
 291:                 state = STATE_MAYBE_CODE;
 292:               }
 293:             else
 294:               {
 295:                 genericSink.write(b);
 296:               }
 297:             break;
 298:           case STATE_CODE:         // response code inside parentheses
 299:             if (b == 0x22)          // quote delimiter
 300:               {
 301:                 inQuote = !inQuote;
 302:               }
 303:             else if (inQuote)
 304:               {
 305:                 genericSink.write(b);
 306:               }
 307:             else
 308:               {
 309:                 if (b == 0x28 || b == 0x5b)   // start parenthesis/bracket
 310:                   {
 311:                     List parent =(List) context.peek();
 312:                     List top = new ArrayList();
 313:                     if (genericSink.size() > 0)
 314:                       {
 315:                         byte[] tb = genericSink.toByteArray();
 316:                         String token =
 317:                           new String(tb, DEFAULT_ENCODING).intern();
 318:                         Pair pair = new Pair(token, top);
 319:                         parent.add(pair);
 320:                         genericSink.reset();
 321:                       }
 322:                     else
 323:                       {
 324:                         parent.add(top);
 325:                       }
 326:                     context.push(top);
 327:                   }
 328:                 else if (b == 0x29 || b == 0x5d)      // end parenthesis/bracket
 329:                   {
 330:                     List top =(List) context.pop();
 331:                     // flush genericSink
 332:                     if (genericSink.size() > 0)
 333:                       {
 334:                         byte[] tb = genericSink.toByteArray();
 335:                         String token =
 336:                           new String(tb, DEFAULT_ENCODING).intern();
 337:                         top.add(token);
 338:                         genericSink.reset();
 339:                       }
 340:                   }
 341:                 else if (b == 0x7b)   // literal length
 342:                   {
 343:                     genericSink.reset();
 344:                     state = STATE_LITERAL_LENGTH;
 345:                   }
 346:                 else if (b == 0x20)   // token delimiter
 347:                   {
 348:                     if (context.size() == 0)    // end state
 349:                       {
 350:                         state = STATE_TEXT;
 351:                       }
 352:                     else                // add token
 353:                       {
 354:                         List top =(List) context.peek();
 355:                         // flush genericSink
 356:                         if (genericSink.size() > 0)
 357:                           {
 358:                             byte[] tb = genericSink.toByteArray();
 359:                             String token =
 360:                               new String(tb, DEFAULT_ENCODING).intern();
 361:                             top.add(token);
 362:                             genericSink.reset();
 363:                           }
 364:                       }
 365:                   }
 366:                 else if (b == 0x0a)   // EOL
 367:                   {
 368:                     state = STATE_TAG;
 369:                     // mark bytes read
 370:                     mark(i);
 371:                     return response;
 372:                   }
 373:                 else if (b != 0x0d)   // ignore CR
 374:                   {
 375:                     genericSink.write(b);
 376:                   }
 377:               }
 378:             break;
 379:           case STATE_LITERAL_LENGTH:
 380:             if (b == 0x7d)          // end literal length
 381:               {
 382:                 byte[] cb = genericSink.toByteArray();
 383:                 genericSink.reset();
 384:                 String cs = new String(cb, DEFAULT_ENCODING);
 385:                 try
 386:                   {
 387:                     literalLength = Integer.parseInt(cs);
 388:                   }
 389:                 catch (NumberFormatException e)
 390:                   {
 391:                     throw new ProtocolException("Expecting number: " + cs);
 392:                   }
 393:               }
 394:             else if (b == 0x0a)     // start literal
 395:               {
 396:                 state = STATE_LITERAL;
 397:                 literalSink = new ByteArrayOutputStream();
 398:                 literalCount = 0;
 399:               }
 400:             else if (b != 0x0d)     // ignore CR
 401:               {
 402:                 genericSink.write(b);
 403:               }
 404:             break;
 405:           case STATE_LITERAL:
 406:             if (literalCount >= literalLength)
 407:               {
 408:                 List top =(List) context.peek();
 409:                 byte[] literal = literalSink.toByteArray();
 410:                 top.add(literal);
 411:                 literalSink = null;
 412:                 inContent = false;
 413:                 state = STATE_CODE;
 414:               }
 415:             else
 416:               {
 417:                 literalSink.write(b);
 418:                 literalCount++;
 419:               }
 420:             break;
 421:           case STATE_TEXT:         // human-readable text
 422:             if (b == 0x0a)          // delimiter
 423:               {
 424:                 byte[] tb = genericSink.toByteArray();
 425:                 genericSink.reset();
 426:                 response.text = new String(tb, DEFAULT_ENCODING);
 427:                 state = STATE_TAG;
 428:                 // mark bytes read
 429:                 mark(i);
 430:                 return response;
 431:               }
 432:             else if (b != 0x0d)     // ignore CR
 433:               {
 434:                 genericSink.write(b);
 435:               }
 436:             break;
 437:           }
 438:       }
 439:     // get more bytes
 440:     buf = read(true);
 441:     return next();
 442:   }
 443:   
 444: }