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.Closeable;
22  import java.io.File;
23  import java.io.FileOutputStream;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.OutputStream;
27  import java.net.MalformedURLException;
28  import java.net.URI;
29  import java.net.URL;
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, Closeable
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.warn(e.toString());
118                 LOG.debug(Log.EXCEPTION,e);
119                 return new BadResource(url,e.toString());
120             }
121         }
122         else if( url_string.startsWith( "jar:file:"))
123         {
124             return new JarFileResource(url, useCaches);
125         }
126         else if( url_string.startsWith( "jar:"))
127         {
128             return new JarResource(url, useCaches);
129         }
130 
131         return new URLResource(url,null,useCaches);
132     }
133 
134     
135     
136     /* ------------------------------------------------------------ */
137     /** Construct a resource from a string.
138      * @param resource A URL or filename.
139      * @return A Resource object.
140      */
141     public static Resource newResource(String resource)
142         throws MalformedURLException, IOException
143     {
144         return newResource(resource, __defaultUseCaches);
145     }
146     
147     /* ------------------------------------------------------------ */
148     /** Construct a resource from a string.
149      * @param resource A URL or filename.
150      * @param useCaches controls URLConnection caching
151      * @return A Resource object.
152      */
153     public static Resource newResource(String resource, boolean useCaches)       
154     throws MalformedURLException, IOException
155     {
156         URL url=null;
157         try
158         {
159             // Try to format as a URL?
160             url = new URL(resource);
161         }
162         catch(MalformedURLException e)
163         {
164             if(!resource.startsWith("ftp:") &&
165                !resource.startsWith("file:") &&
166                !resource.startsWith("jar:"))
167             {
168                 try
169                 {
170                     // It's a file.
171                     if (resource.startsWith("./"))
172                         resource=resource.substring(2);
173                     
174                     File file=new File(resource).getCanonicalFile();
175                     return new FileResource(file);
176                 }
177                 catch(Exception e2)
178                 {
179                     LOG.debug(Log.EXCEPTION,e2);
180                     throw e;
181                 }
182             }
183             else
184             {
185                 LOG.warn("Bad Resource: "+resource);
186                 throw e;
187             }
188         }
189 
190         return newResource(url);
191     }
192 
193     /* ------------------------------------------------------------ */
194     public static Resource newResource(File file)
195     {
196         return new FileResource(file);
197     }
198 
199     /* ------------------------------------------------------------ */
200     /** Construct a system resource from a string.
201      * The resource is tried as classloader resource before being
202      * treated as a normal resource.
203      * @param resource Resource as string representation 
204      * @return The new Resource
205      * @throws IOException Problem accessing resource.
206      */
207     public static Resource newSystemResource(String resource)
208         throws IOException
209     {
210         URL url=null;
211         // Try to format as a URL?
212         ClassLoader loader=Thread.currentThread().getContextClassLoader();
213         if (loader!=null)
214         {
215             try
216             {
217                 url = loader.getResource(resource);
218                 if (url == null && resource.startsWith("/"))
219                     url = loader.getResource(resource.substring(1));
220             }
221             catch (IllegalArgumentException e)
222             {
223                 // Catches scenario where a bad Windows path like "C:\dev" is
224                 // improperly escaped, which various downstream classloaders
225                 // tend to have a problem with
226                 url = null;
227             }
228         }
229         if (url==null)
230         {
231             loader=Resource.class.getClassLoader();
232             if (loader!=null)
233             {
234                 url=loader.getResource(resource);
235                 if (url==null && resource.startsWith("/"))
236                     url=loader.getResource(resource.substring(1));
237             }
238         }
239         
240         if (url==null)
241         {
242             url=ClassLoader.getSystemResource(resource);
243             if (url==null && resource.startsWith("/"))
244                 url=ClassLoader.getSystemResource(resource.substring(1));
245         }
246         
247         if (url==null)
248             return null;
249         
250         return newResource(url);
251     }
252 
253     /* ------------------------------------------------------------ */
254     /** Find a classpath resource.
255      */
256     public static Resource newClassPathResource(String resource)
257     {
258         return newClassPathResource(resource,true,false);
259     }
260 
261     /* ------------------------------------------------------------ */
262     /** Find a classpath resource.
263      * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
264      * found, then the {@link Loader#getResource(Class, String, boolean)} method is used.
265      * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
266      * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
267      * @param name The relative name of the resource
268      * @param useCaches True if URL caches are to be used.
269      * @param checkParents True if forced searching of parent Classloaders is performed to work around 
270      * loaders with inverted priorities
271      * @return Resource or null
272      */
273     public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
274     {
275         URL url=Resource.class.getResource(name);
276         
277         if (url==null)
278             url=Loader.getResource(Resource.class,name,checkParents);
279         if (url==null)
280             return null;
281         return newResource(url,useCaches);
282     }
283     
284     /* ------------------------------------------------------------ */
285     public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
286     {
287         return r.isContainedIn(containingResource);
288     }
289 
290     /* ------------------------------------------------------------ */
291     @Override
292     protected void finalize()
293     {
294         close();
295     }
296     
297     /* ------------------------------------------------------------ */
298     public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
299     
300     
301     /* ------------------------------------------------------------ */
302     /** Release any temporary resources held by the resource.
303      * @deprecated use {@link #close()}
304      */
305     public final void release()
306     {
307         close();
308     }
309 
310     /* ------------------------------------------------------------ */
311     /** Release any temporary resources held by the resource.
312      */
313     @Override
314     public abstract void close();
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 readable bytechannel to the resource or null if one is not available.
394      */
395     public abstract ReadableByteChannel getReadableByteChannel()
396         throws java.io.IOException;
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 is not encoded
424      */
425     public abstract Resource addPath(String path)
426         throws IOException,MalformedURLException;
427 
428     /* ------------------------------------------------------------ */
429     /** Get a resource from within this resource.
430      * <p>
431      * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
432      * This method satisfied the {@link ResourceFactory} interface.
433      * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
434      */
435     @Override
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     /** 
451      * @deprecated
452      */
453     public String encode(String uri)
454     {
455         return null;
456     }
457         
458     /* ------------------------------------------------------------ */
459     public Object getAssociate()
460     {
461         return _associate;
462     }
463 
464     /* ------------------------------------------------------------ */
465     public void setAssociate(Object o)
466     {
467         _associate=o;
468     }
469     
470     /* ------------------------------------------------------------ */
471     /**
472      * @return The canonical Alias of this resource or null if none.
473      */
474     public URL getAlias()
475     {
476         return null;
477     }
478     
479     /* ------------------------------------------------------------ */
480     /** Get the resource list as a HTML directory listing.
481      * @param base The base URL
482      * @param parent True if the parent directory should be included
483      * @return String of HTML
484      */
485     public String getListHTML(String base,boolean parent)
486         throws IOException
487     {
488         base=URIUtil.canonicalPath(base);
489         if (base==null || !isDirectory())
490             return null;
491         
492         String[] ls = list();
493         if (ls==null)
494             return null;
495         Arrays.sort(ls);
496         
497         String decodedBase = URIUtil.decodePath(base);
498         String title = "Directory: "+deTag(decodedBase);
499 
500         StringBuilder buf=new StringBuilder(4096);
501         buf.append("<HTML><HEAD>");
502         buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
503         buf.append(title);
504         buf.append("</TITLE></HEAD><BODY>\n<H1>");
505         buf.append(title);
506         buf.append("</H1>\n<TABLE BORDER=0>\n");
507         
508         if (parent)
509         {
510             buf.append("<TR><TD><A HREF=\"");
511             buf.append(URIUtil.addPaths(base,"../"));
512             buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
513         }
514         
515         String encodedBase = hrefEncodeURI(base);
516         
517         DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
518                                                        DateFormat.MEDIUM);
519         for (int i=0 ; i< ls.length ; i++)
520         {
521             Resource item = addPath(ls[i]);
522             
523             buf.append("\n<TR><TD><A HREF=\"");
524             String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
525             
526             buf.append(path);
527             
528             if (item.isDirectory() && !path.endsWith("/"))
529                 buf.append(URIUtil.SLASH);
530             
531             // URIUtil.encodePath(buf,path);
532             buf.append("\">");
533             buf.append(deTag(ls[i]));
534             buf.append("&nbsp;");
535             buf.append("</A></TD><TD ALIGN=right>");
536             buf.append(item.length());
537             buf.append(" bytes&nbsp;</TD><TD>");
538             buf.append(dfmt.format(new Date(item.lastModified())));
539             buf.append("</TD></TR>");
540         }
541         buf.append("</TABLE>\n");
542 	buf.append("</BODY></HTML>\n");
543         
544         return buf.toString();
545     }
546     
547     /**
548      * Encode any characters that could break the URI string in an HREF.
549      * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
550      * 
551      * The above example would parse incorrectly on various browsers as the "<" or '"' characters
552      * would end the href attribute value string prematurely.
553      * 
554      * @param raw the raw text to encode.
555      * @return the defanged text.
556      */
557     private static String hrefEncodeURI(String raw) 
558     {
559         StringBuffer buf = null;
560 
561         loop:
562         for (int i=0;i<raw.length();i++)
563         {
564             char c=raw.charAt(i);
565             switch(c)
566             {
567                 case '\'':
568                 case '"':
569                 case '<':
570                 case '>':
571                     buf=new StringBuffer(raw.length()<<1);
572                     break loop;
573             }
574         }
575         if (buf==null)
576             return raw;
577 
578         for (int i=0;i<raw.length();i++)
579         {
580             char c=raw.charAt(i);       
581             switch(c)
582             {
583               case '"':
584                   buf.append("%22");
585                   continue;
586               case '\'':
587                   buf.append("%27");
588                   continue;
589               case '<':
590                   buf.append("%3C");
591                   continue;
592               case '>':
593                   buf.append("%3E");
594                   continue;
595               default:
596                   buf.append(c);
597                   continue;
598             }
599         }
600 
601         return buf.toString();
602     }
603     
604     private static String deTag(String raw) 
605     {
606         return StringUtil.replace( StringUtil.replace(raw,"<","&lt;"), ">", "&gt;");
607     }
608     
609     /* ------------------------------------------------------------ */
610     /** 
611      * @param out 
612      * @param start First byte to write
613      * @param count Bytes to write or -1 for all of them.
614      */
615     public void writeTo(OutputStream out,long start,long count)
616         throws IOException
617     {
618         try (InputStream in = getInputStream())
619         {
620             in.skip(start);
621             if (count<0)
622                 IO.copy(in,out);
623             else
624                 IO.copy(in,out,count);
625         }
626     }    
627     
628     /* ------------------------------------------------------------ */
629     public void copyTo(File destination)
630         throws IOException
631     {
632         if (destination.exists())
633             throw new IllegalArgumentException(destination+" exists");
634         try (OutputStream out = new FileOutputStream(destination))
635         {
636             writeTo(out,0,-1);
637         }
638     }
639 
640     /* ------------------------------------------------------------ */
641     public String getWeakETag()
642     {
643         try
644         {
645             StringBuilder b = new StringBuilder(32);
646             b.append("W/\"");
647             
648             String name=getName();
649             int length=name.length();
650             long lhash=0;
651             for (int i=0; i<length;i++)
652                 lhash=31*lhash+name.charAt(i);
653             
654             B64Code.encode(lastModified()^lhash,b);
655             B64Code.encode(length()^lhash,b);
656             b.append('"');
657             return b.toString();
658         } 
659         catch (IOException e)
660         {
661             throw new RuntimeException(e);
662         }
663     }
664     
665     /* ------------------------------------------------------------ */
666     /** Generate a properly encoded URL from a {@link File} instance.
667      * @param file Target file. 
668      * @return URL of the target file.
669      * @throws MalformedURLException 
670      */
671     public static URL toURL(File file) throws MalformedURLException
672     {
673         return file.toURI().toURL();
674     }
675 }