/* * Copyright 2013 Hannes Janetzek * * This program is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later version. * * This program 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with * this program. If not, see . */ package org.oscim.tilesource.common; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.URL; import java.util.zip.InflaterInputStream; import org.oscim.core.Tile; import android.os.SystemClock; import android.util.Log; public class LwHttp { private static final String TAG = LwHttp.class.getName(); private final static byte[] HEADER_HTTP_OK = "200 OK".getBytes(); private final static byte[] HEADER_CONTENT_TYPE = "Content-Type".getBytes(); private final static byte[] HEADER_CONTENT_LENGTH = "Content-Length".getBytes(); private final static int RESPONSE_EXPECTED_LIVES = 100; private final static int RESPONSE_TIMEOUT = 10000; private final static int BUFFER_SIZE = 1024; private final byte[] buffer = new byte[BUFFER_SIZE]; private final String mHost; private final int mPort; private int mMaxReq = 0; private Socket mSocket; private OutputStream mCommandStream; private InputStream mResponseStream; private long mLastRequest = 0; private SocketAddress mSockAddr; private final byte[] REQUEST_GET_START; private final byte[] REQUEST_GET_END; private final byte[] mRequestBuffer; private final boolean mInflateContent; private final byte[] mContentType; private int mContentLength = -1; public LwHttp(URL url, String contentType, String extension, boolean deflate) { mContentType = contentType.getBytes(); mInflateContent = deflate; int port = url.getPort(); if (port < 0) port = 80; String host = url.getHost(); String path = url.getPath(); Log.d(TAG, "open database: " + host + " " + port + " " + path); REQUEST_GET_START = ("GET " + path).getBytes(); REQUEST_GET_END = ("." + extension + " HTTP/1.1" + "\nHost: " + host + "\nConnection: Keep-Alive" + "\n\n").getBytes(); mHost = host; mPort = port; mRequestBuffer = new byte[1024]; System.arraycopy(REQUEST_GET_START, 0, mRequestBuffer, 0, REQUEST_GET_START.length); } static class Buffer extends BufferedInputStream { public Buffer(InputStream is) { super(is, 4096); } @Override public synchronized int read() throws IOException { return super.read(); } @Override public synchronized int read(byte[] buffer, int offset, int byteCount) throws IOException { return super.read(buffer, offset, byteCount); } } public void close() { if (mSocket != null) { try { mSocket.close(); } catch (IOException e) { e.printStackTrace(); } finally { mSocket = null; } } } public InputStream readHeader() throws IOException { InputStream is = mResponseStream; is.mark(4096); byte[] buf = buffer; boolean first = true; boolean ok = true; int read = 0; int pos = 0; int end = 0; int len = 0; mContentLength = -1; // header cannot be larger than BUFFER_SIZE for this to work for (; (pos < read) || ((read < BUFFER_SIZE) && (len = is.read(buf, read, BUFFER_SIZE - read)) >= 0); len = 0) { read += len; // end of header lines while (end < read && (buf[end] != '\n')) end++; if (buf[end] != '\n') continue; if (!ok) { // ignore until end of header } else if (first) { first = false; // check only for OK ("HTTP/1.? ".length == 9) if (!check(HEADER_HTTP_OK, buf, pos + 9, end)) ok = false; } else if (end - pos == 1) { // empty line (header end) end += 1; break; } else if (check(HEADER_CONTENT_TYPE, buf, pos, end)) { if (!check(mContentType, buf, pos + HEADER_CONTENT_TYPE.length + 2, end)) ok = false; } else if (check(HEADER_CONTENT_LENGTH, buf, pos, end)) { mContentLength = parseInt(pos + HEADER_CONTENT_LENGTH.length + 2, end - 1, buf); } if (!ok) { String line = new String(buf, pos, end - pos - 1); Log.d(TAG, ">" + line + "< "); } pos += (end - pos) + 1; end = pos; } if (!ok) return null; // back to start of content is.reset(); is.mark(0); is.skip(end); if (mInflateContent) return new InflaterInputStream(is); return is; } public boolean sendRequest(Tile tile) throws IOException { if (mSocket != null && ((mMaxReq-- <= 0) || (SystemClock.elapsedRealtime() - mLastRequest > RESPONSE_TIMEOUT))) { try { mSocket.close(); } catch (IOException e) { Log.wtf(TAG, e); } // Log.d(TAG, "not alive - recreate connection " + mMaxReq); mSocket = null; } if (mSocket == null) { lwHttpConnect(); // we know our server mMaxReq = RESPONSE_EXPECTED_LIVES; // Log.d(TAG, "create connection"); } else { // FIXME not sure if this is correct way to drain socket int avail = mResponseStream.available(); if (avail > 0) { Log.d(TAG, "Consume left-over bytes: " + avail); while ((avail = mResponseStream.available()) > 0) mResponseStream.read(buffer); } } byte[] request = mRequestBuffer; int pos = REQUEST_GET_START.length; int newPos = 0; if ((newPos = formatTilePath(tile, request, pos)) == 0) { request[pos++] = '/'; pos = writeInt(tile.zoomLevel, pos, request); request[pos++] = '/'; pos = writeInt(tile.tileX, pos, request); request[pos++] = '/'; pos = writeInt(tile.tileY, pos, request); } else { pos = newPos; } int len = REQUEST_GET_END.length; System.arraycopy(REQUEST_GET_END, 0, request, pos, len); len += pos; try { mCommandStream.write(request, 0, len); mCommandStream.flush(); return true; } catch (IOException e) { Log.d(TAG, "recreate connection"); } lwHttpConnect(); mCommandStream.write(request, 0, len); mCommandStream.flush(); return true; } private boolean lwHttpConnect() throws IOException { if (mSockAddr == null) mSockAddr = new InetSocketAddress(mHost, mPort); mSocket = new Socket(); mSocket.connect(mSockAddr, 30000); mSocket.setTcpNoDelay(true); mCommandStream = mSocket.getOutputStream(); mResponseStream = new BufferedInputStream(mSocket.getInputStream()); return true; } // write (positive) integer to byte array protected static int writeInt(int val, int pos, byte[] buf) { if (val == 0) { buf[pos] = '0'; return pos + 1; } int i = 0; for (int n = val; n > 0; n = n / 10, i++) buf[pos + i] = (byte) ('0' + n % 10); // reverse bytes for (int j = pos, end = pos + i - 1, mid = pos + i / 2; j < mid; j++, end--) { byte tmp = buf[j]; buf[j] = buf[end]; buf[end] = tmp; } return pos + i; } // parse (positive) integer from byte array protected static int parseInt(int pos, int end, byte[] buf) { int val = 0; for (; pos < end; pos++) val = val * 10 + (buf[pos]) - '0'; return val; } private static boolean check(byte[] string, byte[] buffer, int position, int available) { int length = string.length; if (available - position < length) return false; for (int i = 0; i < length; i++) if (buffer[position + i] != string[i]) return false; return true; } public void requestCompleted() { mLastRequest = SystemClock.elapsedRealtime(); } public int getContentLength() { return mContentLength; } /** * Write custom tile url * * @param tile Tile * @param path to write url string * @param curPos current position * @return new position */ protected int formatTilePath(Tile tile, byte[] path, int curPos) { return 0; } }