View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util.resource;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.net.MalformedURLException;
27  import java.net.URI;
28  import java.net.URL;
29  import java.net.URLConnection;
30  import java.nio.channels.ReadableByteChannel;
31  import java.text.DateFormat;
32  import java.util.Arrays;
33  import java.util.Date;
34  
35  import org.eclipse.jetty.util.B64Code;
36  import org.eclipse.jetty.util.IO;
37  import org.eclipse.jetty.util.Loader;
38  import org.eclipse.jetty.util.StringUtil;
39  import org.eclipse.jetty.util.URIUtil;
40  import org.eclipse.jetty.util.log.Log;
41  import org.eclipse.jetty.util.log.Logger;
42  
43  
44  /* ------------------------------------------------------------ */
45  /** 
46   * Abstract resource class.
47   */
48  public abstract class Resource implements ResourceFactory
49  {
50      private static final Logger LOG = Log.getLogger(Resource.class);
51      public static boolean __defaultUseCaches = true;
52      volatile Object _associate;
53  
54      /* ------------------------------------------------------------ */
55      /**
56       * Change the default setting for url connection caches.
57       * Subsequent URLConnections will use this default.
58       * @param useCaches
59       */
60      public static void setDefaultUseCaches (boolean useCaches)
61      {
62          __defaultUseCaches=useCaches;
63      }
64  
65      /* ------------------------------------------------------------ */
66      public static boolean getDefaultUseCaches ()
67      {
68          return __defaultUseCaches;
69      }
70      
71      /* ------------------------------------------------------------ */
72      /** Construct a resource from a uri.
73       * @param uri A URI.
74       * @return A Resource object.
75       * @throws IOException Problem accessing URI
76       */
77      public static Resource newResource(URI uri)
78          throws IOException
79      {
80          return newResource(uri.toURL());
81      }
82      
83      /* ------------------------------------------------------------ */
84      /** Construct a resource from a url.
85       * @param url A URL.
86       * @return A Resource object.
87       * @throws IOException Problem accessing URL
88       */
89      public static Resource newResource(URL url)
90          throws IOException
91      {
92          return newResource(url, __defaultUseCaches);
93      }
94      
95      /* ------------------------------------------------------------ */   
96      /**
97       * Construct a resource from a url.
98       * @param url the url for which to make the resource
99       * @param useCaches true enables URLConnection caching if applicable to the type of resource
100      * @return
101      */
102     static Resource newResource(URL url, boolean useCaches)
103     {
104         if (url==null)
105             return null;
106 
107         String url_string=url.toExternalForm();
108         if( url_string.startsWith( "file:"))
109         {
110             try
111             {
112                 FileResource fileResource= new FileResource(url);
113                 return fileResource;
114             }
115             catch(Exception e)
116             {
117                 LOG.debug(Log.EXCEPTION,e);
118                 return new BadResource(url,e.toString());
119             }
120         }
121         else if( url_string.startsWith( "jar:file:"))
122         {
123             return new JarFileResource(url, useCaches);
124         }
125         else if( url_string.startsWith( "jar:"))
126         {
127             return new JarResource(url, useCaches);
128         }
129 
130         return new URLResource(url,null,useCaches);
131     }
132 
133     
134     
135     /* ------------------------------------------------------------ */
136     /** Construct a resource from a string.
137      * @param resource A URL or filename.
138      * @return A Resource object.
139      */
140     public static Resource newResource(String resource)
141         throws MalformedURLException, IOException
142     {
143         return newResource(resource, __defaultUseCaches);
144     }
145     
146     /* ------------------------------------------------------------ */
147     /** Construct a resource from a string.
148      * @param resource A URL or filename.
149      * @param useCaches controls URLConnection caching
150      * @return A Resource object.
151      */
152     public static Resource newResource (String resource, boolean useCaches)       
153     throws MalformedURLException, IOException
154     {
155         URL url=null;
156         try
157         {
158             // Try to format as a URL?
159             url = new URL(resource);
160         }
161         catch(MalformedURLException e)
162         {
163             if(!resource.startsWith("ftp:") &&
164                !resource.startsWith("file:") &&
165                !resource.startsWith("jar:"))
166             {
167                 try
168                 {
169                     // It's a file.
170                     if (resource.startsWith("./"))
171                         resource=resource.substring(2);
172                     
173                     File file=new File(resource).getCanonicalFile();
174                     url=Resource.toURL(file);            
175                     
176                     URLConnection connection=url.openConnection();
177                     connection.setUseCaches(useCaches);
178                     return new FileResource(url,connection,file);
179                 }
180                 catch(Exception e2)
181                 {
182                     LOG.debug(Log.EXCEPTION,e2);
183                     throw e;
184                 }
185             }
186             else
187             {
188                 LOG.warn("Bad Resource: "+resource);
189                 throw e;
190             }
191         }
192 
193         return newResource(url);
194     }
195 
196     /* ------------------------------------------------------------ */
197     public static Resource newResource (File file)
198     throws MalformedURLException, IOException
199     {
200         file = file.getCanonicalFile();
201         URL url = Resource.toURL(file);
202 
203         URLConnection connection = url.openConnection();
204         FileResource fileResource = new FileResource(url, connection, file);
205         return fileResource;
206     }
207 
208     /* ------------------------------------------------------------ */
209     /** Construct a system resource from a string.
210      * The resource is tried as classloader resource before being
211      * treated as a normal resource.
212      * @param resource Resource as string representation 
213      * @return The new Resource
214      * @throws IOException Problem accessing resource.
215      */
216     public static Resource newSystemResource(String resource)
217         throws IOException
218     {
219         URL url=null;
220         // Try to format as a URL?
221         ClassLoader
222             loader=Thread.currentThread().getContextClassLoader();
223         if (loader!=null)
224         {
225             try
226             {
227                 url = loader.getResource(resource);
228                 if (url == null && resource.startsWith("/"))
229                     url = loader.getResource(resource.substring(1));
230             }
231             catch (IllegalArgumentException e)
232             {
233                 // Catches scenario where a bad Windows path like "C:\dev" is
234                 // improperly escaped, which various downstream classloaders
235                 // tend to have a problem with
236                 url = null;
237             }
238         }
239         if (url==null)
240         {
241             loader=Resource.class.getClassLoader();
242             if (loader!=null)
243             {
244                 url=loader.getResource(resource);
245                 if (url==null && resource.startsWith("/"))
246                     url=loader.getResource(resource.substring(1));
247             }
248         }
249         
250         if (url==null)
251         {
252             url=ClassLoader.getSystemResource(resource);
253             if (url==null && resource.startsWith("/"))
254                 url=loader.getResource(resource.substring(1));
255         }
256         
257         if (url==null)
258             return null;
259         
260         return newResource(url);
261     }
262 
263     /* ------------------------------------------------------------ */
264     /** Find a classpath resource.
265      */
266     public static Resource newClassPathResource(String resource)
267     {
268         return newClassPathResource(resource,true,false);
269     }
270 
271     /* ------------------------------------------------------------ */
272     /** Find a classpath resource.
273      * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
274      * found, then the {@link Loader#getResource(Class, String, boolean)} method is used.
275      * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
276      * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
277      * @param name The relative name of the resource
278      * @param useCaches True if URL caches are to be used.
279      * @param checkParents True if forced searching of parent Classloaders is performed to work around 
280      * loaders with inverted priorities
281      * @return Resource or null
282      */
283     public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
284     {
285         URL url=Resource.class.getResource(name);
286         
287         if (url==null)
288             url=Loader.getResource(Resource.class,name,checkParents);
289         if (url==null)
290             return null;
291         return newResource(url,useCaches);
292     }
293     
294     /* ------------------------------------------------------------ */
295     public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
296     {
297         return r.isContainedIn(containingResource);
298     }
299 
300     /* ------------------------------------------------------------ */
301     @Override
302     protected void finalize()
303     {
304         release();
305     }
306     
307     /* ------------------------------------------------------------ */
308     public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
309     
310     
311     /* ------------------------------------------------------------ */
312     /** Release any temporary resources held by the resource.
313      */
314     public abstract void release();
315     
316 
317     /* ------------------------------------------------------------ */
318     /**
319      * Returns true if the respresened resource exists.
320      */
321     public abstract boolean exists();
322     
323 
324     /* ------------------------------------------------------------ */
325     /**
326      * Returns true if the respresenetd resource is a container/directory.
327      * If the resource is not a file, resources ending with "/" are
328      * considered directories.
329      */
330     public abstract boolean isDirectory();
331 
332     /* ------------------------------------------------------------ */
333     /**
334      * Returns the last modified time
335      */
336     public abstract long lastModified();
337 
338 
339     /* ------------------------------------------------------------ */
340     /**
341      * Return the length of the resource
342      */
343     public abstract long length();
344     
345 
346     /* ------------------------------------------------------------ */
347     /**
348      * Returns an URL representing the given resource
349      */
350     public abstract URL getURL();
351 
352     /* ------------------------------------------------------------ */
353     /**
354      * Returns an URI representing the given resource
355      */
356     public URI getURI()
357     {
358         try
359         {
360             return getURL().toURI();
361         }
362         catch(Exception e)
363         {
364             throw new RuntimeException(e);
365         }
366     }
367     
368 
369     /* ------------------------------------------------------------ */
370     /**
371      * Returns an File representing the given resource or NULL if this
372      * is not possible.
373      */
374     public abstract File getFile()
375         throws IOException;
376     
377 
378     /* ------------------------------------------------------------ */
379     /**
380      * Returns the name of the resource
381      */
382     public abstract String getName();
383     
384 
385     /* ------------------------------------------------------------ */
386     /**
387      * Returns an input stream to the resource
388      */
389     public abstract InputStream getInputStream()
390         throws java.io.IOException;
391     
392     /* ------------------------------------------------------------ */
393     /**
394      * Returns an readable bytechannel to the resource or null if one is not available.
395      */
396     public abstract ReadableByteChannel getReadableByteChannel()
397         throws java.io.IOException;
398 
399     /* ------------------------------------------------------------ */
400     /**
401      * Returns an output stream to the resource
402      */
403     public abstract OutputStream getOutputStream()
404         throws java.io.IOException, SecurityException;
405     
406     /* ------------------------------------------------------------ */
407     /**
408      * Deletes the given resource
409      */
410     public abstract boolean delete()
411         throws SecurityException;
412     
413     /* ------------------------------------------------------------ */
414     /**
415      * Rename the given resource
416      */
417     public abstract boolean renameTo( Resource dest)
418         throws SecurityException;
419     
420     /* ------------------------------------------------------------ */
421     /**
422      * Returns a list of resource names contained in the given resource
423      * The resource names are not URL encoded.
424      */
425     public abstract String[] list();
426 
427     /* ------------------------------------------------------------ */
428     /**
429      * Returns the resource contained inside the current resource with the
430      * given name.
431      * @param path The path segment to add, which should be encoded by the
432      * encode method. 
433      */
434     public abstract Resource addPath(String path)
435         throws IOException,MalformedURLException;
436 
437     /* ------------------------------------------------------------ */
438     /** Get a resource from withing this resource.
439      * <p>
440      * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
441      * This method satisfied the {@link ResourceFactory} interface.
442      * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
443      */
444     public Resource getResource(String path)
445     {
446         try
447         {
448             return addPath(path);
449         }
450         catch(Exception e)
451         {
452             LOG.debug(e);
453             return null;
454         }
455     }
456 
457     /* ------------------------------------------------------------ */
458     /** Encode according to this resource type.
459      * The default implementation calls URI.encodePath(uri)
460      * @param uri 
461      * @return String encoded for this resource type.
462      */
463     public String encode(String uri)
464     {
465         return URIUtil.encodePath(uri);
466     }
467         
468     /* ------------------------------------------------------------ */
469     public Object getAssociate()
470     {
471         return _associate;
472     }
473 
474     /* ------------------------------------------------------------ */
475     public void setAssociate(Object o)
476     {
477         _associate=o;
478     }
479     
480     /* ------------------------------------------------------------ */
481     /**
482      * @return The canonical Alias of this resource or null if none.
483      */
484     public URL getAlias()
485     {
486         return null;
487     }
488     
489     /* ------------------------------------------------------------ */
490     /** Get the resource list as a HTML directory listing.
491      * @param base The base URL
492      * @param parent True if the parent directory should be included
493      * @return String of HTML
494      */
495     public String getListHTML(String base,boolean parent)
496         throws IOException
497     {
498         base=URIUtil.canonicalPath(base);
499         if (base==null || !isDirectory())
500             return null;
501         
502         String[] ls = list();
503         if (ls==null)
504             return null;
505         Arrays.sort(ls);
506         
507         String decodedBase = URIUtil.decodePath(base);
508         String title = "Directory: "+deTag(decodedBase);
509 
510         StringBuilder buf=new StringBuilder(4096);
511         buf.append("<HTML><HEAD>");
512         buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
513         buf.append(title);
514         buf.append("</TITLE></HEAD><BODY>\n<H1>");
515         buf.append(title);
516         buf.append("</H1>\n<TABLE BORDER=0>\n");
517         
518         if (parent)
519         {
520             buf.append("<TR><TD><A HREF=\"");
521             buf.append(URIUtil.addPaths(base,"../"));
522             buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
523         }
524         
525         String encodedBase = hrefEncodeURI(base);
526         
527         DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
528                                                        DateFormat.MEDIUM);
529         for (int i=0 ; i< ls.length ; i++)
530         {
531             Resource item = addPath(ls[i]);
532             
533             buf.append("\n<TR><TD><A HREF=\"");
534             String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
535             
536             buf.append(path);
537             
538             if (item.isDirectory() && !path.endsWith("/"))
539                 buf.append(URIUtil.SLASH);
540             
541             // URIUtil.encodePath(buf,path);
542             buf.append("\">");
543             buf.append(deTag(ls[i]));
544             buf.append("&nbsp;");
545             buf.append("</A></TD><TD ALIGN=right>");
546             buf.append(item.length());
547             buf.append(" bytes&nbsp;</TD><TD>");
548             buf.append(dfmt.format(new Date(item.lastModified())));
549             buf.append("</TD></TR>");
550         }
551         buf.append("</TABLE>\n");
552 	buf.append("</BODY></HTML>\n");
553         
554         return buf.toString();
555     }
556     
557     /**
558      * Encode any characters that could break the URI string in an HREF.
559      * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
560      * 
561      * The above example would parse incorrectly on various browsers as the "<" or '"' characters
562      * would end the href attribute value string prematurely.
563      * 
564      * @param raw the raw text to encode.
565      * @return the defanged text.
566      */
567     private static String hrefEncodeURI(String raw) 
568     {
569         StringBuffer buf = null;
570 
571         loop:
572         for (int i=0;i<raw.length();i++)
573         {
574             char c=raw.charAt(i);
575             switch(c)
576             {
577                 case '\'':
578                 case '"':
579                 case '<':
580                 case '>':
581                     buf=new StringBuffer(raw.length()<<1);
582                     break loop;
583             }
584         }
585         if (buf==null)
586             return raw;
587 
588         for (int i=0;i<raw.length();i++)
589         {
590             char c=raw.charAt(i);       
591             switch(c)
592             {
593               case '"':
594                   buf.append("%22");
595                   continue;
596               case '\'':
597                   buf.append("%27");
598                   continue;
599               case '<':
600                   buf.append("%3C");
601                   continue;
602               case '>':
603                   buf.append("%3E");
604                   continue;
605               default:
606                   buf.append(c);
607                   continue;
608             }
609         }
610 
611         return buf.toString();
612     }
613     
614     private static String deTag(String raw) 
615     {
616         return StringUtil.replace( StringUtil.replace(raw,"<","&lt;"), ">", "&gt;");
617     }
618     
619     /* ------------------------------------------------------------ */
620     /** 
621      * @param out 
622      * @param start First byte to write
623      * @param count Bytes to write or -1 for all of them.
624      */
625     public void writeTo(OutputStream out,long start,long count)
626         throws IOException
627     {
628         InputStream in = getInputStream();
629         try
630         {
631             in.skip(start);
632             if (count<0)
633                 IO.copy(in,out);
634             else
635                 IO.copy(in,out,count);
636         }
637         finally
638         {
639             in.close();
640         }
641     }    
642     
643     /* ------------------------------------------------------------ */
644     public void copyTo(File destination)
645         throws IOException
646     {
647         if (destination.exists())
648             throw new IllegalArgumentException(destination+" exists");
649         writeTo(new FileOutputStream(destination),0,-1);
650     }
651 
652     /* ------------------------------------------------------------ */
653     public String getWeakETag()
654     {
655         try
656         {
657             StringBuilder b = new StringBuilder(32);
658             b.append("W/\"");
659             
660             String name=getName();
661             int length=name.length();
662             long lhash=0;
663             for (int i=0; i<length;i++)
664                 lhash=31*lhash+name.charAt(i);
665             
666             B64Code.encode(lastModified()^lhash,b);
667             B64Code.encode(length()^lhash,b);
668             b.append('"');
669             return b.toString();
670         } 
671         catch (IOException e)
672         {
673             throw new RuntimeException(e);
674         }
675     }
676     
677     /* ------------------------------------------------------------ */
678     /** Generate a properly encoded URL from a {@link File} instance.
679      * @param file Target file. 
680      * @return URL of the target file.
681      * @throws MalformedURLException 
682      */
683     public static URL toURL(File file) throws MalformedURLException
684     {
685         return file.toURI().toURL();
686     }
687 }