/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2003-2004 Pierre G. Richard *
* Copyright (C) 2003-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine 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. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.net;
import totalcross.io.*;
import totalcross.net.mail.*;
import totalcross.net.ssl.SSLSocket;
import totalcross.sys.*;
import totalcross.ui.image.Image;
import totalcross.ui.image.ImageException;
import totalcross.util.Hashtable;
/**
* A HttpStream HAS-A totalcross.net.Socket and takes care of exchange protocol. It starts reading (in a buffer) at the
* message-body. If you are having read problems, try to increase the timeout with Options.readtimeout.
*
* <p>
* Here is an example showing data being read from records in a PDBFile:
*
* <pre>
* public boolean verifyLogin()
* {
* String url = "http://www.totalcross.com.br/tests/legalizePalm.jsp?login=" + login + "&passwd=" + passwd;
* HttpStream hs = new HttpStream(new URI(url));
* byte[] buf = new byte[hs.contentLength];
* hs.readBytes(buf, 0, hs.contentLength);
* String str = new String(buf);
* return (str.equals("yes"));
* }
* </pre>
*
* At this case, the page legalizePalm verifies login and passwd, returning "yes" if it's ok, and "no" otherwise.
*/
public class HttpStream extends Stream
{
/** Can be overloaded by classes to late-init the data. */
protected HttpStream()
{
}
/**
* Constructor for a HttpStream with the default options.
*
* @param uri to connect to
* @see Options
*/
public HttpStream(URI uri) throws totalcross.net.UnknownHostException, totalcross.io.IOException
{
init(uri, new Options());
}
/**
* Constructor for a HttpStream with specific variant options.
* Options must have been filled before called this constructor.
*
* @param uri to connect to
* @param options the specific options for this HttpStream
* @see Options
*/
public HttpStream(URI uri, Options options) throws totalcross.net.UnknownHostException, totalcross.io.IOException
{
init(uri, options);
}
/** Set to true to print the header to the debug console. */
public static boolean debugHeader;
/** READ-ONLY should be one of the 20x. Used in the response. */
public int responseCode;
/** READ-ONLY encoding. Used in the response. */
public String contentEncoding; // flsobral@tc110_102: Content encoding support.
/** READ-ONLY the size of the returned data (-1 if unknown). Used in the response. */
public int contentLength;
/** READ-ONLY number of bytes read from the response's content. Initialized with 0 and incremented whenever the readBytes method is executed. */
public int contentRead; // flsobral@tc110_103: May be useful when content length is not provided.
/** READ-ONLY see the xxx_TYPE enum below. Used in the response. */
public byte contentType;
/** READ-ONLY HTTP Version. Used in the response. */
public ByteString version;
/** READ-ONLY connection status. Used in the response. */
public String connection; // guich@570_32
/** READ-ONLY location. Used in the response. */
public URI location;
/** READ-ONLY cookies. Null if response returned no cookies.
* You can get all returned cookies by iterating in the keys returned by
* <code>cookies.getKeys()</code>.
* If you want to persist a session, just store the cookies sent by the server
* and then send them back in each request you make, like this:
* <pre>
* public class SessionTest extends MainWindow
* {
* String url = "http://localhost:8080/servlet/sessionTest";
* Button btnGO;
* Hashtable cookies;
* ...
* if (event.target == btnGO)
* {
* HttpStream.Options options = new HttpStream.Options();
*
* // set cookies if they already exist
* if (cookies != null)
* options.setCookies(cookies);
*
* HttpStream st = new HttpStream(new URI(url),options);
*
* // Save cookies sent by server
* if (st.cookies != null)
* cookies = st.cookies;
* }
* ...
* }
* </pre>
* Note that its important that you call <code>Socket.disconnect()</code>
* before exiting your application.
* @see Options#setCookies(Hashtable)
*/
public Hashtable cookies; // guich@570_32
/** This Hashtable contains all headers that came in the response that don't belong to the public fields.
* @since TotalCross 1.62
*/
public Hashtable headers = new Hashtable(10);
/** Used in the contentType property. */
public static final byte UNKNOWN_TYPE = 0;
/** Used in the contentType property. */
public static final byte TEXT_HTML_TYPE = 1;
/** Used in the contentType property. */
public static final byte IMAGE_TYPE = 2;
/** Used in the contentType property. */
public static final byte MULTIPART_TYPE = 3;
/** Used in the httpType field
* @see Options#httpType
*/
public static final String GET = "GET ";
/** Used in the httpType field
* @see Options#httpType
*/
public static final String POST = "POST ";
/**
* This static class is used by one the constructor methods.
* It allows to tune the Stream to behave in variant ways.
*/
public static class Options
{
/**
* Basic support for proxy servers on HttpStream.
* You set the variables <var>proxyAddress</var> and <var>proxyPort</var>
* for this HttpStream instance to get the data thru your proxy.
*<p>
* There is yet the SSL part and the "don't use proxy for
* these address" part. I'm not interested in doing it, but a
* good resource is:
* http://www.innovation.ch/java/HTTPClient/advanced_info.html#proxies
* <p>
* I've just tested on my work proxy, using a bluetooth
* connection, so I'm not sure if it will work with
* other proxies.
*/
public String proxyAddress; // hsoares@421_63
/**
* Associated to the <var>proxyAddress</var>,
* <var>proxyPort</var> must match your proxy's port.
* Defaults to -1.
*/
public int proxyPort = -1; // hsoares@421_63
/** HTTP request headers. Default header:<br>
* User-Agent: <Setings.platform>/<Settings.deviceId><br>
* Note that <code>host</code>, appended later, is all lower-case.
*/
public Hashtable requestHeaders = new Hashtable(13); // flsobral@tc110_104: hash table for request headers.
/**
* Set this with your post commands to send a post. The final data string will be the concatenation of postPrefix+postDataSB+postData+postSuffix.
* @see #postPrefix
* @see #postSuffix
* @see #postDataSB
*/
public String postData;
/**
* Set this with your post commands to send a post. The final data string will be the concatenation of postPrefix+postDataSB+postData+postSuffix.
* @see #postPrefix
* @see #postSuffix
* @since TotalCross 1.23
*/
public StringBuffer postDataSB;
/**
* Set this with your post commands to send a post. The final data string will be the concatenation of postPrefix+postDataSB+postData+postSuffix.
* @see #postSuffix
* @see #postData
* @see #postDataSB
* @since TotalCross 1.23
*/
public String postPrefix; // guich@tc123_39
/**
* Set this with your post commands to send a post. The final data string will be the concatenation of postPrefix+postDataSB+postData+postSuffix.
* @see #postPrefix
* @see #postData
* @see #postDataSB
* @since TotalCross 1.23
*/
public String postSuffix; // guich@tc123_39
/** The headers used in POST. These are the default:<br>
* Content-Type: application/x-www-form-urlencoded<br>
*/
public Hashtable postHeaders = new Hashtable(13);
/** The read timeout. The default value is 5 seconds. */
public int readTimeOut = 5000;
/** The write timeout. The default value is to be the same value of readTimeOut. */
public int writeTimeOut = -1; // guich@tc114_1
/** The timeout to open. The default value is 25 seconds. */
public int openTimeOut = 25000; // guich@570_32
/** Set to false to not issue a GET, but a POST.
* @deprecated Use httpType instead
*/
public boolean doGet = true;
/** Set to true to issue a post instead of a get. Note that the doGet is automatically set to false.
<br>Here's a sample code: <pre>
String conn = hostUrl+"survey/RequestArticle.aspx";
HttpStream.Options options = new HttpStream.Options();
options.readTimeOut = 60000; // 1 minute
options.writeTimeOut = 50000; // 50 seconds
options.openTimeOut = 120000; // 2 minutes
options.doPost = true;
options.postData = "xml=<root><update login=\""+state.login+"\" password=\""+state.pass+"\" dayofsync=\""+state.dayOfSync+"\"/></root>";
XmlReadableSocket stream = new XmlReadableSocket(new URI(conn),options);
</pre>
@deprecated Use httpType instead
*/
public boolean doPost;
/** Defines the http type that will be used. To issue a POST, do:
* <pre>
* String conn = hostUrl+"survey/RequestArticle.aspx";
* HttpStream.Options options = new HttpStream.Options();
* options.readTimeOut = 60000; // 1 minute
* options.writeTimeOut = 50000; // 50 seconds
* options.openTimeOut = 120000; // 2 minutes
* options.httpType = HttpStream.POST;
* options.postData = "xml=<root><update login=\""+state.login+"\" password=\""+state.pass+"\" dayofsync=\""+state.dayOfSync+"\"/></root>";
* XmlReadableSocket stream = new XmlReadableSocket(new URI(conn),options);
* </pre>
* The default type is GET.<br><br>
* You can also define a custom type, like if you want to use restful services. In this case,
* the header will be set to what you store in the httpType String.
* @see #GET
* @see #POST
* @since TotalCross 1.23
*/
public String httpType = GET;
/** Number of bytes to write at once. Defaults to 1024. */
public int writeBytesSize = 1024;
/**
* Socket factory used to create the underlying connection. You may replace it with a SSLSocketFactory to make a
* HTTPS connection over a secure socket, or with your own subclass of SocketFactory.
*
* @since TotalCross 1.6
*/
public SocketFactory socketFactory = SocketFactory.getDefault();
/**
* Charset encoding ISO-8859-1
*/
public static final String CHARSET_ISO88591 = "ISO-8859-1";
/**
* Charset encoding UTF-8
*/
public static final String CHARSET_UTF8 = "UTF-8";
/**
* Charset encoding to be used by HttpStream. Defaults to CHARSET_ISO88591.
*/
private String encoding = CHARSET_ISO88591;
/** Constructs a new Options class, from where you change the behaviour of an Http connection.
* Sets the <code>postHeaders</code> to:
* <pre>
* Content-Type: application/x-www-form-urlencoded
* User-Agent: platform / deviceId
* </pre>
* You can override these if you want, just put the new values in the Hashtable or use
* other methods available in this class.
*/
public Options()
{
requestHeaders.put("User-Agent",Settings.platform+" / "+Settings.deviceId); // flsobral@tc110_104: this is a request header, not a post header.
postHeaders.put("Content-Type", "application/x-www-form-urlencoded");
}
/**
* Sets the charset encoding to be used by HttpStream.
*
* @param encoding
* the cjarset encoding to be set. Must be either "ISO-8859-1" or "UTF-8".
*/
public void setCharsetEncoding(String encoding)
{
if (encoding == CHARSET_ISO88591 || CHARSET_ISO88591.equals(encoding))
this.encoding = CHARSET_ISO88591;
else if (encoding == CHARSET_UTF8 || CHARSET_UTF8.equals(encoding))
this.encoding = CHARSET_UTF8;
else
throw new IllegalArgumentException();
}
/**
* Returns the currently set charset encoding.
*
* @return the charset encoding
*/
public String getCharsetEncoding()
{
return this.encoding;
}
/** Replaces the default Content-Type (<code>application/x-www-form-urlencoded</code>) by the given one. */
public void setContentType(String newContentType)
{
if (newContentType != null)
postHeaders.put("Content-Type", newContentType);
}
/** Sets the cookies to the ones stored in the given Hashtable. */
public void setCookies(Hashtable cookies) // guich@570_32
{
postHeaders.put("Cookie",cookies.dumpKeysValues(new StringBuffer(512), "=","; ").toString());
}
/**
* Base64 encodes the username and password given for basic server authentication
*
* @param user
* The username for the server. Passing null disables authentication.
* @param password
* The password for the username account on the server. Passing null disables authentication.
*/
public void setBasicAuthentication(String user, String password)
{
if (user == null || password == null)
postHeaders.remove("Authorization");
else
postHeaders.put("Authorization", Base64.encode((user + ":" + password).getBytes()));
}
/**
* Encodes the given username and password in Base64 for basic proxy authorization
*
* @param user
* the username for the proxy. Passing null disables proxy authorization.
* @param password
* the password for the proxy. Passing null disables proxy authorization.
* @since TotalCross 1.27
*/
public void setBasicProxyAuthorization(String user, String password)
{
if (user == null || password == null)
postHeaders.remove("Proxy-Authorization");
else
postHeaders.put("Proxy-Authorization", "Basic "+ Base64.encode((user + ":" + password).getBytes()));
}
/** Part that contains the Multipart content to be used by HttpStream */
Part partContent;
/**
* Set the given MIME multipart to be used as the HttpStream POST data.<br>
* This method may not be used with post data fields. These fields are ignored by the HttpStream after this method is used.
*
* @param multipart the multipart to be set as POST data
* @since TotalCross 1.25
*/
public void setContent(Multipart multipart) //flsobral@tc125_37: created the method setContent which receives a MIME multipart to be used as the HTTP POST data.
{
if (partContent == null)
partContent = new Part();
partContent.setContent(multipart);
}
}
/** This makes a sleep during the send of a file.
* Important: when using softick, you must set this to 500(ms) or more, or softick will
* starve to death.
*/
public int sendSleep; // guich@568_6
/** The delimiter used when reading data using readTokens.
* Once the readTokens method is called, changing the delimiter field will be useless.
* @since TotalCross 1.25
* @see #readTokens
*/
public char readTokensDelimiter;
/** This value is passed to the TokenReader created when readTokens is called.
* @since TotalCross 1.25
* @see TokenReader#doTrim
* @see #readTokens
*/
public boolean readTokensDoTrim;
private static final byte [] contentEncodingFieldName = "Content-Encoding:".getBytes(); // // flsobral@tc110_102: content encoding support.
private static final byte [] contentTypeFieldName = "Content-Type:".getBytes();
private static final byte [] contentLengthFieldName = "Content-Length:".getBytes();
private static final byte [] textType = "text/".getBytes();
private static final byte [] htmlType = "html".getBytes();
private static final byte [] imageType = "image/".getBytes();
private static final byte [] multipartType = "multipart/".getBytes();
private static final byte [] boundaryAtt = "boundary=".getBytes();
private static final byte [] connectionFieldName = "Connection:".getBytes();
private static final byte [] cookiesFieldName = "Set-Cookie:".getBytes();
private static final byte [] locationFieldName = "Location:".getBytes();
private static final ByteString bsContentEncodingFieldName = new ByteString(0, contentEncodingFieldName.length, contentEncodingFieldName); // flsobral@tc110_102: content encoding support.
private static final ByteString bsContentTypeFieldName = new ByteString(0, contentTypeFieldName.length, contentTypeFieldName);
private static final ByteString bsContentLengthFieldName = new ByteString(0, contentLengthFieldName.length, contentLengthFieldName);
private static final ByteString bsConnectionFieldName = new ByteString(0, connectionFieldName.length, connectionFieldName);
private static final ByteString bsCookiesFieldName = new ByteString(0, cookiesFieldName.length, cookiesFieldName);
private static final ByteString bsLocationFieldName = new ByteString(0, locationFieldName.length, locationFieldName);
protected Socket socket;
protected int ofsStart;
protected int ofsEnd;
protected byte[] buffer;
protected int readPos;
protected LineReader lr;
protected TokenReader tr;
private URI uri;
private int state;
private int ofsCur;
private byte[] multipartSep;
// Warning: make it enough big to hold the request!
private static final int BUFSIZE = 1024;
private int writeBytesSize;
/** Returns true if the response code represents an error. */
public boolean badResponseCode; // flsobral@tc115_65: Must be an instance field, otherwise the HttpStream will always return ok.
private CharacterConverter cc;
public int readBytes(byte buf[], int start, int count) throws totalcross.io.IOException
{
int bytesRead;
if (contentLength != -1)
{
if (contentRead == contentLength)
return 0;
if (contentLength - contentRead < count)
count = contentLength - contentRead;
}
if (ofsStart < ofsEnd)
{
int len = ofsEnd - ofsStart;
if (count < len)
{
Vm.arrayCopy(buffer, ofsStart, buf, start, count);
ofsStart += count;
bytesRead = count;
}
else
{
Vm.arrayCopy(buffer, ofsStart, buf, start, len);
ofsStart = ofsEnd;
buffer = null;
bytesRead = len;
if (count > len)
{
int n = socket.readBytes(buf, start+len, count-len);
bytesRead += (n == -1 ? 0 : n);
}
}
}
else
{
bytesRead = socket.readBytes(buf,start,count);
}
contentRead += (bytesRead == -1 ? 0 : bytesRead);
return bytesRead;
}
public int writeBytes(byte buf[], int start, int count) throws totalcross.io.IOException
{
int startPos = start;
int bytesLeft = count;
int sentBytes = 0;
while (bytesLeft > 0)
{
int ret = socket.writeBytes(buf, startPos, (bytesLeft >= writeBytesSize ? writeBytesSize : bytesLeft));
sentBytes += ret;
bytesLeft -= ret;
startPos += ret;
if (sendSleep > 0)
Vm.sleep(sendSleep); // this is needed for softick
}
return sentBytes;
}
public void close() throws totalcross.io.IOException
{
socket.close();
}
/**
* Internal method to initialize this HttpStream
*
* @param URI uri to connect to
* @param options the specific options for this HttpStream
* @throws totalcross.io.IOException
* @throws totalcross.net.UnknownHostException
*/
protected void init(URI uri, Options options) throws totalcross.net.UnknownHostException, totalcross.io.IOException
{
int port;
String strUri;
this.uri = uri;
if (options.proxyAddress != null) // hsoares@421_63
{
port = options.proxyPort;
strUri = options.proxyAddress;
}
else
{
port = uri.port;
if (uri.host == null)
throw new UnknownHostException("Please prefix the host with http:// or the correct protocol");
strUri = uri.host.toString();
}
contentLength = -1;
contentType = UNKNOWN_TYPE;
buffer = new byte[BUFSIZE];
if (port <= 0)
{
if (uri.scheme.toString().equals("https")) //flsobral@tc170: use 443 for https if not specified, fallback to 80 otherwise
port = 443;
else
port = 80;
}
state = -1;
socket = options.socketFactory.createSocket(strUri, port, options.openTimeOut);
socket.readTimeout = options.readTimeOut;
socket.writeTimeout = options.writeTimeOut == -1 ? options.readTimeOut : options.writeTimeOut;
if (socket instanceof SSLSocket)
((SSLSocket) socket).startHandshake();
writeBytesSize = options.writeBytesSize;
getResponse(options);
}
/**
* Initialize the HTTP dialog to get the response, and process the headers.
*
* @param options
*/
private void getResponse(Options options) throws totalcross.io.IOException
{
boolean useProxy = options.proxyAddress != null;
StringBuffer sb = new StringBuffer(2048);
if (options.partContent != null)
{
options.postHeaders.remove("Content-Type");
String header = (String) options.postHeaders.remove("Cookie");
if (header != null)
options.partContent.addHeader("Cookie", header);
}
else
{
String contentType = (String) options.postHeaders.get("Content-Type");
if (contentType != null && contentType.indexOf("charset") != -1)
options.postHeaders.put("Content-Type", contentType + ";charset=\"" + options.encoding + "\"");
if (options.encoding == Options.CHARSET_ISO88591)
cc = new CharacterConverter();
else if (options.encoding == Options.CHARSET_UTF8)
cc = new UTF8CharacterConverter();
}
if (GET.equals(options.httpType))
{options.doGet = true; options.doPost = false;}
else
if (POST.equals(options.httpType))
{options.doPost = true; options.doGet = false;}
if (!options.doGet) // if user set to false, make sure doPost is true.
options.doPost = true;
// Header Method
if (options.httpType != null)
sb.append(options.httpType);
String serverPath = useProxy ? uri.toString() : uri.path.toString(); //flsobral@tc126: had problems with a proxy that would only reply if we send the whole url on the post/get line.
if (POST.equals(options.httpType) || options.doPost)
{
if (options.httpType == null) sb.append("POST ");
// server path
sb.append(serverPath);
if (uri.query != null) // vitorhc@570_38: if the URI has also GET options, send them to the server
sb.append("?").append(uri.query.toString());
options.doGet = false;
}
if (GET.equals(options.httpType) || options.doGet)
{
useProxy = options.proxyAddress != null;
if (options.httpType == null) sb.append("GET ");
if (useProxy)
sb.append(serverPath);
else
{
if (uri.path != null && uri.path.len > 0)
sb.append(uri.path.toString());
else
sb.append("/");
if (uri.query != null && uri.query.len > 0)
{
sb.append("?");
sb.append(uri.query.toString());
}
}
}
// absolute URI
sb.append(" HTTP/1.0\r\n");
// Header Host
if (!options.requestHeaders.exists("Host")) // hsoares@421_63: only when no proxy
options.requestHeaders.put("Host", uri.host != null ? uri.host.toString() : ""); //flsobral@tc126: Host must always be provided, empty if not available. // guich@570_32: check if its already set
options.requestHeaders.dumpKeysValues(sb, ": ", Convert.CRLF);
sb.append(Convert.CRLF);
if (options.partContent == null && (POST.equals(options.httpType) || options.doPost))
{
int len = 0;
if (options.postPrefix != null)
len += cc.chars2bytes(options.postPrefix.toCharArray(), 0, options.postPrefix.length()).length;
if (options.postDataSB != null)
len += cc.chars2bytes(options.postDataSB.toString().toCharArray(), 0, options.postDataSB.length()).length;
if (options.postData != null)
len += cc.chars2bytes(options.postData.toCharArray(), 0, options.postData.length()).length;
if (options.postSuffix != null)
len += cc.chars2bytes(options.postSuffix.toCharArray(), 0, options.postSuffix.length()).length;
if (len > 0) //luciana@570_119: avoids null pointer exception if the post has no data
options.postHeaders.put("Content-Length", Convert.toString(len));
}
// get the post headers in a single string
options.postHeaders.dumpKeysValues(sb, ": ", Convert.CRLF); //flsobral@tc126: send post headers also on GET. Temporary fix to fix proxy and authorization support for both GET and POST.
sb.append(Convert.CRLF);
if (options.partContent == null)
sb.append(Convert.CRLF); // append the last line separator
if (debugHeader)
Vm.debug(sb.toString());
if (options.partContent == null)
writeResponseRequest(sb, options); //flsobral@tc120_17: fixed bug with HttpStream connection over BIS transport on BlackBerry.
else
{
byte[] bytes = cc.chars2bytes(sb.toString().toCharArray(), 0, sb.length());
writeBytes(bytes, 0, bytes.length);
try
{
options.partContent.writeTo(socket); //flsobral@tc125_XX: added support to set a MIME part as the POST data.
socket.writeBytes(Convert.CRLF + "0" + Convert.CRLF + Convert.CRLF);
}
catch (MessagingException e)
{
throw new IOException(e.getMessage());
}
}
ofsEnd = socket.readBytes(buffer,0,BUFSIZE);
state = 0;
while ((ofsCur < ofsEnd) && !readHttpHeader() && refill()) {}
if (state != 6 && Settings.onJavaSE) // flsobral@tc115: some cleaning.
Vm.debug("HTTP: " + getStatus()); // flsobral@tc110_95: No longer stop reading the header when a bad response code is found, so we can get the error cause.
}
protected void writeResponseRequest(StringBuffer sb, Options options) throws totalcross.io.IOException
{
byte[] bytes = cc.chars2bytes(sb.toString().toCharArray(), 0, sb.length());
writeBytes(bytes, 0, bytes.length);
bytes = null;
if (POST.equals(options.httpType) || options.doPost)
{
if (options.postPrefix != null)
{
bytes = cc.chars2bytes(options.postPrefix.toCharArray(), 0, options.postPrefix.length());
writeBytes(bytes, 0, bytes.length);
bytes = null;
}
if (options.postDataSB != null)
{
bytes = cc.chars2bytes(options.postDataSB.toString().toCharArray(), 0, options.postDataSB.length());
writeBytes(bytes, 0, bytes.length);
bytes = null;
}
if (options.postData != null)
{
bytes = cc.chars2bytes(options.postData.toCharArray(), 0, options.postData.length());
writeBytes(bytes, 0, bytes.length);
bytes = null;
}
if (options.postSuffix != null)
{
bytes = cc.chars2bytes(options.postSuffix.toCharArray(), 0, options.postSuffix.length());
writeBytes(bytes, 0, bytes.length);
bytes = null;
}
}
}
/**
* Tell if this HttpStream is functioning properly.
*
* @return true, if this HttpStream is functionning properly and the socket is open;
* false otherwise.
*/
public boolean isOk()
{
return (state == 6 && socket != null); // guich@570_70: now it also depends on socket be open
}
/**
* Get a human readable text to describe the current status
* of this HttpStream
*/
public String getStatus()
{
switch (state)
{
case -1:
return "Open error";
case 0:
return "Read error";
case 1:
return "Unknown version: " + version;
case 2:
return "Bad response: " + responseCode;
case 6:
return (socket != null) ? "OK." : "Socket closed"; // guich@553_12: is nonsense return "Ok" if the socket is closed
case 7:
return "Missing multipart boundary separator.";
default:
return "Premature end";
}
}
/**
* Refill the buffer, assuming that ofsCur is at ofsEnd.
*/
public final boolean refill() throws totalcross.io.IOException
{
if (ofsEnd == buffer.length) // sigh. no more room
{
if (ofsStart > 0) // oh, good! tidy still possible
{
Vm.arrayCopy(buffer, ofsStart, buffer, 0, ofsEnd-ofsStart);
readPos += ofsStart;
ofsCur -= ofsStart;
ofsStart = 0;
}
else // plenty full: extend!
{
byte oldBuffer[] = buffer;
buffer = new byte[oldBuffer.length << 1];
Vm.arrayCopy(buffer, 0, oldBuffer, 0, ofsEnd);
}
}
ofsEnd = ofsCur + socket.readBytes(buffer,ofsCur,buffer.length-ofsCur);
return (ofsEnd >= ofsCur);
}
/**
* Create the image instance from this HttpStream.
*
* @return the resulting totalcross.ui.image.Image
*/
public Image makeImage() throws ImageException, IOException // flsobral@tc100: throw the specific exceptions instead of the generic Exception
{
return new Image(this);
}
/**
* Position the stream until we find the beginning of the message-body.
* Fill-up the HTTP version and response code.
* <P>
* If the message-body was found, state is 6 and the message-body starts at
* ofsCur.
*
* @return true if we are done (either b/c an error occurred, or the
* message-body was found); false if the buffer needs to be refilled;
*/
private final boolean readHttpHeader() throws totalcross.io.IOException
{
while (ofsCur < ofsEnd)
{
byte ch = buffer[ofsCur];
switch (state)
{
case 0: // Status Line - skip leading spaces
while (ch == 0x20)
{
if (++ofsCur >= ofsEnd) return false;
ch = buffer[ofsCur];
}
ofsStart = ofsCur;
state = 1;
break;
case 1: // Status Line: HTTP-version
while (ch != 0x20)
{
if (++ofsCur >= ofsEnd) return false;
ch = buffer[ofsCur];
}
version = new ByteString(buffer, ofsStart, ofsCur-ofsStart);
ofsStart = ++ofsCur;
state = 2;
break;
case 2: // Status Line: Status-Code
while (ch != 0x20)
{
if (++ofsCur >= ofsEnd) return false;
ch = buffer[ofsCur];
}
responseCode = ByteString.convertToInt(buffer, ofsStart, ofsCur);
if ((responseCode < 200) || (responseCode >= 400))
badResponseCode = true; // not an HTTP response, or a bad one
ofsStart = ++ofsCur;
state = 3;
break;
case 3: // Looking for (CR)LF, ending non-fields (status line, etc)
case 4: // Looking for (CR)LF, ending a field
while (ch != (byte)'\n')
{
if (++ofsCur >= ofsEnd) return false;
ch = buffer[ofsCur];
}
if (state == 4)
{
setFields();
if (contentType == MULTIPART_TYPE)
{
contentType = UNKNOWN_TYPE;
if (!skipToNextMimePart())
{
state = 7;
return true; // can't get the mime part
}
else
{
state = 3; // take care of the (CR)LF
continue;
}
}
}
state = 5;
ofsStart = ++ofsCur;
break;
case 5: // (CR)LF found. Followed by another (CR)LF?
ofsStart = ofsCur++; // empty the buffer
switch (ch)
{
case 0x0d:
break;
case 0x0a:
state = 6; // Yes! Success!
break;
default:
state = 4;
break;
}
break;
case 6:
{
if (badResponseCode)
state = 2;
ofsStart = ofsCur;
return true; // Yes! Success!
}
}
}
return false;
}
/**
* Analyse and set a few relevant fields from the response,
*/
void setFields()
{
int type = -1;
int start = ofsStart;
int end,end1=0,start1=0;
if ((contentType == UNKNOWN_TYPE) && bsContentTypeFieldName.equalsIgnoreCase(buffer, ofsStart, bsContentTypeFieldName.len))
{
type = 0;
start += bsContentTypeFieldName.len;
}
else
if ((connection == null) && bsConnectionFieldName.equalsIgnoreCase(buffer, ofsStart, bsConnectionFieldName.len))
{
type = 1;
start += bsConnectionFieldName.len;
}
else
if ((cookies == null) && bsCookiesFieldName.equalsIgnoreCase(buffer, ofsStart, bsCookiesFieldName.len))
{
type = 2;
start += bsCookiesFieldName.len;
}
else
if ((contentLength == -1) && bsContentLengthFieldName.equalsIgnoreCase(buffer, ofsStart, bsContentLengthFieldName.len))
{
type = 3;
start += bsContentLengthFieldName.len;
}
else
if ((location == null) && bsLocationFieldName.equalsIgnoreCase(buffer, ofsStart, bsLocationFieldName.len))
{
type = 4;
start += bsLocationFieldName.len;
}
else
if ((contentEncoding == null) && bsContentEncodingFieldName.equalsIgnoreCase(buffer, ofsStart, bsContentEncodingFieldName.len))
{
type = 5; // flsobral@tc110_102: content encoding support.
start += bsContentEncodingFieldName.len;
}
else
{
start1 = start;
while (buffer[start] != ':' && buffer[start] >= 32)
start++;
end1 = start;
if (buffer[start] == ':') start++;
}
// trim the string
byte b;
end = ofsCur;
if (buffer[end-1] == (byte)'\r') --end;
while (((b=buffer[start]) == (byte)' ') || (b == (byte)'\t'))
{
++start;
}
while ((end > start) && (((b=buffer[end-1]) == (byte)' ') || (b == (byte)'\t')))
{
--end;
}
// now set the properties
switch (type)
{
case -1:
headers.put(new String(cc.bytes2chars(buffer,start1,end1-start1)), new String(cc.bytes2chars(buffer,start, end-start)));
break;
case 0:
contentType = getContentType(start, end-start);
break;
case 1:
connection = new String(buffer,start, end-start);
break;
case 2:
cookies = new Hashtable(31);
String s = new String(buffer, start, end-start);
String []cs = Convert.tokenizeString(s,';');
for (int i =0; i < cs.length; i++)
{
String tok = cs[i].trim();
int eq = tok.indexOf('=',0);
if (eq == -1)
continue;
String key = tok.substring(0,eq);
String value = tok.substring(eq+1);
cookies.put(key,value);
}
break;
case 3:
contentLength = ByteString.convertToInt(buffer, start, end);
break;
case 4:
location = new URI(new String(buffer, start, end-start));
break;
case 5:
contentEncoding = new String(buffer,start, end-start); // flsobral@tc110_102: content encoding support.
break;
}
}
/**
* Translate the content type into one of the xxx_TYPE values
*
* @param start start of the content-type value inside this buffer
* @param len length of the content-type value inside this buffer
* @return one of the corresponding xxx_TYPE values
*/
byte getContentType(int start, int len)
{
ByteString bsCntType = new ByteString(start, len, buffer);
if (bsCntType.equalsAtIgnoreCase(imageType, 0))
return IMAGE_TYPE;
else
if (bsCntType.equalsAtIgnoreCase(textType, 0))
{
if (bsCntType.equalsAtIgnoreCase(htmlType, imageType.length))
{
return TEXT_HTML_TYPE;
}
}
else
if (bsCntType.equalsAtIgnoreCase(multipartType, 0))
{
// Look for the "boundary=" attribute
int begBnd = bsCntType.indexOf(boundaryAtt, multipartType.length);
if (begBnd >= 0)
{
// get the value of this attribute
begBnd += boundaryAtt.length;
int endBnd = bsCntType.indexOf((byte) ';', begBnd);
if (endBnd == -1)
endBnd = len;
multipartSep = new byte[3 + endBnd - begBnd];
multipartSep[0] = (byte) '\n';
multipartSep[1] = (byte) '-';
multipartSep[2] = (byte) '-';
Vm.arrayCopy(buffer, start + begBnd, multipartSep, 3, endBnd - begBnd);
}
return MULTIPART_TYPE;
}
return UNKNOWN_TYPE;
}
/**
* Skip to the next mime part.
* <ul>
* <li>ofsCur points after the '\n' ending the content-type parameter
* <li>multipartSep can be null if no mime separator (boundary) was found
* </ul>
*
* @return true if we were able to position the stream on the '\n' that ends
* the mime-separator signaling the beginning of the mime part.
* @throws totalcross.io.IOException
*
* Actually, we only handle the first mime part This code might later be
* extended for other parts.
*/
protected boolean skipToNextMimePart() throws totalcross.io.IOException
{
if (multipartSep == null)
return false;
while (true)
{
ByteString bs;
int ix;
if ((ofsCur + multipartSep.length) > ofsEnd)
{
ofsStart = ofsCur; // make room
if (!refill()) return false;
}
bs = new ByteString(ofsCur, ofsEnd-ofsCur, buffer);
if ((ix = bs.indexOf(multipartSep, 0)) >= 0)
{
ofsCur += (ix+multipartSep.length);
break;
}
ofsCur = ofsEnd - multipartSep.length - 1;
}
ofsStart = ofsCur; // make room
return true;
}
/**
* Reads a line of text comming from the socket attached to this HttpStream.
* This method correctly handles newlines with \\n or \\r\\n.
* If you're reading tokens, use the readTokens method.
* Note that both readLine and readTokens cannot be used at the same time.
*
* @return the read line or <code>null</code> if nothing was read.
* @since SuperWaba 5.7
* @see #readTokens
*/
public String readLine() throws totalcross.io.IOException
{
if (lr == null)
lr = new LineReader(socket, buffer, ofsCur, ofsEnd-ofsCur);
return lr.readLine();
}
/**
* Reads a line of text comming from the socket attached to this HttpStream.
* This method correctly handles newlines with \\n or \\r\\n.
* If you're reading lines, use the readLines method.
* Note that both readLine and readTokens cannot be used at the same time.
* The delimiter must be set using the delimiter field, prior to using this method.
*
* @return the read line or <code>null</code> if nothing was read.
* @since TotalCross 1.25
* @see #readTokensDelimiter
* @see #readTokensDoTrim
* @see #readLine
*/
public String[] readTokens() throws totalcross.io.IOException // guich@tc125_16
{
if (tr == null)
{
tr = new TokenReader(socket, readTokensDelimiter, buffer, ofsCur, ofsEnd-ofsCur);
tr.doTrim = readTokensDoTrim;
}
return tr.readTokens();
}
}