View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.Date;
36  
37  import org.eclipse.jetty.util.B64Code;
38  import org.eclipse.jetty.util.IO;
39  import org.eclipse.jetty.util.Loader;
40  import org.eclipse.jetty.util.StringUtil;
41  import org.eclipse.jetty.util.URIUtil;
42  import org.eclipse.jetty.util.UrlEncoded;
43  import org.eclipse.jetty.util.log.Log;
44  import org.eclipse.jetty.util.log.Logger;
45  
46  
47  /* ------------------------------------------------------------ */
48  /** 
49   * Abstract resource class.
50   * <p>
51   * This class provides a resource abstraction, where a resource may be
52   * a file, a URL or an entry in a jar file.
53   * </p>
54   */
55  public abstract class Resource implements ResourceFactory, Closeable
56  {
57      private static final Logger LOG = Log.getLogger(Resource.class);
58      public static boolean __defaultUseCaches = true;
59      volatile Object _associate;
60  
61      /* ------------------------------------------------------------ */
62      /**
63       * Change the default setting for url connection caches.
64       * Subsequent URLConnections will use this default.
65       * @param useCaches true to enable URL connection caches, false otherwise.
66       */
67      public static void setDefaultUseCaches (boolean useCaches)
68      {
69          __defaultUseCaches=useCaches;
70      }
71  
72      /* ------------------------------------------------------------ */
73      public static boolean getDefaultUseCaches ()
74      {
75          return __defaultUseCaches;
76      }
77      
78      /* ------------------------------------------------------------ */
79      /** Construct a resource from a uri.
80       * @param uri A URI.
81       * @return A Resource object.
82       * @throws MalformedURLException Problem accessing URI
83       */
84      public static Resource newResource(URI uri)
85          throws MalformedURLException
86      {
87          return newResource(uri.toURL());
88      }
89      
90      /* ------------------------------------------------------------ */
91      /** Construct a resource from a url.
92       * @param url A URL.
93       * @return A Resource object.
94       */
95      public static Resource newResource(URL url)
96      {
97          return newResource(url, __defaultUseCaches);
98      }
99      
100     /* ------------------------------------------------------------ */   
101     /**
102      * Construct a resource from a url.
103      * @param url the url for which to make the resource
104      * @param useCaches true enables URLConnection caching if applicable to the type of resource
105      * @return
106      */
107     static Resource newResource(URL url, boolean useCaches)
108     {
109         if (url==null)
110             return null;
111 
112         String url_string=url.toExternalForm();
113         if( url_string.startsWith( "file:"))
114         {
115             try
116             {
117                 return new PathResource(url);
118             }
119             catch(Exception e)
120             {
121                 LOG.warn(e.toString());
122                 LOG.debug(Log.EXCEPTION,e);
123                 return new BadResource(url,e.toString());
124             }
125         }
126         else if( url_string.startsWith( "jar:file:"))
127         {
128             return new JarFileResource(url, useCaches);
129         }
130         else if( url_string.startsWith( "jar:"))
131         {
132             return new JarResource(url, useCaches);
133         }
134 
135         return new URLResource(url,null,useCaches);
136     }
137 
138     
139     
140     /* ------------------------------------------------------------ */
141     /** Construct a resource from a string.
142      * @param resource A URL or filename.
143      * @throws MalformedURLException Problem accessing URI
144      * @return A Resource object.
145      */
146     public static Resource newResource(String resource)
147         throws MalformedURLException
148     {
149         return newResource(resource, __defaultUseCaches);
150     }
151     
152     /* ------------------------------------------------------------ */
153     /** Construct a resource from a string.
154      * @param resource A URL or filename.
155      * @param useCaches controls URLConnection caching
156      * @return A Resource object.
157      * @throws MalformedURLException Problem accessing URI
158      */
159     public static Resource newResource(String resource, boolean useCaches)       
160         throws MalformedURLException
161     {
162         URL url=null;
163         try
164         {
165             // Try to format as a URL?
166             url = new URL(resource);
167         }
168         catch(MalformedURLException e)
169         {
170             if(!resource.startsWith("ftp:") &&
171                !resource.startsWith("file:") &&
172                !resource.startsWith("jar:"))
173             {
174                 try
175                 {
176                     // It's a file.
177                     if (resource.startsWith("./"))
178                         resource=resource.substring(2);
179                     File file=new File(resource).getCanonicalFile();
180                     return new PathResource(file);
181                 }
182                 catch(IOException e2)
183                 {
184                     // TODO throw the IOException instead
185                     e.addSuppressed(e2);
186                     throw e;
187                 }
188             }
189             else
190             {
191                 LOG.warn("Bad Resource: "+resource);
192                 throw e;
193             }
194         }
195 
196         return newResource(url, useCaches);
197     }
198 
199     /* ------------------------------------------------------------ */
200     public static Resource newResource(File file)
201     {
202         return new PathResource(file.toPath());
203     }
204 
205     /* ------------------------------------------------------------ */
206     /** Construct a system resource from a string.
207      * The resource is tried as classloader resource before being
208      * treated as a normal resource.
209      * @param resource Resource as string representation 
210      * @return The new Resource
211      * @throws IOException Problem accessing resource.
212      */
213     public static Resource newSystemResource(String resource)
214         throws IOException
215     {
216         URL url=null;
217         // Try to format as a URL?
218         ClassLoader loader=Thread.currentThread().getContextClassLoader();
219         if (loader!=null)
220         {
221             try
222             {
223                 url = loader.getResource(resource);
224                 if (url == null && resource.startsWith("/"))
225                     url = loader.getResource(resource.substring(1));
226             }
227             catch (IllegalArgumentException e)
228             {
229                 LOG.ignore(e);
230                 // Catches scenario where a bad Windows path like "C:\dev" is
231                 // improperly escaped, which various downstream classloaders
232                 // tend to have a problem with
233                 url = null;
234             }
235         }
236         if (url==null)
237         {
238             loader=Resource.class.getClassLoader();
239             if (loader!=null)
240             {
241                 url=loader.getResource(resource);
242                 if (url==null && resource.startsWith("/"))
243                     url=loader.getResource(resource.substring(1));
244             }
245         }
246         
247         if (url==null)
248         {
249             url=ClassLoader.getSystemResource(resource);
250             if (url==null && resource.startsWith("/"))
251                 url=ClassLoader.getSystemResource(resource.substring(1));
252         }
253         
254         if (url==null)
255             return null;
256         
257         return newResource(url);
258     }
259 
260     /* ------------------------------------------------------------ */
261     /** Find a classpath resource.
262      * @param resource the relative name of the resource
263      * @return Resource or null
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)} 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);
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         close();
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      * @deprecated use {@link #close()}
313      */
314     public final void release()
315     {
316         close();
317     }
318 
319     /* ------------------------------------------------------------ */
320     /** Release any temporary resources held by the resource.
321      */
322     @Override
323     public abstract void close();
324 
325     /* ------------------------------------------------------------ */
326     /**
327      * @return true if the represented resource exists.
328      */
329     public abstract boolean exists();
330     
331 
332     /* ------------------------------------------------------------ */
333     /**
334      * @return true if the represented resource is a container/directory.
335      * if the resource is not a file, resources ending with "/" are
336      * considered directories.
337      */
338     public abstract boolean isDirectory();
339 
340     /* ------------------------------------------------------------ */
341     /**
342      * Time resource was last modified.
343      * 
344      * @return the last modified time as milliseconds since unix epoch
345      */
346     public abstract long lastModified();
347 
348 
349     /* ------------------------------------------------------------ */
350     /**
351      * Length of the resource.
352      * 
353      * @return the length of the resource
354      */
355     public abstract long length();
356     
357 
358     /* ------------------------------------------------------------ */
359     /**
360      * URL representing the resource.
361      * 
362      * @return an URL representing the given resource
363      * @deprecated use {{@link #getURI()}.toURL() instead.
364      */
365     @Deprecated
366     public abstract URL getURL();
367 
368     /* ------------------------------------------------------------ */
369     /**
370      * URI representing the resource.
371      * 
372      * @return an URI representing the given resource
373      */
374     public URI getURI()
375     {
376         try
377         {
378             return getURL().toURI();
379         }
380         catch(Exception e)
381         {
382             throw new RuntimeException(e);
383         }
384     }
385     
386 
387     /* ------------------------------------------------------------ */
388     /**
389      * File representing the given resource.
390      * 
391      * @return an File representing the given resource or NULL if this
392      * is not possible.
393      * @throws IOException if unable to get the resource due to permissions 
394      */
395     public abstract File getFile()
396         throws IOException;
397     
398 
399     /* ------------------------------------------------------------ */
400     /**
401      * The name of the resource.
402      * 
403      * @return the name of the resource
404      */
405     public abstract String getName();
406     
407 
408     /* ------------------------------------------------------------ */
409     /**
410      * Input stream to the resource
411      * 
412      * @return an input stream to the resource
413      * @throws IOException if unable to open the input stream
414      */
415     public abstract InputStream getInputStream()
416         throws IOException;
417     
418     /* ------------------------------------------------------------ */
419     /**
420      * Readable ByteChannel for the resource.
421      * 
422      * @return an readable bytechannel to the resource or null if one is not available.
423      * @throws IOException if unable to open the readable bytechannel for the resource.
424      */
425     public abstract ReadableByteChannel getReadableByteChannel()
426         throws IOException;
427 
428     /* ------------------------------------------------------------ */
429     /**
430      * Deletes the given resource
431      * @return true if resource was found and successfully deleted, false if resource didn't exist or was unable to
432      * be deleted.
433      * @throws SecurityException if unable to delete due to permissions 
434      */
435     public abstract boolean delete()
436         throws SecurityException;
437     
438     /* ------------------------------------------------------------ */
439     /**
440      * Rename the given resource
441      * @param dest the destination name for the resource
442      * @return true if the resource was renamed, false if the resource didn't exist or was unable to be renamed.
443      * @throws SecurityException if unable to rename due to permissions
444      */
445     public abstract boolean renameTo(Resource dest)
446         throws SecurityException;
447     
448     /* ------------------------------------------------------------ */
449     /**
450      * list of resource names contained in the given resource.
451      * 
452      * @return a list of resource names contained in the given resource.
453      * Note: The resource names are not URL encoded.
454      */
455     public abstract String[] list();
456 
457     /* ------------------------------------------------------------ */
458     /**
459      * Returns the resource contained inside the current resource with the
460      * given name.
461      * @param path The path segment to add, which is not encoded
462      * @return the Resource for the resolved path within this Resource.
463      * @throws IOException if unable to resolve the path
464      * @throws MalformedURLException if the resolution of the path fails because the input path parameter is malformed.
465      */
466     public abstract Resource addPath(String path)
467         throws IOException,MalformedURLException;
468 
469     /* ------------------------------------------------------------ */
470     /** Get a resource from within this resource.
471      * <p>
472      * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
473      * This method satisfied the {@link ResourceFactory} interface.
474      * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
475      */
476     @Override
477     public Resource getResource(String path)
478     {
479         try
480         {
481             return addPath(path);
482         }
483         catch(Exception e)
484         {
485             LOG.debug(e);
486             return null;
487         }
488     }
489 
490     /* ------------------------------------------------------------ */
491     /** 
492      * @param uri the uri to encode
493      * @return null (this is deprecated)
494      * @deprecated use {@link URIUtil} or {@link UrlEncoded} instead
495      */
496     @Deprecated
497     public String encode(String uri)
498     {
499         return null;
500     }
501         
502     /* ------------------------------------------------------------ */
503     // FIXME: this appears to not be used
504     @SuppressWarnings("javadoc")
505     public Object getAssociate()
506     {
507         return _associate;
508     }
509 
510     /* ------------------------------------------------------------ */
511     // FIXME: this appear to not be used
512     @SuppressWarnings("javadoc")
513     public void setAssociate(Object o)
514     {
515         _associate=o;
516     }
517 
518     /* ------------------------------------------------------------ */
519     /**
520      * @return true if this Resource is an alias to another real Resource
521      */
522     public boolean isAlias()
523     {
524         return getAlias()!=null;
525     }
526     
527     /* ------------------------------------------------------------ */
528     /**
529      * @return The canonical Alias of this resource or null if none.
530      */
531     public URI getAlias()
532     {
533         return null;
534     }
535     
536     /* ------------------------------------------------------------ */
537     /** Get the resource list as a HTML directory listing.
538      * @param base The base URL
539      * @param parent True if the parent directory should be included
540      * @return String of HTML
541      * @throws IOException if unable to get the list of resources as HTML
542      */
543     public String getListHTML(String base,boolean parent)
544         throws IOException
545     {
546         base=URIUtil.canonicalPath(base);
547         if (base==null || !isDirectory())
548             return null;
549         
550         String[] ls = list();
551         if (ls==null)
552             return null;
553         Arrays.sort(ls);
554         
555         String decodedBase = URIUtil.decodePath(base);
556         String title = "Directory: "+deTag(decodedBase);
557 
558         StringBuilder buf=new StringBuilder(4096);
559         buf.append("<HTML><HEAD>");
560         buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
561         buf.append(title);
562         buf.append("</TITLE></HEAD><BODY>\n<H1>");
563         buf.append(title);
564         buf.append("</H1>\n<TABLE BORDER=0>\n");
565         
566         if (parent)
567         {
568             buf.append("<TR><TD><A HREF=\"");
569             buf.append(URIUtil.addPaths(base,"../"));
570             buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
571         }
572         
573         String encodedBase = hrefEncodeURI(base);
574         
575         DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
576                                                        DateFormat.MEDIUM);
577         for (int i=0 ; i< ls.length ; i++)
578         {
579             Resource item = addPath(ls[i]);
580             
581             buf.append("\n<TR><TD><A HREF=\"");
582             String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
583             
584             buf.append(path);
585             
586             if (item.isDirectory() && !path.endsWith("/"))
587                 buf.append(URIUtil.SLASH);
588             
589             // URIUtil.encodePath(buf,path);
590             buf.append("\">");
591             buf.append(deTag(ls[i]));
592             buf.append("&nbsp;");
593             buf.append("</A></TD><TD ALIGN=right>");
594             buf.append(item.length());
595             buf.append(" bytes&nbsp;</TD><TD>");
596             buf.append(dfmt.format(new Date(item.lastModified())));
597             buf.append("</TD></TR>");
598         }
599         buf.append("</TABLE>\n");
600         buf.append("</BODY></HTML>\n");
601         
602         return buf.toString();
603     }
604     
605     /**
606      * Encode any characters that could break the URI string in an HREF.
607      * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
608      * 
609      * The above example would parse incorrectly on various browsers as the "<" or '"' characters
610      * would end the href attribute value string prematurely.
611      * 
612      * @param raw the raw text to encode.
613      * @return the defanged text.
614      */
615     private static String hrefEncodeURI(String raw) 
616     {
617         StringBuffer buf = null;
618 
619         loop:
620         for (int i=0;i<raw.length();i++)
621         {
622             char c=raw.charAt(i);
623             switch(c)
624             {
625                 case '\'':
626                 case '"':
627                 case '<':
628                 case '>':
629                     buf=new StringBuffer(raw.length()<<1);
630                     break loop;
631             }
632         }
633         if (buf==null)
634             return raw;
635 
636         for (int i=0;i<raw.length();i++)
637         {
638             char c=raw.charAt(i);       
639             switch(c)
640             {
641               case '"':
642                   buf.append("%22");
643                   continue;
644               case '\'':
645                   buf.append("%27");
646                   continue;
647               case '<':
648                   buf.append("%3C");
649                   continue;
650               case '>':
651                   buf.append("%3E");
652                   continue;
653               default:
654                   buf.append(c);
655                   continue;
656             }
657         }
658 
659         return buf.toString();
660     }
661     
662     private static String deTag(String raw) 
663     {
664         return StringUtil.sanitizeXmlString(raw);
665     }
666     
667     /* ------------------------------------------------------------ */
668     /** 
669      * @param out the output stream to write to 
670      * @param start First byte to write
671      * @param count Bytes to write or -1 for all of them.
672      * @throws IOException if unable to copy the Resource to the output
673      */
674     public void writeTo(OutputStream out,long start,long count)
675         throws IOException
676     {
677         try (InputStream in = getInputStream())
678         {
679             in.skip(start);
680             if (count<0)
681                 IO.copy(in,out);
682             else
683                 IO.copy(in,out,count);
684         }
685     }    
686     
687     /* ------------------------------------------------------------ */
688     /**
689      * Copy the Resource to the new destination file.
690      * <p>
691      * Will not replace existing destination file.
692      * 
693      * @param destination the destination file to create
694      * @throws IOException if unable to copy the resource
695      */
696     public void copyTo(File destination)
697         throws IOException
698     {
699         if (destination.exists())
700             throw new IllegalArgumentException(destination + " exists");
701         
702         try (OutputStream out = new FileOutputStream(destination))
703         {
704             writeTo(out,0,-1);
705         }
706     }
707 
708     /* ------------------------------------------------------------ */
709     /**
710      * Generate a weak ETag reference for this Resource.
711      * 
712      * @return the weak ETag reference for this resource.
713      */
714     public String getWeakETag()
715     {
716         return getWeakETag("");
717     }
718     
719     public String getWeakETag(String suffix)
720     {
721         try
722         {
723             StringBuilder b = new StringBuilder(32);
724             b.append("W/\"");
725             
726             String name=getName();
727             int length=name.length();
728             long lhash=0;
729             for (int i=0; i<length;i++)
730                 lhash=31*lhash+name.charAt(i);
731             
732             B64Code.encode(lastModified()^lhash,b);
733             B64Code.encode(length()^lhash,b);
734             b.append(suffix);
735             b.append('"');
736             return b.toString();
737         } 
738         catch (IOException e)
739         {
740             throw new RuntimeException(e);
741         }
742     }
743     
744     /* ------------------------------------------------------------ */
745     public Collection<Resource> getAllResources()
746     {
747         try
748         {
749             ArrayList<Resource> deep=new ArrayList<>();
750             {
751                 String[] list=list();
752                 if (list!=null)
753                 {
754                     for (String i:list)
755                     {
756                         Resource r=addPath(i);
757                         if (r.isDirectory())
758                             deep.addAll(r.getAllResources());
759                         else
760                             deep.add(r);
761                     }
762                 }
763             }
764             return deep;
765         }
766         catch(Exception e)
767         {
768             throw new IllegalStateException(e);
769         }
770     }
771     
772     /* ------------------------------------------------------------ */
773     /** Generate a properly encoded URL from a {@link File} instance.
774      * @param file Target file. 
775      * @return URL of the target file.
776      * @throws MalformedURLException if unable to convert File to URL
777      */
778     public static URL toURL(File file) throws MalformedURLException
779     {
780         return file.toURI().toURL();
781     }
782 }