calabash-android 0.1.0 → 0.2.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  module Calabash
2
2
  module Android
3
- VERSION = "0.1.0"
4
- FRAMEWORK_VERSION = "0.1.0"
3
+ VERSION = "0.2.0.pre2"
4
+ FRAMEWORK_VERSION = "0.2.0.pre2"
5
5
  end
6
6
  end
@@ -25,6 +25,8 @@
25
25
  <property name="android.lib" location="${env.ANDROID_HOME}/platforms/android-${android.api.level}/android.jar"/>
26
26
  <path id="android.antlibs">
27
27
  <pathelement path="${env.ANDROID_HOME}/tools/lib/anttasks.jar" />
28
+ <pathelement path="${env.ANDROID_HOME}/tools/lib/sdklib.jar" />
29
+ <pathelement path="${env.ANDROID_HOME}/tools/lib/androidprefs.jar" />
28
30
  </path>
29
31
  <taskdef name="xpath" classname="com.android.ant.XPathTask" classpathref="android.antlibs"/>
30
32
  <taskdef name="aapt" classname="com.android.ant.AaptExecTask" classpathref="android.antlibs" />
@@ -1,15 +1,21 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
3
- package="com.lesspainful.simpleui.test"
4
- android:versionCode="1"
5
- android:versionName="1.0">
6
- <application android:label="lesspainfultest">
3
+ package="sh.calaba.test"
4
+ android:versionCode="1"
5
+ android:versionName="1.0" >
7
6
 
8
- <uses-library android:name="android.test.runner" />
7
+ <uses-sdk android:minSdkVersion="6" />
8
+
9
+ <application android:label="CalabashTestServer" >
10
+ <uses-library android:name="android.test.runner" />
9
11
  </application>
10
-
11
- <instrumentation android:targetPackage="com.lesspainful.simpleui" android:name="android.test.InstrumentationTestRunner" />
12
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
13
- <uses-permission android:name="android.permission.INTERNET" />
14
-
15
- </manifest>
12
+
13
+ <instrumentation
14
+ android:name="android.test.InstrumentationTestRunner"
15
+ android:targetPackage="sh.calaba" />
16
+
17
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
18
+ <uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />
19
+ <uses-permission android:name="android.permission.INTERNET" />
20
+
21
+ </manifest>
@@ -1,20 +1,7 @@
1
1
  package sh.calaba.instrumentationbackend;
2
2
 
3
- import java.io.BufferedReader;
4
- import java.io.IOException;
5
- import java.io.InputStreamReader;
6
- import java.io.PrintStream;
7
- import java.io.UnsupportedEncodingException;
8
- import java.net.ServerSocket;
9
- import java.net.Socket;
10
- import java.net.SocketException;
11
-
12
-
13
3
  import sh.calaba.instrumentationbackend.actions.Actions;
14
- import sh.calaba.org.codehaus.jackson.JsonGenerationException;
15
- import sh.calaba.org.codehaus.jackson.map.JsonMappingException;
16
- import sh.calaba.org.codehaus.jackson.map.ObjectMapper;
17
- import sh.calaba.org.codehaus.jackson.map.DeserializationConfig.Feature;
4
+ import sh.calaba.instrumentationbackend.actions.HttpServer;
18
5
  import android.app.Instrumentation;
19
6
  import android.test.ActivityInstrumentationTestCase2;
20
7
  import android.util.Log;
@@ -27,15 +14,10 @@ public class InstrumentationBackend extends ActivityInstrumentationTestCase2 {
27
14
 
28
15
  private static final String TAG = "IntrumentationBackend";
29
16
 
30
- private ServerSocket myService;
31
- private Socket serviceSocket = null;
32
- private PrintStream resultStream;
33
- private BufferedReader commandStream;
34
-
35
- private ObjectMapper mapper;
36
17
  public static Instrumentation instrumentation;
37
18
  public static Solo solo;
38
19
  public static Actions actions;
20
+ private HttpServer httpServer;
39
21
 
40
22
  private static Class getActivityClass() {
41
23
  try {
@@ -53,25 +35,28 @@ public class InstrumentationBackend extends ActivityInstrumentationTestCase2 {
53
35
  protected void setUp() throws Exception {
54
36
  super.setUp();
55
37
 
56
- mapper = createJsonMapper();
57
- createSockets();
58
38
  solo = new Solo(getInstrumentation(), this.getActivity());
59
39
  actions = new Actions(getInstrumentation(), this);
60
40
  instrumentation = getInstrumentation();
61
41
  TestHelpers.loadIds(instrumentation.getContext());
62
42
  }
63
43
 
64
-
65
44
  /**
66
45
  * Here to have JUnit3 start the instrumentationBackend
67
46
  */
68
47
  public void testHook() throws Exception {
69
- startInstrumentationBackend();
70
-
48
+ httpServer = new HttpServer();
49
+ while(httpServer.isRunning()) {
50
+ Thread.sleep(500);
51
+ }
71
52
  }
72
53
 
73
54
  @Override
74
55
  public void tearDown() throws Exception {
56
+
57
+ if (httpServer != null) {
58
+ httpServer.stop();
59
+ }
75
60
  System.out.println("Finishing");
76
61
  try {
77
62
  solo.finalize();
@@ -84,108 +69,6 @@ public class InstrumentationBackend extends ActivityInstrumentationTestCase2 {
84
69
 
85
70
  }
86
71
 
87
- // TODO: create tearDown method. Should close sockets and streams
88
- // TODO: create setupUp method. create sockets, streams and jsonMapper,
89
- // build Action set.
90
- // TODO: Clean up waitForCucumberConnect
91
- // TODO: load actions
92
-
93
- public void startInstrumentationBackend() throws Exception {
94
-
95
- acceptConnectionFromCucumber();
96
-
97
- // listen for commands
98
- String commandString;
99
- while ((commandString = commandStream.readLine()) != null) {
100
- returnResult(runCommand(commandString));
101
- }
102
-
103
- // TODO: Move to teardown
104
- log("Cucumber disconnected");
105
- try {
106
- commandStream.close();
107
- resultStream.close();
108
- serviceSocket.close();
109
- myService.close();
110
- } catch (IOException e) {
111
- e.printStackTrace();
112
- }
113
- }
114
-
115
- private void returnResult(Result result) throws JsonGenerationException, JsonMappingException, IOException {
116
- log("Result:" + result);
117
- resultStream.println(mapper.writeValueAsString(result));
118
- resultStream.flush();
119
- }
120
-
121
- private Result runCommand(String commandString) {
122
- try {
123
- System.out.println("CommandString:" + commandString);
124
- Command command = mapper.readValue(commandString, Command.class);
125
- log("Got command:'" + command);
126
- return command.execute();
127
- // String result = mapper.writeValueAsString(actions.lookup(command.getCommand()).execute(command.getArguments()));
128
- } catch (Throwable t) { // Robotium throws AssertionErrors on occations,
129
- // we need to catch these and map them to json
130
- // results
131
- // TODO: Create result from Throwable
132
- t.printStackTrace();
133
- // String jsonResult =
134
- // mapper.writeValueAsString(Result.fromThrowable(t));
135
- // logError("Returning error:" + jsonResult);
136
- // output.println(jsonResult);
137
- // output.flush();
138
- return Result.fromThrowable(t);
139
- }
140
- }
141
-
142
- private void createResultStream() throws IOException {
143
- resultStream = new PrintStream(serviceSocket.getOutputStream());
144
- }
145
-
146
- private void createCommandStream() throws UnsupportedEncodingException, IOException {
147
- commandStream = new BufferedReader(new InputStreamReader(serviceSocket.getInputStream(), "UTF8"));
148
- }
149
-
150
- private ObjectMapper createJsonMapper() {
151
- ObjectMapper mapper = new ObjectMapper();
152
- mapper.configure(Feature.FAIL_ON_UNKNOWN_PROPERTIES, true);
153
- return mapper;
154
- }
155
-
156
- private void acceptConnectionFromCucumber() throws Exception {
157
- log("Waiting for connection from Cucumber");
158
-
159
- try {
160
- serviceSocket = myService.accept();
161
- } catch (Exception e) {
162
- log("Timeout waiting for connection");
163
- e.printStackTrace();
164
- throw new RuntimeException(e);
165
- }
166
-
167
- createCommandStream();
168
- createResultStream();
169
-
170
- String s;
171
- while ((s = commandStream.readLine()) != null) {
172
- if (s.equals("Ping!")) { // Cucumber connected
173
- resultStream.println("Pong!"); // Reply with acknowledgement
174
- resultStream.flush();
175
- break;
176
- }
177
- }
178
- }
179
-
180
- private void createSockets() throws IOException {
181
- try {
182
- myService = new ServerSocket(7101);
183
- myService.setSoTimeout(120000);
184
- } catch (SocketException e) {
185
- throw new RuntimeException("Could not create socket. Did you set the android.permission.INTERNET permission in your AndroidManifest.xml", e);
186
- }
187
- }
188
-
189
72
  public static void log(String message) {
190
73
  Log.i(TAG, message);
191
74
  }
@@ -0,0 +1,76 @@
1
+ package sh.calaba.instrumentationbackend.actions;
2
+
3
+ import java.io.File;
4
+ import java.util.Properties;
5
+
6
+ import sh.calaba.instrumentationbackend.Command;
7
+ import sh.calaba.instrumentationbackend.Result;
8
+ import sh.calaba.org.codehaus.jackson.map.DeserializationConfig.Feature;
9
+ import sh.calaba.org.codehaus.jackson.map.ObjectMapper;
10
+ import android.util.Log;
11
+
12
+ public class HttpServer extends NanoHTTPD {
13
+ private static final String TAG = "IntrumentationBackend";
14
+ private boolean running = true;
15
+
16
+ private ObjectMapper mapper;
17
+
18
+ public HttpServer() {
19
+ super(7102, new File("/"));
20
+
21
+ mapper = createJsonMapper();
22
+ }
23
+
24
+ public Response serve( String uri, String method, Properties header, Properties params, Properties files )
25
+ {
26
+ System.out.println("URI: " + uri);
27
+ if ("/ping".equals(uri)) {
28
+ return new NanoHTTPD.Response( HTTP_OK, MIME_HTML, "pong");
29
+ } else if ("/kill".equals(uri)) {
30
+ running = false;
31
+ System.out.println("Stopping test server");
32
+ stop();
33
+ return new NanoHTTPD.Response( HTTP_OK, MIME_HTML, "Affirmative!");
34
+ }
35
+
36
+ String commandString = params.getProperty("command");
37
+ System.out.println("command: "+ commandString);
38
+ String result = toJson(runCommand(commandString));
39
+ System.out.println("result:" + result);
40
+
41
+ return new NanoHTTPD.Response( HTTP_OK, MIME_HTML, result);
42
+ }
43
+
44
+ private ObjectMapper createJsonMapper() {
45
+ ObjectMapper mapper = new ObjectMapper();
46
+ mapper.configure(Feature.FAIL_ON_UNKNOWN_PROPERTIES, true);
47
+ return mapper;
48
+ }
49
+
50
+ private String toJson(Result result) {
51
+ try {
52
+ return mapper.writeValueAsString(result);
53
+ } catch (Exception e) {
54
+ throw new RuntimeException(e);
55
+ }
56
+ }
57
+
58
+ private Result runCommand(String commandString) {
59
+ try {
60
+ Command command = mapper.readValue(commandString, Command.class);
61
+ log("Got command:'" + command);
62
+ return command.execute();
63
+ } catch (Throwable t) {
64
+ t.printStackTrace();
65
+ return Result.fromThrowable(t);
66
+ }
67
+ }
68
+
69
+ public boolean isRunning() {
70
+ return running;
71
+ }
72
+
73
+ public static void log(String message) {
74
+ Log.i(TAG, message);
75
+ }
76
+ }
@@ -0,0 +1,1084 @@
1
+ package sh.calaba.instrumentationbackend.actions;
2
+
3
+ import java.io.BufferedReader;
4
+ import java.io.ByteArrayInputStream;
5
+ import java.io.File;
6
+ import java.io.FileInputStream;
7
+ import java.io.IOException;
8
+ import java.io.InputStream;
9
+ import java.io.InputStreamReader;
10
+ import java.io.OutputStream;
11
+ import java.io.PrintStream;
12
+ import java.io.PrintWriter;
13
+ import java.io.UnsupportedEncodingException;
14
+ import java.net.ServerSocket;
15
+ import java.net.Socket;
16
+ import java.net.URLEncoder;
17
+ import java.util.Date;
18
+ import java.util.Enumeration;
19
+ import java.util.Vector;
20
+ import java.util.Hashtable;
21
+ import java.util.Locale;
22
+ import java.util.Properties;
23
+ import java.util.StringTokenizer;
24
+ import java.util.TimeZone;
25
+
26
+ import java.io.ByteArrayOutputStream;
27
+ import java.io.FileOutputStream;
28
+
29
+ /**
30
+ * A simple, tiny, nicely embeddable HTTP 1.0 (partially 1.1) server in Java
31
+ *
32
+ * <p> NanoHTTPD version 1.25,
33
+ * Copyright &copy; 2001,2005-2012 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
34
+ * and Copyright &copy; 2010 Konstantinos Togias (info@ktogias.gr, http://ktogias.gr)
35
+ *
36
+ * <p><b>Features + limitations: </b><ul>
37
+ *
38
+ * <li> Only one Java file </li>
39
+ * <li> Java 1.1 compatible </li>
40
+ * <li> Released as open source, Modified BSD licence </li>
41
+ * <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
42
+ * <li> Supports parameter parsing of GET and POST methods (+ rudimentary PUT support in 1.25) </li>
43
+ * <li> Supports both dynamic content and file serving </li>
44
+ * <li> Supports file upload (since version 1.2, 2010) </li>
45
+ * <li> Supports partial content (streaming)</li>
46
+ * <li> Supports ETags</li>
47
+ * <li> Never caches anything </li>
48
+ * <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
49
+ * <li> Default code serves files and shows all HTTP parameters and headers</li>
50
+ * <li> File server supports directory listing, index.html and index.htm</li>
51
+ * <li> File server supports partial content (streaming)</li>
52
+ * <li> File server supports ETags</li>
53
+ * <li> File server does the 301 redirection trick for directories without '/'</li>
54
+ * <li> File server supports simple skipping for files (continue download) </li>
55
+ * <li> File server serves also very long files without memory overhead </li>
56
+ * <li> Contains a built-in list of most common mime types </li>
57
+ * <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
58
+ *
59
+ * </ul>
60
+ *
61
+ * <p><b>Ways to use: </b><ul>
62
+ *
63
+ * <li> Run as a standalone app, serves files and shows requests</li>
64
+ * <li> Subclass serve() and embed to your own program </li>
65
+ * <li> Call serveFile() from serve() with your own base directory </li>
66
+ *
67
+ * </ul>
68
+ *
69
+ * See the end of the source file for distribution license
70
+ * (Modified BSD licence)
71
+ */
72
+ public class NanoHTTPD
73
+ {
74
+ // ==================================================
75
+ // API parts
76
+ // ==================================================
77
+
78
+ /**
79
+ * Override this to customize the server.<p>
80
+ *
81
+ * (By default, this delegates to serveFile() and allows directory listing.)
82
+ *
83
+ * @param uri Percent-decoded URI without parameters, for example "/index.cgi"
84
+ * @param method "GET", "POST" etc.
85
+ * @param parms Parsed, percent decoded parameters from URI and, in case of POST, data.
86
+ * @param header Header entries, percent decoded
87
+ * @return HTTP response, see class Response for details
88
+ */
89
+ public Response serve( String uri, String method, Properties header, Properties parms, Properties files )
90
+ {
91
+ myOut.println( method + " '" + uri + "' " );
92
+
93
+ Enumeration e = header.propertyNames();
94
+ while ( e.hasMoreElements())
95
+ {
96
+ String value = (String)e.nextElement();
97
+ myOut.println( " HDR: '" + value + "' = '" +
98
+ header.getProperty( value ) + "'" );
99
+ }
100
+ e = parms.propertyNames();
101
+ while ( e.hasMoreElements())
102
+ {
103
+ String value = (String)e.nextElement();
104
+ myOut.println( " PRM: '" + value + "' = '" +
105
+ parms.getProperty( value ) + "'" );
106
+ }
107
+ e = files.propertyNames();
108
+ while ( e.hasMoreElements())
109
+ {
110
+ String value = (String)e.nextElement();
111
+ myOut.println( " UPLOADED: '" + value + "' = '" +
112
+ files.getProperty( value ) + "'" );
113
+ }
114
+
115
+ return serveFile( uri, header, myRootDir, true );
116
+ }
117
+
118
+ /**
119
+ * HTTP response.
120
+ * Return one of these from serve().
121
+ */
122
+ public class Response
123
+ {
124
+ /**
125
+ * Default constructor: response = HTTP_OK, data = mime = 'null'
126
+ */
127
+ public Response()
128
+ {
129
+ this.status = HTTP_OK;
130
+ }
131
+
132
+ /**
133
+ * Basic constructor.
134
+ */
135
+ public Response( String status, String mimeType, InputStream data )
136
+ {
137
+ this.status = status;
138
+ this.mimeType = mimeType;
139
+ this.data = data;
140
+ }
141
+
142
+ /**
143
+ * Convenience method that makes an InputStream out of
144
+ * given text.
145
+ */
146
+ public Response( String status, String mimeType, String txt )
147
+ {
148
+ this.status = status;
149
+ this.mimeType = mimeType;
150
+ try
151
+ {
152
+ this.data = new ByteArrayInputStream( txt.getBytes("UTF-8"));
153
+ }
154
+ catch ( java.io.UnsupportedEncodingException uee )
155
+ {
156
+ uee.printStackTrace();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Adds given line to the header.
162
+ */
163
+ public void addHeader( String name, String value )
164
+ {
165
+ header.put( name, value );
166
+ }
167
+
168
+ /**
169
+ * HTTP status code after processing, e.g. "200 OK", HTTP_OK
170
+ */
171
+ public String status;
172
+
173
+ /**
174
+ * MIME type of content, e.g. "text/html"
175
+ */
176
+ public String mimeType;
177
+
178
+ /**
179
+ * Data of the response, may be null.
180
+ */
181
+ public InputStream data;
182
+
183
+ /**
184
+ * Headers for the HTTP response. Use addHeader()
185
+ * to add lines.
186
+ */
187
+ public Properties header = new Properties();
188
+ }
189
+
190
+ /**
191
+ * Some HTTP response status codes
192
+ */
193
+ public static final String
194
+ HTTP_OK = "200 OK",
195
+ HTTP_PARTIALCONTENT = "206 Partial Content",
196
+ HTTP_RANGE_NOT_SATISFIABLE = "416 Requested Range Not Satisfiable",
197
+ HTTP_REDIRECT = "301 Moved Permanently",
198
+ HTTP_NOTMODIFIED = "304 Not Modified",
199
+ HTTP_FORBIDDEN = "403 Forbidden",
200
+ HTTP_NOTFOUND = "404 Not Found",
201
+ HTTP_BADREQUEST = "400 Bad Request",
202
+ HTTP_INTERNALERROR = "500 Internal Server Error",
203
+ HTTP_NOTIMPLEMENTED = "501 Not Implemented";
204
+
205
+ /**
206
+ * Common mime types for dynamic content
207
+ */
208
+ public static final String
209
+ MIME_PLAINTEXT = "text/plain",
210
+ MIME_HTML = "text/html",
211
+ MIME_DEFAULT_BINARY = "application/octet-stream",
212
+ MIME_XML = "text/xml";
213
+
214
+ // ==================================================
215
+ // Socket & server code
216
+ // ==================================================
217
+
218
+ /**
219
+ * Starts a HTTP server to given port.<p>
220
+ */
221
+ public NanoHTTPD( int port, File wwwroot )
222
+ {
223
+ try {
224
+ myTcpPort = port;
225
+ this.myRootDir = wwwroot;
226
+ myServerSocket = new ServerSocket( myTcpPort );
227
+ myThread = new Thread( new Runnable()
228
+ {
229
+ public void run()
230
+ {
231
+ try
232
+ {
233
+ while( true )
234
+ new HTTPSession( myServerSocket.accept());
235
+ }
236
+ catch ( IOException ioe )
237
+ {}
238
+ }
239
+ });
240
+ myThread.setDaemon( true );
241
+ myThread.start();
242
+ } catch (Exception e) {
243
+ throw new RuntimeException(e);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Stops the server.
249
+ */
250
+ public void stop()
251
+ {
252
+ try
253
+ {
254
+ myServerSocket.close();
255
+ myThread.join();
256
+ }
257
+ catch ( IOException ioe ) {}
258
+ catch ( InterruptedException e ) {}
259
+ }
260
+
261
+ /**
262
+ * Handles one session, i.e. parses the HTTP request
263
+ * and returns the response.
264
+ */
265
+ private class HTTPSession implements Runnable
266
+ {
267
+ public HTTPSession( Socket s )
268
+ {
269
+ mySocket = s;
270
+ Thread t = new Thread( this );
271
+ t.setDaemon( true );
272
+ t.start();
273
+ }
274
+
275
+ public void run()
276
+ {
277
+ try
278
+ {
279
+ InputStream is = mySocket.getInputStream();
280
+ if ( is == null) return;
281
+
282
+ // Read the first 8192 bytes.
283
+ // The full header should fit in here.
284
+ // Apache's default header limit is 8KB.
285
+ int bufsize = 8192;
286
+ byte[] buf = new byte[bufsize];
287
+ int rlen = is.read(buf, 0, bufsize);
288
+ if (rlen <= 0) return;
289
+
290
+ // Create a BufferedReader for parsing the header.
291
+ ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
292
+ BufferedReader hin = new BufferedReader( new InputStreamReader( hbis ));
293
+ Properties pre = new Properties();
294
+ Properties parms = new Properties();
295
+ Properties header = new Properties();
296
+ Properties files = new Properties();
297
+
298
+ // Decode the header into parms and header java properties
299
+ decodeHeader(hin, pre, parms, header);
300
+ String method = pre.getProperty("method");
301
+ String uri = pre.getProperty("uri");
302
+
303
+ long size = 0x7FFFFFFFFFFFFFFFl;
304
+ String contentLength = header.getProperty("content-length");
305
+ if (contentLength != null)
306
+ {
307
+ try { size = Integer.parseInt(contentLength); }
308
+ catch (NumberFormatException ex) {}
309
+ }
310
+
311
+ // We are looking for the byte separating header from body.
312
+ // It must be the last byte of the first two sequential new lines.
313
+ int splitbyte = 0;
314
+ boolean sbfound = false;
315
+ while (splitbyte < rlen)
316
+ {
317
+ if (buf[splitbyte] == '\r' && buf[++splitbyte] == '\n' && buf[++splitbyte] == '\r' && buf[++splitbyte] == '\n') {
318
+ sbfound = true;
319
+ break;
320
+ }
321
+ splitbyte++;
322
+ }
323
+ splitbyte++;
324
+
325
+ // Write the part of body already read to ByteArrayOutputStream f
326
+ ByteArrayOutputStream f = new ByteArrayOutputStream();
327
+ if (splitbyte < rlen) f.write(buf, splitbyte, rlen-splitbyte);
328
+
329
+ // While Firefox sends on the first read all the data fitting
330
+ // our buffer, Chrome and Opera sends only the headers even if
331
+ // there is data for the body. So we do some magic here to find
332
+ // out whether we have already consumed part of body, if we
333
+ // have reached the end of the data to be sent or we should
334
+ // expect the first byte of the body at the next read.
335
+ if (splitbyte < rlen)
336
+ size -= rlen - splitbyte +1;
337
+ else if (!sbfound || size == 0x7FFFFFFFFFFFFFFFl)
338
+ size = 0;
339
+
340
+ // Now read all the body and write it to f
341
+ buf = new byte[512];
342
+ while ( rlen >= 0 && size > 0 )
343
+ {
344
+ rlen = is.read(buf, 0, 512);
345
+ size -= rlen;
346
+ if (rlen > 0)
347
+ f.write(buf, 0, rlen);
348
+ }
349
+
350
+ // Get the raw body as a byte []
351
+ byte [] fbuf = f.toByteArray();
352
+
353
+ // Create a BufferedReader for easily reading it as string.
354
+ ByteArrayInputStream bin = new ByteArrayInputStream(fbuf);
355
+ BufferedReader in = new BufferedReader( new InputStreamReader(bin));
356
+
357
+ // If the method is POST, there may be parameters
358
+ // in data section, too, read it:
359
+ if ( method.equalsIgnoreCase( "POST" ))
360
+ {
361
+ String contentType = "";
362
+ String contentTypeHeader = header.getProperty("content-type");
363
+ StringTokenizer st = new StringTokenizer( contentTypeHeader , "; " );
364
+ if ( st.hasMoreTokens()) {
365
+ contentType = st.nextToken();
366
+ }
367
+
368
+ if (contentType.equalsIgnoreCase("multipart/form-data"))
369
+ {
370
+ // Handle multipart/form-data
371
+ if ( !st.hasMoreTokens())
372
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html" );
373
+ String boundaryExp = st.nextToken();
374
+ st = new StringTokenizer( boundaryExp , "=" );
375
+ if (st.countTokens() != 2)
376
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but boundary syntax error. Usage: GET /example/file.html" );
377
+ st.nextToken();
378
+ String boundary = st.nextToken();
379
+
380
+ decodeMultipartData(boundary, fbuf, in, parms, files);
381
+ }
382
+ else
383
+ {
384
+ // Handle application/x-www-form-urlencoded
385
+ String postLine = "";
386
+ char pbuf[] = new char[512];
387
+ int read = in.read(pbuf);
388
+ while ( read >= 0 && !postLine.endsWith("\r\n") )
389
+ {
390
+ postLine += String.valueOf(pbuf, 0, read);
391
+ read = in.read(pbuf);
392
+ }
393
+ postLine = postLine.trim();
394
+ decodeParms( postLine, parms );
395
+ }
396
+ }
397
+
398
+ if ( method.equalsIgnoreCase( "PUT" ))
399
+ files.put("content", saveTmpFile( fbuf, 0, f.size()));
400
+
401
+ // Ok, now do the serve()
402
+ Response r = serve( uri, method, header, parms, files );
403
+ if ( r == null )
404
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
405
+ else
406
+ sendResponse( r.status, r.mimeType, r.header, r.data );
407
+
408
+ in.close();
409
+ is.close();
410
+ }
411
+ catch ( IOException ioe )
412
+ {
413
+ try
414
+ {
415
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
416
+ }
417
+ catch ( Throwable t ) {}
418
+ }
419
+ catch ( InterruptedException ie )
420
+ {
421
+ // Thrown by sendError, ignore and exit the thread.
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Decodes the sent headers and loads the data into
427
+ * java Properties' key - value pairs
428
+ **/
429
+ private void decodeHeader(BufferedReader in, Properties pre, Properties parms, Properties header)
430
+ throws InterruptedException
431
+ {
432
+ try {
433
+ // Read the request line
434
+ String inLine = in.readLine();
435
+ if (inLine == null) return;
436
+ StringTokenizer st = new StringTokenizer( inLine );
437
+ if ( !st.hasMoreTokens())
438
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
439
+
440
+ String method = st.nextToken();
441
+ pre.put("method", method);
442
+
443
+ if ( !st.hasMoreTokens())
444
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
445
+
446
+ String uri = st.nextToken();
447
+
448
+ // Decode parameters from the URI
449
+ int qmi = uri.indexOf( '?' );
450
+ if ( qmi >= 0 )
451
+ {
452
+ decodeParms( uri.substring( qmi+1 ), parms );
453
+ uri = decodePercent( uri.substring( 0, qmi ));
454
+ }
455
+ else uri = decodePercent(uri);
456
+
457
+ // If there's another token, it's protocol version,
458
+ // followed by HTTP headers. Ignore version but parse headers.
459
+ // NOTE: this now forces header names lowercase since they are
460
+ // case insensitive and vary by client.
461
+ if ( st.hasMoreTokens())
462
+ {
463
+ String line = in.readLine();
464
+ while ( line != null && line.trim().length() > 0 )
465
+ {
466
+ int p = line.indexOf( ':' );
467
+ if ( p >= 0 )
468
+ header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
469
+ line = in.readLine();
470
+ }
471
+ }
472
+
473
+ pre.put("uri", uri);
474
+ }
475
+ catch ( IOException ioe )
476
+ {
477
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Decodes the Multipart Body data and put it
483
+ * into java Properties' key - value pairs.
484
+ **/
485
+ private void decodeMultipartData(String boundary, byte[] fbuf, BufferedReader in, Properties parms, Properties files)
486
+ throws InterruptedException
487
+ {
488
+ try
489
+ {
490
+ int[] bpositions = getBoundaryPositions(fbuf,boundary.getBytes());
491
+ int boundarycount = 1;
492
+ String mpline = in.readLine();
493
+ while ( mpline != null )
494
+ {
495
+ if (mpline.indexOf(boundary) == -1)
496
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but next chunk does not start with boundary. Usage: GET /example/file.html" );
497
+ boundarycount++;
498
+ Properties item = new Properties();
499
+ mpline = in.readLine();
500
+ while (mpline != null && mpline.trim().length() > 0)
501
+ {
502
+ int p = mpline.indexOf( ':' );
503
+ if (p != -1)
504
+ item.put( mpline.substring(0,p).trim().toLowerCase(), mpline.substring(p+1).trim());
505
+ mpline = in.readLine();
506
+ }
507
+ if (mpline != null)
508
+ {
509
+ String contentDisposition = item.getProperty("content-disposition");
510
+ if (contentDisposition == null)
511
+ {
512
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Content type is multipart/form-data but no content-disposition info found. Usage: GET /example/file.html" );
513
+ }
514
+ StringTokenizer st = new StringTokenizer( contentDisposition , "; " );
515
+ Properties disposition = new Properties();
516
+ while ( st.hasMoreTokens())
517
+ {
518
+ String token = st.nextToken();
519
+ int p = token.indexOf( '=' );
520
+ if (p!=-1)
521
+ disposition.put( token.substring(0,p).trim().toLowerCase(), token.substring(p+1).trim());
522
+ }
523
+ String pname = disposition.getProperty("name");
524
+ pname = pname.substring(1,pname.length()-1);
525
+
526
+ String value = "";
527
+ if (item.getProperty("content-type") == null) {
528
+ while (mpline != null && mpline.indexOf(boundary) == -1)
529
+ {
530
+ mpline = in.readLine();
531
+ if ( mpline != null)
532
+ {
533
+ int d = mpline.indexOf(boundary);
534
+ if (d == -1)
535
+ value+=mpline;
536
+ else
537
+ value+=mpline.substring(0,d-2);
538
+ }
539
+ }
540
+ }
541
+ else
542
+ {
543
+ if (boundarycount> bpositions.length)
544
+ sendError( HTTP_INTERNALERROR, "Error processing request" );
545
+ int offset = stripMultipartHeaders(fbuf, bpositions[boundarycount-2]);
546
+ String path = saveTmpFile(fbuf, offset, bpositions[boundarycount-1]-offset-4);
547
+ files.put(pname, path);
548
+ value = disposition.getProperty("filename");
549
+ value = value.substring(1,value.length()-1);
550
+ do {
551
+ mpline = in.readLine();
552
+ } while (mpline != null && mpline.indexOf(boundary) == -1);
553
+ }
554
+ parms.put(pname, value);
555
+ }
556
+ }
557
+ }
558
+ catch ( IOException ioe )
559
+ {
560
+ sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Find the byte positions where multipart boundaries start.
566
+ **/
567
+ public int[] getBoundaryPositions(byte[] b, byte[] boundary)
568
+ {
569
+ int matchcount = 0;
570
+ int matchbyte = -1;
571
+ Vector matchbytes = new Vector();
572
+ for (int i=0; i<b.length; i++)
573
+ {
574
+ if (b[i] == boundary[matchcount])
575
+ {
576
+ if (matchcount == 0)
577
+ matchbyte = i;
578
+ matchcount++;
579
+ if (matchcount==boundary.length)
580
+ {
581
+ matchbytes.addElement(new Integer(matchbyte));
582
+ matchcount = 0;
583
+ matchbyte = -1;
584
+ }
585
+ }
586
+ else
587
+ {
588
+ i -= matchcount;
589
+ matchcount = 0;
590
+ matchbyte = -1;
591
+ }
592
+ }
593
+ int[] ret = new int[matchbytes.size()];
594
+ for (int i=0; i < ret.length; i++)
595
+ {
596
+ ret[i] = ((Integer)matchbytes.elementAt(i)).intValue();
597
+ }
598
+ return ret;
599
+ }
600
+
601
+ /**
602
+ * Retrieves the content of a sent file and saves it
603
+ * to a temporary file.
604
+ * The full path to the saved file is returned.
605
+ **/
606
+ private String saveTmpFile(byte[] b, int offset, int len)
607
+ {
608
+ String path = "";
609
+ if (len > 0)
610
+ {
611
+ String tmpdir = System.getProperty("java.io.tmpdir");
612
+ try {
613
+ File temp = File.createTempFile("NanoHTTPD", "", new File(tmpdir));
614
+ OutputStream fstream = new FileOutputStream(temp);
615
+ fstream.write(b, offset, len);
616
+ fstream.close();
617
+ path = temp.getAbsolutePath();
618
+ } catch (Exception e) { // Catch exception if any
619
+ myErr.println("Error: " + e.getMessage());
620
+ }
621
+ }
622
+ return path;
623
+ }
624
+
625
+
626
+ /**
627
+ * It returns the offset separating multipart file headers
628
+ * from the file's data.
629
+ **/
630
+ private int stripMultipartHeaders(byte[] b, int offset)
631
+ {
632
+ int i = 0;
633
+ for (i=offset; i<b.length; i++)
634
+ {
635
+ if (b[i] == '\r' && b[++i] == '\n' && b[++i] == '\r' && b[++i] == '\n')
636
+ break;
637
+ }
638
+ return i+1;
639
+ }
640
+
641
+ /**
642
+ * Decodes the percent encoding scheme. <br/>
643
+ * For example: "an+example%20string" -> "an example string"
644
+ */
645
+ private String decodePercent( String str ) throws InterruptedException
646
+ {
647
+ try
648
+ {
649
+ StringBuffer sb = new StringBuffer();
650
+ for( int i=0; i<str.length(); i++ )
651
+ {
652
+ char c = str.charAt( i );
653
+ switch ( c )
654
+ {
655
+ case '+':
656
+ sb.append( ' ' );
657
+ break;
658
+ case '%':
659
+ sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
660
+ i += 2;
661
+ break;
662
+ default:
663
+ sb.append( c );
664
+ break;
665
+ }
666
+ }
667
+ return sb.toString();
668
+ }
669
+ catch( Exception e )
670
+ {
671
+ sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
672
+ return null;
673
+ }
674
+ }
675
+
676
+ /**
677
+ * Decodes parameters in percent-encoded URI-format
678
+ * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
679
+ * adds them to given Properties. NOTE: this doesn't support multiple
680
+ * identical keys due to the simplicity of Properties -- if you need multiples,
681
+ * you might want to replace the Properties with a Hashtable of Vectors or such.
682
+ */
683
+ private void decodeParms( String parms, Properties p )
684
+ throws InterruptedException
685
+ {
686
+ if ( parms == null )
687
+ return;
688
+
689
+ StringTokenizer st = new StringTokenizer( parms, "&" );
690
+ while ( st.hasMoreTokens())
691
+ {
692
+ String e = st.nextToken();
693
+ int sep = e.indexOf( '=' );
694
+ if ( sep >= 0 )
695
+ p.put( decodePercent( e.substring( 0, sep )).trim(),
696
+ decodePercent( e.substring( sep+1 )));
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Returns an error message as a HTTP response and
702
+ * throws InterruptedException to stop further request processing.
703
+ */
704
+ private void sendError( String status, String msg ) throws InterruptedException
705
+ {
706
+ sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
707
+ throw new InterruptedException();
708
+ }
709
+
710
+ /**
711
+ * Sends given response to the socket.
712
+ */
713
+ private void sendResponse( String status, String mime, Properties header, InputStream data )
714
+ {
715
+ try
716
+ {
717
+ if ( status == null )
718
+ throw new Error( "sendResponse(): Status can't be null." );
719
+
720
+ OutputStream out = mySocket.getOutputStream();
721
+ PrintWriter pw = new PrintWriter( out );
722
+ pw.print("HTTP/1.0 " + status + " \r\n");
723
+
724
+ if ( mime != null )
725
+ pw.print("Content-Type: " + mime + "\r\n");
726
+
727
+ if ( header == null || header.getProperty( "Date" ) == null )
728
+ pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
729
+
730
+ if ( header != null )
731
+ {
732
+ Enumeration e = header.keys();
733
+ while ( e.hasMoreElements())
734
+ {
735
+ String key = (String)e.nextElement();
736
+ String value = header.getProperty( key );
737
+ pw.print( key + ": " + value + "\r\n");
738
+ }
739
+ }
740
+
741
+ pw.print("\r\n");
742
+ pw.flush();
743
+
744
+ if ( data != null )
745
+ {
746
+ int pending = data.available(); // This is to support partial sends, see serveFile()
747
+ byte[] buff = new byte[theBufferSize];
748
+ while (pending>0)
749
+ {
750
+ int read = data.read( buff, 0, ( (pending>theBufferSize) ? theBufferSize : pending ));
751
+ if (read <= 0) break;
752
+ out.write( buff, 0, read );
753
+ pending -= read;
754
+ }
755
+ }
756
+ out.flush();
757
+ out.close();
758
+ if ( data != null )
759
+ data.close();
760
+ }
761
+ catch( IOException ioe )
762
+ {
763
+ // Couldn't write? No can do.
764
+ try { mySocket.close(); } catch( Throwable t ) {}
765
+ }
766
+ }
767
+
768
+ private Socket mySocket;
769
+ }
770
+
771
+ /**
772
+ * URL-encodes everything between "/"-characters.
773
+ * Encodes spaces as '%20' instead of '+'.
774
+ */
775
+ private String encodeUri( String uri )
776
+ {
777
+ String newUri = "";
778
+ StringTokenizer st = new StringTokenizer( uri, "/ ", true );
779
+ while ( st.hasMoreTokens())
780
+ {
781
+ String tok = st.nextToken();
782
+ if ( tok.equals( "/" ))
783
+ newUri += "/";
784
+ else if ( tok.equals( " " ))
785
+ newUri += "%20";
786
+ else
787
+ {
788
+ newUri += URLEncoder.encode( tok );
789
+ // For Java 1.4 you'll want to use this instead:
790
+ // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( java.io.UnsupportedEncodingException uee ) {}
791
+ }
792
+ }
793
+ return newUri;
794
+ }
795
+
796
+ private int myTcpPort;
797
+ private final ServerSocket myServerSocket;
798
+ private Thread myThread;
799
+ private File myRootDir;
800
+
801
+ // ==================================================
802
+ // File server code
803
+ // ==================================================
804
+
805
+ /**
806
+ * Serves file from homeDir and its' subdirectories (only).
807
+ * Uses only URI, ignores all headers and HTTP parameters.
808
+ */
809
+ public Response serveFile( String uri, Properties header, File homeDir,
810
+ boolean allowDirectoryListing )
811
+ {
812
+ Response res = null;
813
+
814
+ // Make sure we won't die of an exception later
815
+ if ( !homeDir.isDirectory())
816
+ res = new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
817
+ "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
818
+
819
+ if ( res == null )
820
+ {
821
+ // Remove URL arguments
822
+ uri = uri.trim().replace( File.separatorChar, '/' );
823
+ if ( uri.indexOf( '?' ) >= 0 )
824
+ uri = uri.substring(0, uri.indexOf( '?' ));
825
+
826
+ // Prohibit getting out of current directory
827
+ if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
828
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
829
+ "FORBIDDEN: Won't serve ../ for security reasons." );
830
+ }
831
+
832
+ File f = new File( homeDir, uri );
833
+ if ( res == null && !f.exists())
834
+ res = new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
835
+ "Error 404, file not found." );
836
+
837
+ // List the directory, if necessary
838
+ if ( res == null && f.isDirectory())
839
+ {
840
+ // Browsers get confused without '/' after the
841
+ // directory, send a redirect.
842
+ if ( !uri.endsWith( "/" ))
843
+ {
844
+ uri += "/";
845
+ res = new Response( HTTP_REDIRECT, MIME_HTML,
846
+ "<html><body>Redirected: <a href=\"" + uri + "\">" +
847
+ uri + "</a></body></html>");
848
+ res.addHeader( "Location", uri );
849
+ }
850
+
851
+ if ( res == null )
852
+ {
853
+ // First try index.html and index.htm
854
+ if ( new File( f, "index.html" ).exists())
855
+ f = new File( homeDir, uri + "/index.html" );
856
+ else if ( new File( f, "index.htm" ).exists())
857
+ f = new File( homeDir, uri + "/index.htm" );
858
+ // No index file, list the directory if it is readable
859
+ else if ( allowDirectoryListing && f.canRead() )
860
+ {
861
+ String[] files = f.list();
862
+ String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
863
+
864
+ if ( uri.length() > 1 )
865
+ {
866
+ String u = uri.substring( 0, uri.length()-1 );
867
+ int slash = u.lastIndexOf( '/' );
868
+ if ( slash >= 0 && slash < u.length())
869
+ msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
870
+ }
871
+
872
+ if (files!=null)
873
+ {
874
+ for ( int i=0; i<files.length; ++i )
875
+ {
876
+ File curFile = new File( f, files[i] );
877
+ boolean dir = curFile.isDirectory();
878
+ if ( dir )
879
+ {
880
+ msg += "<b>";
881
+ files[i] += "/";
882
+ }
883
+
884
+ msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
885
+ files[i] + "</a>";
886
+
887
+ // Show file size
888
+ if ( curFile.isFile())
889
+ {
890
+ long len = curFile.length();
891
+ msg += " &nbsp;<font size=2>(";
892
+ if ( len < 1024 )
893
+ msg += len + " bytes";
894
+ else if ( len < 1024 * 1024 )
895
+ msg += len/1024 + "." + (len%1024/10%100) + " KB";
896
+ else
897
+ msg += len/(1024*1024) + "." + len%(1024*1024)/10%100 + " MB";
898
+
899
+ msg += ")</font>";
900
+ }
901
+ msg += "<br/>";
902
+ if ( dir ) msg += "</b>";
903
+ }
904
+ }
905
+ msg += "</body></html>";
906
+ res = new Response( HTTP_OK, MIME_HTML, msg );
907
+ }
908
+ else
909
+ {
910
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
911
+ "FORBIDDEN: No directory listing." );
912
+ }
913
+ }
914
+ }
915
+
916
+ try
917
+ {
918
+ if ( res == null )
919
+ {
920
+ // Get MIME type from file name extension, if possible
921
+ String mime = null;
922
+ int dot = f.getCanonicalPath().lastIndexOf( '.' );
923
+ if ( dot >= 0 )
924
+ mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
925
+ if ( mime == null )
926
+ mime = MIME_DEFAULT_BINARY;
927
+
928
+ // Calculate etag
929
+ String etag = Integer.toHexString((f.getAbsolutePath() + f.lastModified() + "" + f.length()).hashCode());
930
+
931
+ // Support (simple) skipping:
932
+ long startFrom = 0;
933
+ long endAt = -1;
934
+ String range = header.getProperty( "range" );
935
+ if ( range != null )
936
+ {
937
+ if ( range.startsWith( "bytes=" ))
938
+ {
939
+ range = range.substring( "bytes=".length());
940
+ int minus = range.indexOf( '-' );
941
+ try {
942
+ if ( minus > 0 )
943
+ {
944
+ startFrom = Long.parseLong( range.substring( 0, minus ));
945
+ endAt = Long.parseLong( range.substring( minus+1 ));
946
+ }
947
+ }
948
+ catch ( NumberFormatException nfe ) {}
949
+ }
950
+ }
951
+
952
+ // Change return code and add Content-Range header when skipping is requested
953
+ long fileLen = f.length();
954
+ if (range != null && startFrom >= 0)
955
+ {
956
+ if ( startFrom >= fileLen)
957
+ {
958
+ res = new Response( HTTP_RANGE_NOT_SATISFIABLE, MIME_PLAINTEXT, "" );
959
+ res.addHeader( "Content-Range", "bytes 0-0/" + fileLen);
960
+ res.addHeader( "ETag", etag);
961
+ }
962
+ else
963
+ {
964
+ if ( endAt < 0 )
965
+ endAt = fileLen-1;
966
+ long newLen = endAt - startFrom + 1;
967
+ if ( newLen < 0 ) newLen = 0;
968
+
969
+ final long dataLen = newLen;
970
+ FileInputStream fis = new FileInputStream( f ) {
971
+ public int available() throws IOException { return (int)dataLen; }
972
+ };
973
+ fis.skip( startFrom );
974
+
975
+ res = new Response( HTTP_PARTIALCONTENT, mime, fis );
976
+ res.addHeader( "Content-Length", "" + dataLen);
977
+ res.addHeader( "Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
978
+ res.addHeader( "ETag", etag);
979
+ }
980
+ }
981
+ else
982
+ {
983
+ if (etag.equals(header.getProperty("if-none-match")))
984
+ res = new Response( HTTP_NOTMODIFIED, mime, "");
985
+ else
986
+ {
987
+ res = new Response( HTTP_OK, mime, new FileInputStream( f ));
988
+ res.addHeader( "Content-Length", "" + fileLen);
989
+ res.addHeader( "ETag", etag);
990
+ }
991
+ }
992
+ }
993
+ }
994
+ catch( IOException ioe )
995
+ {
996
+ res = new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
997
+ }
998
+
999
+ res.addHeader( "Accept-Ranges", "bytes"); // Announce that the file server accepts partial content requestes
1000
+ return res;
1001
+ }
1002
+
1003
+ /**
1004
+ * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
1005
+ */
1006
+ private static Hashtable theMimeTypes = new Hashtable();
1007
+ static
1008
+ {
1009
+ StringTokenizer st = new StringTokenizer(
1010
+ "css text/css "+
1011
+ "htm text/html "+
1012
+ "html text/html "+
1013
+ "xml text/xml "+
1014
+ "txt text/plain "+
1015
+ "asc text/plain "+
1016
+ "gif image/gif "+
1017
+ "jpg image/jpeg "+
1018
+ "jpeg image/jpeg "+
1019
+ "png image/png "+
1020
+ "mp3 audio/mpeg "+
1021
+ "m3u audio/mpeg-url " +
1022
+ "mp4 video/mp4 " +
1023
+ "ogv video/ogg " +
1024
+ "flv video/x-flv " +
1025
+ "mov video/quicktime " +
1026
+ "swf application/x-shockwave-flash " +
1027
+ "js application/javascript "+
1028
+ "pdf application/pdf "+
1029
+ "doc application/msword "+
1030
+ "ogg application/x-ogg "+
1031
+ "zip application/octet-stream "+
1032
+ "exe application/octet-stream "+
1033
+ "class application/octet-stream " );
1034
+ while ( st.hasMoreTokens())
1035
+ theMimeTypes.put( st.nextToken(), st.nextToken());
1036
+ }
1037
+
1038
+ private static int theBufferSize = 16 * 1024;
1039
+
1040
+ // Change these if you want to log to somewhere else than stdout
1041
+ protected static PrintStream myOut = System.out;
1042
+ protected static PrintStream myErr = System.err;
1043
+
1044
+ /**
1045
+ * GMT date formatter
1046
+ */
1047
+ private static java.text.SimpleDateFormat gmtFrmt;
1048
+ static
1049
+ {
1050
+ gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
1051
+ gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
1052
+ }
1053
+
1054
+ /**
1055
+ * The distribution licence
1056
+ */
1057
+ private static final String LICENCE =
1058
+ "Copyright (C) 2001,2005-2011 by Jarno Elonen <elonen@iki.fi>\n"+
1059
+ "and Copyright (C) 2010 by Konstantinos Togias <info@ktogias.gr>\n"+
1060
+ "\n"+
1061
+ "Redistribution and use in source and binary forms, with or without\n"+
1062
+ "modification, are permitted provided that the following conditions\n"+
1063
+ "are met:\n"+
1064
+ "\n"+
1065
+ "Redistributions of source code must retain the above copyright notice,\n"+
1066
+ "this list of conditions and the following disclaimer. Redistributions in\n"+
1067
+ "binary form must reproduce the above copyright notice, this list of\n"+
1068
+ "conditions and the following disclaimer in the documentation and/or other\n"+
1069
+ "materials provided with the distribution. The name of the author may not\n"+
1070
+ "be used to endorse or promote products derived from this software without\n"+
1071
+ "specific prior written permission. \n"+
1072
+ " \n"+
1073
+ "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
1074
+ "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
1075
+ "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
1076
+ "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
1077
+ "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
1078
+ "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
1079
+ "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
1080
+ "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
1081
+ "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
1082
+ "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
1083
+ }
1084
+