Clover Coverage Report
Coverage timestamp: Sat Sep 18 2010 04:09:52 UTC
../../../../../img/srcFileCovDistChart0.png 89% of files have more coverage
250   725   98   13.89
112   488   0.39   6
18     5.44  
3    
 
  NanoHTTPD       Line # 45 128 51 0% 0.0
  NanoHTTPD.Response       Line # 88 8 4 0% 0.0
  NanoHTTPD.HTTPSession       Line # 248 114 43 0% 0.0
 
No Tests
 
1    package org.qedeq.kernel.bo.test;
2   
3    import java.io.*;
4    import java.util.*;
5    import java.net.*;
6   
7    /**
8    * A simple, tiny, nicely embeddable HTTP 1.0 server in Java
9    *
10    * <p> NanoHTTPD version 1.13,
11    * Copyright &copy; 2001,2005-2010 Jarno Elonen (elonen@iki.fi, http://iki.fi/elonen/)
12    *
13    * <p><b>Features + limitations: </b><ul>
14    *
15    * <li> Only one Java file </li>
16    * <li> Java 1.1 compatible </li>
17    * <li> Released as open source, Modified BSD licence </li>
18    * <li> No fixed config files, logging, authorization etc. (Implement yourself if you need them.) </li>
19    * <li> Supports parameter parsing of GET and POST methods </li>
20    * <li> Supports both dynamic content and file serving </li>
21    * <li> Never caches anything </li>
22    * <li> Doesn't limit bandwidth, request time or simultaneous connections </li>
23    * <li> Default code serves files and shows all HTTP parameters and headers</li>
24    * <li> File server supports directory listing, index.html and index.htm </li>
25    * <li> File server does the 301 redirection trick for directories without '/'</li>
26    * <li> File server supports simple skipping for files (continue download) </li>
27    * <li> File server uses current directory as a web root </li>
28    * <li> File server serves also very long files without memory overhead </li>
29    * <li> Contains a built-in list of most common mime types </li>
30    * <li> All header names are converted lowercase so they don't vary between browsers/clients </li>
31    *
32    * </ul>
33    *
34    * <p><b>Ways to use: </b><ul>
35    *
36    * <li> Run as a standalone app, serves files from current directory and shows requests</li>
37    * <li> Subclass serve() and embed to your own program </li>
38    * <li> Call serveFile() from serve() with your own base directory </li>
39    *
40    * </ul>
41    *
42    * See the end of the source file for distribution license
43    * (Modified BSD licence)
44    */
 
45    public class NanoHTTPD
46    {
47    // ==================================================
48    // API parts
49    // ==================================================
50   
51    /**
52    * Override this to customize the server.<p>
53    *
54    * (By default, this delegates to serveFile() and allows directory listing.)
55    *
56    * @parm uri Percent-decoded URI without parameters, for example "/index.cgi"
57    * @parm method "GET", "POST" etc.
58    * @parm parms Parsed, percent decoded parameters from URI and, in case of POST, data.
59    * @parm header Header entries, percent decoded
60    * @return HTTP response, see class Response for details
61    */
 
62  0 toggle public Response serve( String uri, String method, Properties header, Properties parms )
63    {
64  0 System.out.println( method + " '" + uri + "' " );
65   
66  0 Enumeration e = header.propertyNames();
67  0 while ( e.hasMoreElements())
68    {
69  0 String value = (String)e.nextElement();
70  0 System.out.println( " HDR: '" + value + "' = '" +
71    header.getProperty( value ) + "'" );
72    }
73  0 e = parms.propertyNames();
74  0 while ( e.hasMoreElements())
75    {
76  0 String value = (String)e.nextElement();
77  0 System.out.println( " PRM: '" + value + "' = '" +
78    parms.getProperty( value ) + "'" );
79    }
80   
81  0 return serveFile( uri, header, new File("."), true );
82    }
83   
84    /**
85    * HTTP response.
86    * Return one of these from serve().
87    */
 
88    public class Response
89    {
90    /**
91    * Default constructor: response = HTTP_OK, data = mime = 'null'
92    */
 
93  0 toggle public Response()
94    {
95  0 this.status = HTTP_OK;
96    }
97   
98    /**
99    * Basic constructor.
100    */
 
101  0 toggle public Response( String status, String mimeType, InputStream data )
102    {
103  0 this.status = status;
104  0 this.mimeType = mimeType;
105  0 this.data = data;
106    }
107   
108    /**
109    * Convenience method that makes an InputStream out of
110    * given text.
111    */
 
112  0 toggle public Response( String status, String mimeType, String txt )
113    {
114  0 this.status = status;
115  0 this.mimeType = mimeType;
116  0 this.data = new ByteArrayInputStream( txt.getBytes());
117    }
118   
119    /**
120    * Adds given line to the header.
121    */
 
122  0 toggle public void addHeader( String name, String value )
123    {
124  0 header.put( name, value );
125    }
126   
127    /**
128    * HTTP status code after processing, e.g. "200 OK", HTTP_OK
129    */
130    public String status;
131   
132    /**
133    * MIME type of content, e.g. "text/html"
134    */
135    public String mimeType;
136   
137    /**
138    * Data of the response, may be null.
139    */
140    public InputStream data;
141   
142    /**
143    * Headers for the HTTP response. Use addHeader()
144    * to add lines.
145    */
146    public Properties header = new Properties();
147    }
148   
149    /**
150    * Some HTTP response status codes
151    */
152    public static final String
153    HTTP_OK = "200 OK",
154    HTTP_REDIRECT = "301 Moved Permanently",
155    HTTP_FORBIDDEN = "403 Forbidden",
156    HTTP_NOTFOUND = "404 Not Found",
157    HTTP_BADREQUEST = "400 Bad Request",
158    HTTP_INTERNALERROR = "500 Internal Server Error",
159    HTTP_NOTIMPLEMENTED = "501 Not Implemented";
160   
161    /**
162    * Common mime types for dynamic content
163    */
164    public static final String
165    MIME_PLAINTEXT = "text/plain",
166    MIME_HTML = "text/html",
167    MIME_DEFAULT_BINARY = "application/octet-stream";
168   
169    // ==================================================
170    // Socket & server code
171    // ==================================================
172   
173    /**
174    * Starts a HTTP server to given port.<p>
175    * Throws an IOException if the socket is already in use
176    */
 
177  0 toggle public NanoHTTPD( int port ) throws IOException
178    {
179  0 myTcpPort = port;
180   
181  0 final ServerSocket ss = new ServerSocket( myTcpPort );
182  0 Thread t = new Thread( new Runnable()
183    {
 
184  0 toggle public void run()
185    {
186  0 try
187    {
188  0 while( true )
189  0 new HTTPSession( ss.accept());
190    }
191    catch ( IOException ioe )
192    {}
193    }
194    });
195  0 t.setDaemon( true );
196  0 t.start();
197    }
198   
199    /**
200    * Starts as a standalone file server and waits for Enter.
201    */
 
202  0 toggle public static void main( String[] args )
203    {
204  0 System.out.println( "NanoHTTPD 1.13 (C) 2001,2005-2010 Jarno Elonen\n" +
205    "(Command line options: [port] [--licence])\n" );
206   
207    // Show licence if requested
208  0 int lopt = -1;
209  0 for ( int i=0; i<args.length; ++i )
210  0 if ( args[i].toLowerCase().endsWith( "licence" ))
211    {
212  0 lopt = i;
213  0 System.out.println( LICENCE + "\n" );
214    }
215   
216    // Change port if requested
217  0 int port = 80;
218  0 if ( args.length > 0 && lopt != 0 )
219  0 port = Integer.parseInt( args[0] );
220   
221  0 if ( args.length > 1 &&
222    args[1].toLowerCase().endsWith( "licence" ))
223  0 System.out.println( LICENCE + "\n" );
224   
225  0 NanoHTTPD nh = null;
226  0 try
227    {
228  0 nh = new NanoHTTPD( port );
229    }
230    catch( IOException ioe )
231    {
232  0 System.err.println( "Couldn't start server:\n" + ioe );
233  0 System.exit( -1 );
234    }
235  0 nh.myFileDir = new File("");
236   
237  0 System.out.println( "Now serving files in port " + port + " from \"" +
238    new File("").getAbsolutePath() + "\"" );
239  0 System.out.println( "Hit Enter to stop.\n" );
240   
241  0 try { System.in.read(); } catch( Throwable t ) {};
242    }
243   
244    /**
245    * Handles one session, i.e. parses the HTTP request
246    * and returns the response.
247    */
 
248    private class HTTPSession implements Runnable
249    {
 
250  0 toggle public HTTPSession( Socket s )
251    {
252  0 mySocket = s;
253  0 Thread t = new Thread( this );
254  0 t.setDaemon( true );
255  0 t.start();
256    }
257   
 
258  0 toggle public void run()
259    {
260  0 try
261    {
262  0 InputStream is = mySocket.getInputStream();
263  0 if ( is == null) return;
264  0 BufferedReader in = new BufferedReader( new InputStreamReader( is ));
265   
266    // Read the request line
267  0 String inLine = in.readLine();
268  0 if (inLine == null) return;
269  0 StringTokenizer st = new StringTokenizer( inLine );
270  0 if ( !st.hasMoreTokens())
271  0 sendError( HTTP_BADREQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html" );
272   
273  0 String method = st.nextToken();
274   
275  0 if ( !st.hasMoreTokens())
276  0 sendError( HTTP_BADREQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html" );
277   
278  0 String uri = st.nextToken();
279   
280    // Decode parameters from the URI
281  0 Properties parms = new Properties();
282  0 int qmi = uri.indexOf( '?' );
283  0 if ( qmi >= 0 )
284    {
285  0 decodeParms( uri.substring( qmi+1 ), parms );
286  0 uri = decodePercent( uri.substring( 0, qmi ));
287    }
288  0 else uri = decodePercent(uri);
289   
290   
291    // If there's another token, it's protocol version,
292    // followed by HTTP headers. Ignore version but parse headers.
293    // NOTE: this now forces header names uppercase since they are
294    // case insensitive and vary by client.
295  0 Properties header = new Properties();
296  0 if ( st.hasMoreTokens())
297    {
298  0 String line = in.readLine();
299  0 while ( line.trim().length() > 0 )
300    {
301  0 int p = line.indexOf( ':' );
302  0 header.put( line.substring(0,p).trim().toLowerCase(), line.substring(p+1).trim());
303  0 line = in.readLine();
304    }
305    }
306   
307    // If the method is POST, there may be parameters
308    // in data section, too, read it:
309  0 if ( method.equalsIgnoreCase( "POST" ))
310    {
311  0 long size = 0x7FFFFFFFFFFFFFFFl;
312  0 String contentLength = header.getProperty("content-length");
313  0 if (contentLength != null)
314    {
315  0 try { size = Integer.parseInt(contentLength); }
316    catch (NumberFormatException ex) {}
317    }
318  0 String postLine = "";
319  0 char buf[] = new char[512];
320  0 int read = in.read(buf);
321  0 while ( read >= 0 && size > 0 && !postLine.endsWith("\r\n") )
322    {
323  0 size -= read;
324  0 postLine += String.valueOf(buf, 0, read);
325  0 if ( size > 0 )
326  0 read = in.read(buf);
327    }
328  0 postLine = postLine.trim();
329  0 decodeParms( postLine, parms );
330    }
331   
332    // Ok, now do the serve()
333  0 Response r = serve( uri, method, header, parms );
334  0 if ( r == null )
335  0 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: Serve() returned a null response." );
336    else
337  0 sendResponse( r.status, r.mimeType, r.header, r.data );
338   
339  0 in.close();
340    }
341    catch ( IOException ioe )
342    {
343  0 try
344    {
345  0 sendError( HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
346    }
347    catch ( Throwable t ) {}
348    }
349    catch ( InterruptedException ie )
350    {
351    // Thrown by sendError, ignore and exit the thread.
352    }
353    }
354   
355    /**
356    * Decodes the percent encoding scheme. <br/>
357    * For example: "an+example%20string" -> "an example string"
358    */
 
359  0 toggle private String decodePercent( String str ) throws InterruptedException
360    {
361  0 try
362    {
363  0 StringBuffer sb = new StringBuffer();
364  0 for( int i=0; i<str.length(); i++ )
365    {
366  0 char c = str.charAt( i );
367  0 switch ( c )
368    {
369  0 case '+':
370  0 sb.append( ' ' );
371  0 break;
372  0 case '%':
373  0 sb.append((char)Integer.parseInt( str.substring(i+1,i+3), 16 ));
374  0 i += 2;
375  0 break;
376  0 default:
377  0 sb.append( c );
378  0 break;
379    }
380    }
381  0 return new String( sb.toString().getBytes());
382    }
383    catch( Exception e )
384    {
385  0 sendError( HTTP_BADREQUEST, "BAD REQUEST: Bad percent-encoding." );
386  0 return null;
387    }
388    }
389   
390    /**
391    * Decodes parameters in percent-encoded URI-format
392    * ( e.g. "name=Jack%20Daniels&pass=Single%20Malt" ) and
393    * adds them to given Properties. NOTE: this doesn't support multiple
394    * identical keys due to the simplicity of Properties -- if you need multiples,
395    * you might want to replace the Properties with a Hastable of Vectors or such.
396    */
 
397  0 toggle private void decodeParms( String parms, Properties p )
398    throws InterruptedException
399    {
400  0 if ( parms == null )
401  0 return;
402   
403  0 StringTokenizer st = new StringTokenizer( parms, "&" );
404  0 while ( st.hasMoreTokens())
405    {
406  0 String e = st.nextToken();
407  0 int sep = e.indexOf( '=' );
408  0 if ( sep >= 0 )
409  0 p.put( decodePercent( e.substring( 0, sep )).trim(),
410    decodePercent( e.substring( sep+1 )));
411    }
412    }
413   
414    /**
415    * Returns an error message as a HTTP response and
416    * throws InterruptedException to stop furhter request processing.
417    */
 
418  0 toggle private void sendError( String status, String msg ) throws InterruptedException
419    {
420  0 sendResponse( status, MIME_PLAINTEXT, null, new ByteArrayInputStream( msg.getBytes()));
421  0 throw new InterruptedException();
422    }
423   
424    /**
425    * Sends given response to the socket.
426    */
 
427  0 toggle private void sendResponse( String status, String mime, Properties header, InputStream data )
428    {
429  0 try
430    {
431  0 if ( status == null )
432  0 throw new Error( "sendResponse(): Status can't be null." );
433   
434  0 OutputStream out = mySocket.getOutputStream();
435  0 PrintWriter pw = new PrintWriter( out );
436  0 pw.print("HTTP/1.0 " + status + " \r\n");
437   
438  0 if ( mime != null )
439  0 pw.print("Content-Type: " + mime + "\r\n");
440   
441  0 if ( header == null || header.getProperty( "Date" ) == null )
442  0 pw.print( "Date: " + gmtFrmt.format( new Date()) + "\r\n");
443   
444  0 if ( header != null )
445    {
446  0 Enumeration e = header.keys();
447  0 while ( e.hasMoreElements())
448    {
449  0 String key = (String)e.nextElement();
450  0 String value = header.getProperty( key );
451  0 pw.print( key + ": " + value + "\r\n");
452    }
453    }
454   
455  0 pw.print("\r\n");
456  0 pw.flush();
457   
458  0 if ( data != null )
459    {
460  0 byte[] buff = new byte[2048];
461  0 while (true)
462    {
463  0 int read = data.read( buff, 0, 2048 );
464  0 if (read <= 0)
465  0 break;
466  0 out.write( buff, 0, read );
467    }
468    }
469  0 out.flush();
470  0 out.close();
471  0 if ( data != null )
472  0 data.close();
473    }
474    catch( IOException ioe )
475    {
476    // Couldn't write? No can do.
477  0 try { mySocket.close(); } catch( Throwable t ) {}
478    }
479    }
480   
481    private Socket mySocket;
482    };
483   
484    /**
485    * URL-encodes everything between "/"-characters.
486    * Encodes spaces as '%20' instead of '+'.
487    */
 
488  0 toggle private String encodeUri( String uri )
489    {
490  0 String newUri = "";
491  0 StringTokenizer st = new StringTokenizer( uri, "/ ", true );
492  0 while ( st.hasMoreTokens())
493    {
494  0 String tok = st.nextToken();
495  0 if ( tok.equals( "/" ))
496  0 newUri += "/";
497  0 else if ( tok.equals( " " ))
498  0 newUri += "%20";
499    else
500    {
501  0 newUri += URLEncoder.encode( tok );
502    // For Java 1.4 you'll want to use this instead:
503    // try { newUri += URLEncoder.encode( tok, "UTF-8" ); } catch ( UnsupportedEncodingException uee )
504    }
505    }
506  0 return newUri;
507    }
508   
509    private int myTcpPort;
510    File myFileDir;
511   
512    // ==================================================
513    // File server code
514    // ==================================================
515   
516    /**
517    * Serves file from homeDir and its' subdirectories (only).
518    * Uses only URI, ignores all headers and HTTP parameters.
519    */
 
520  0 toggle public Response serveFile( String uri, Properties header, File homeDir,
521    boolean allowDirectoryListing )
522    {
523    // Make sure we won't die of an exception later
524  0 if ( !homeDir.isDirectory())
525  0 return new Response( HTTP_INTERNALERROR, MIME_PLAINTEXT,
526    "INTERNAL ERRROR: serveFile(): given homeDir is not a directory." );
527   
528    // Remove URL arguments
529  0 uri = uri.trim().replace( File.separatorChar, '/' );
530  0 if ( uri.indexOf( '?' ) >= 0 )
531  0 uri = uri.substring(0, uri.indexOf( '?' ));
532   
533    // Prohibit getting out of current directory
534  0 if ( uri.startsWith( ".." ) || uri.endsWith( ".." ) || uri.indexOf( "../" ) >= 0 )
535  0 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
536    "FORBIDDEN: Won't serve ../ for security reasons." );
537   
538  0 File f = new File( homeDir, uri );
539  0 if ( !f.exists())
540  0 return new Response( HTTP_NOTFOUND, MIME_PLAINTEXT,
541    "Error 404, file not found." );
542   
543    // List the directory, if necessary
544  0 if ( f.isDirectory())
545    {
546    // Browsers get confused without '/' after the
547    // directory, send a redirect.
548  0 if ( !uri.endsWith( "/" ))
549    {
550  0 uri += "/";
551  0 Response r = new Response( HTTP_REDIRECT, MIME_HTML,
552    "<html><body>Redirected: <a href=\"" + uri + "\">" +
553    uri + "</a></body></html>");
554  0 r.addHeader( "Location", uri );
555  0 return r;
556    }
557   
558    // First try index.html and index.htm
559  0 if ( new File( f, "index.html" ).exists())
560  0 f = new File( homeDir, uri + "/index.html" );
561  0 else if ( new File( f, "index.htm" ).exists())
562  0 f = new File( homeDir, uri + "/index.htm" );
563   
564    // No index file, list the directory
565  0 else if ( allowDirectoryListing )
566    {
567  0 String[] files = f.list();
568  0 String msg = "<html><body><h1>Directory " + uri + "</h1><br/>";
569   
570  0 if ( uri.length() > 1 )
571    {
572  0 String u = uri.substring( 0, uri.length()-1 );
573  0 int slash = u.lastIndexOf( '/' );
574  0 if ( slash >= 0 && slash < u.length())
575  0 msg += "<b><a href=\"" + uri.substring(0, slash+1) + "\">..</a></b><br/>";
576    }
577   
578  0 for ( int i=0; i<files.length; ++i )
579    {
580  0 File curFile = new File( f, files[i] );
581  0 boolean dir = curFile.isDirectory();
582  0 if ( dir )
583    {
584  0 msg += "<b>";
585  0 files[i] += "/";
586    }
587   
588  0 msg += "<a href=\"" + encodeUri( uri + files[i] ) + "\">" +
589    files[i] + "</a>";
590   
591    // Show file size
592  0 if ( curFile.isFile())
593    {
594  0 long len = curFile.length();
595  0 msg += " &nbsp;<font size=2>(";
596  0 if ( len < 1024 )
597  0 msg += curFile.length() + " bytes";
598  0 else if ( len < 1024 * 1024 )
599  0 msg += curFile.length()/1024 + "." + (curFile.length()%1024/10%100) + " KB";
600    else
601  0 msg += curFile.length()/(1024*1024) + "." + curFile.length()%(1024*1024)/10%100 + " MB";
602   
603  0 msg += ")</font>";
604    }
605  0 msg += "<br/>";
606  0 if ( dir ) msg += "</b>";
607    }
608  0 return new Response( HTTP_OK, MIME_HTML, msg );
609    }
610    else
611    {
612  0 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT,
613    "FORBIDDEN: No directory listing." );
614    }
615    }
616   
617  0 try
618    {
619    // Get MIME type from file name extension, if possible
620  0 String mime = null;
621  0 int dot = f.getCanonicalPath().lastIndexOf( '.' );
622  0 if ( dot >= 0 )
623  0 mime = (String)theMimeTypes.get( f.getCanonicalPath().substring( dot + 1 ).toLowerCase());
624  0 if ( mime == null )
625  0 mime = MIME_DEFAULT_BINARY;
626   
627    // Support (simple) skipping:
628  0 long startFrom = 0;
629  0 String range = header.getProperty( "range" );
630  0 if ( range != null )
631    {
632  0 if ( range.startsWith( "bytes=" ))
633    {
634  0 range = range.substring( "bytes=".length());
635  0 int minus = range.indexOf( '-' );
636  0 if ( minus > 0 )
637  0 range = range.substring( 0, minus );
638  0 try {
639  0 startFrom = Long.parseLong( range );
640    }
641    catch ( NumberFormatException nfe ) {}
642    }
643    }
644   
645  0 FileInputStream fis = new FileInputStream( f );
646  0 fis.skip( startFrom );
647  0 Response r = new Response( HTTP_OK, mime, fis );
648  0 r.addHeader( "Content-length", "" + (f.length() - startFrom));
649  0 r.addHeader( "Content-range", "" + startFrom + "-" +
650    (f.length()-1) + "/" + f.length());
651  0 return r;
652    }
653    catch( IOException ioe )
654    {
655  0 return new Response( HTTP_FORBIDDEN, MIME_PLAINTEXT, "FORBIDDEN: Reading file failed." );
656    }
657    }
658   
659    /**
660    * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
661    */
662    private static Hashtable theMimeTypes = new Hashtable();
 
663  0 toggle static
664    {
665  0 StringTokenizer st = new StringTokenizer(
666    "htm text/html "+
667    "html text/html "+
668    "txt text/plain "+
669    "xml text/plain "+
670    "asc text/plain "+
671    "gif image/gif "+
672    "jpg image/jpeg "+
673    "jpeg image/jpeg "+
674    "png image/png "+
675    "mp3 audio/mpeg "+
676    "m3u audio/mpeg-url " +
677    "pdf application/pdf "+
678    "doc application/msword "+
679    "ogg application/x-ogg "+
680    "zip application/octet-stream "+
681    "exe application/octet-stream "+
682    "class application/octet-stream " );
683  0 while ( st.hasMoreTokens())
684  0 theMimeTypes.put( st.nextToken(), st.nextToken());
685    }
686   
687    /**
688    * GMT date formatter
689    */
690    private static java.text.SimpleDateFormat gmtFrmt;
 
691  0 toggle static
692    {
693  0 gmtFrmt = new java.text.SimpleDateFormat( "E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
694  0 gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
695    }
696   
697    /**
698    * The distribution licence
699    */
700    private static final String LICENCE =
701    "Copyright (C) 2001,2005-2010 by Jarno Elonen <elonen@iki.fi>\n"+
702    "\n"+
703    "Redistribution and use in source and binary forms, with or without\n"+
704    "modification, are permitted provided that the following conditions\n"+
705    "are met:\n"+
706    "\n"+
707    "Redistributions of source code must retain the above copyright notice,\n"+
708    "this list of conditions and the following disclaimer. Redistributions in\n"+
709    "binary form must reproduce the above copyright notice, this list of\n"+
710    "conditions and the following disclaimer in the documentation and/or other\n"+
711    "materials provided with the distribution. The name of the author may not\n"+
712    "be used to endorse or promote products derived from this software without\n"+
713    "specific prior written permission. \n"+
714    " \n"+
715    "THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n"+
716    "IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n"+
717    "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n"+
718    "IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n"+
719    "INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n"+
720    "NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n"+
721    "DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n"+
722    "THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n"+
723    "(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n"+
724    "OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.";
725    }