/*
* $Id: IMAPResponseTokenizer.java,v 1.1 2003/10/19 08:51:37 dog Exp $
* Copyright (C) 2003 The Free Software Foundation
*
* This file is part of GNU inetlib, a library.
*
* GNU inetlib is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GNU inetlib is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* As a special exception, if you link this library with other files to
* produce an executable, this library does not by itself cause the
* resulting executable to be covered by the GNU General Public License.
* This exception does not however invalidate any other reasons why the
* executable file might be covered by the GNU General Public License.
*/
package gnu.inet.imap;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
/**
* An object that can parse an underlying socket stream containing IMAP
* protocol server responses into IMAPResponse tokens.
*
* @author Chris Burdess
* @version $Revision: 1.1 $ $Date: 2003/10/19 08:51:37 $
*/
public class IMAPResponseTokenizer implements IMAPConstants
{
/**
* The server stream.
*/
protected InputStream in;
private byte[] buffer = null;
private static final int BUFFER_SIZE = 4096; // TODO review
private static final String DEFAULT_ENCODING = "US-ASCII";
private static final int STATE_TAG = 0;
private static final int STATE_COUNT = 1;
private static final int STATE_ID = 2;
private static final int STATE_MAYBE_CODE = 3;
private static final int STATE_CODE = 4;
private static final int STATE_LITERAL_LENGTH = 5;
private static final int STATE_LITERAL = 6;
private static final int STATE_TEXT = 7;
private static final int STATE_STATUS = 8;
/**
* Constructor.
* @param in the server socket input stream
*/
public IMAPResponseTokenizer(InputStream in) {
this.in = in;
}
/*
* Reads bytes, from the underlying stream if necessary, or from the
* cache. If moreNeeded is specified, always performs a read to append
* more to the cache.
*/
byte[] read(boolean moreNeeded)
throws IOException
{
if (buffer!=null && !moreNeeded && buffer.length>0)
return buffer;
int max = in.available();
if (max<1)
max = BUFFER_SIZE;
byte[] tmp = new byte[max];
int len = 0;
while (len==0)
len = in.read(tmp, 0, max);
if (len==-1)
return null; // EOF
int blen = (buffer==null) ? 0 : buffer.length;
byte[] uni = new byte[blen+len];
if (blen!=0)
System.arraycopy(buffer, 0, uni, 0, blen);
System.arraycopy(tmp, 0, uni, blen, len);
buffer = uni;
return buffer;
}
/*
* Invalidates the byte cache up to the specified index.
*/
void mark(int index) {
int len = buffer.length;
int start = index + 1;
if (start0x39)
state = STATE_ID;
if (b==0x20) // delimiter
{
byte[] cb = genericSink.toByteArray();
genericSink.reset();
String cs = new String(cb, DEFAULT_ENCODING);
try
{
response.count = Integer.parseInt(cs);
}
catch (NumberFormatException e)
{
throw new ProtocolException("Expecting number: "+cs);
}
state = STATE_ID;
}
else
genericSink.write(b);
break;
case STATE_ID: // expect id
if (b==0x20) // delimiter
{
byte[] ib = genericSink.toByteArray();
genericSink.reset();
response.id = new String(ib, DEFAULT_ENCODING).intern();
state = STATE_MAYBE_CODE;
}
else if (b==0x0a) // EOL
{
byte[] ib = genericSink.toByteArray();
genericSink.reset();
response.id = new String(ib, DEFAULT_ENCODING).intern();
state = STATE_TAG;
// mark bytes read
mark(i);
return response;
}
else if (b!=0x0d) // id literal
genericSink.write(b);
break;
case STATE_MAYBE_CODE: // expect code or text
if (b==0x28 || b==0x5b)
{
List top = new ArrayList();
response.code = top;
context.push(top);
state = STATE_CODE;
}
else
{
if (response.id==FETCH)
{
// We can't have text here so it must be the beginning of
// FETCH FLAGS. Go back to ID state.
genericSink.reset();
byte[] fetchBytes =
new byte[]{0x46, 0x45, 0x54, 0x43, 0x48, 0x20};
genericSink.write(fetchBytes);
genericSink.write(b);
state = STATE_ID;
}
else if (response.id==STATUS)
{
// We are in the mailbox name part of the STATUS response
genericSink.write(b);
state = STATE_STATUS;
}
else
{
genericSink.write(b);
state = STATE_TEXT;
}
}
break;
case STATE_STATUS:
if (b==0x20) // delimiter
{
response.mailbox = genericSink.toString();
genericSink.reset();
state = STATE_MAYBE_CODE;
}
else
genericSink.write(b);
break;
case STATE_CODE: // response code inside parentheses
if (b==0x22) // quote delimiter
{
inQuote = !inQuote;
}
else if (inQuote)
{
genericSink.write(b);
}
else
{
if (b==0x28 || b==0x5b) // start parenthesis/bracket
{
List parent = (List)context.peek();
List top = new ArrayList();
if (genericSink.size()>0)
{
byte[] tb = genericSink.toByteArray();
String token = new String(tb, DEFAULT_ENCODING).intern();
Pair pair = new Pair(token, top);
parent.add(pair);
genericSink.reset();
}
else
{
parent.add(top);
}
context.push(top);
}
else if (b==0x29 || b==0x5d) // end parenthesis/bracket
{
List top = (List)context.pop();
// flush genericSink
if (genericSink.size()>0)
{
byte[] tb = genericSink.toByteArray();
String token = new String(tb, DEFAULT_ENCODING).intern();
top.add(token);
genericSink.reset();
}
}
else if (b==0x7b) // literal length
{
genericSink.reset();
state = STATE_LITERAL_LENGTH;
}
else if (b==0x20) // token delimiter
{
if (context.size()==0) // end state
state = STATE_TEXT;
else // add token
{
List top = (List)context.peek();
// flush genericSink
if (genericSink.size()>0)
{
byte[] tb = genericSink.toByteArray();
String token = new String(tb, DEFAULT_ENCODING).intern();
top.add(token);
genericSink.reset();
}
}
}
else if (b==0x0a) // EOL
{
state = STATE_TAG;
// mark bytes read
mark(i);
return response;
}
else if (b!=0x0d) // ignore CR
genericSink.write(b);
}
break;
case STATE_LITERAL_LENGTH:
if (b==0x7d) // end literal length
{
byte[] cb = genericSink.toByteArray();
genericSink.reset();
String cs = new String(cb, DEFAULT_ENCODING);
try
{
literalLength = Integer.parseInt(cs);
}
catch (NumberFormatException e)
{
throw new ProtocolException("Expecting number: "+cs);
}
}
else if (b==0x0a) // start literal
{
state = STATE_LITERAL;
literalSink = new ByteArrayOutputStream();
literalCount = 0;
}
else if (b!=0x0d) // ignore CR
genericSink.write(b);
break;
case STATE_LITERAL:
if (literalCount>=literalLength)
{
List top = (List)context.peek();
byte[] literal = literalSink.toByteArray();
top.add(literal);
literalSink = null;
inContent = false;
state = STATE_CODE;
}
else
{
literalSink.write(b);
literalCount++;
}
break;
case STATE_TEXT: // human-readable text
if (b==0x0a) // delimiter
{
byte[] tb = genericSink.toByteArray();
genericSink.reset();
response.text = new String(tb, DEFAULT_ENCODING);
state = STATE_TAG;
// mark bytes read
mark(i);
return response;
}
else if (b!=0x0d) // ignore CR
genericSink.write(b);
break;
}
}
// get more bytes
buf = read(true);
return next();
}
}