honeydew 0.1.4

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