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