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.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.log.Log;
43  import org.eclipse.jetty.util.log.Logger;
44  
45  
46  /* ------------------------------------------------------------ */
47  /** 
48   * Abstract resource class.
49   */
50  public abstract class Resource implements ResourceFactory, Closeable
51  {
52      private static final Logger LOG = Log.getLogger(Resource.class);
53      public static boolean __defaultUseCaches = true;
54      volatile Object _associate;
55  
56      /* ------------------------------------------------------------ */
57      /**
58       * Change the default setting for url connection caches.
59       * Subsequent URLConnections will use this default.
60       * @param useCaches
61       */
62      public static void setDefaultUseCaches (boolean useCaches)
63      {
64          __defaultUseCaches=useCaches;
65      }
66  
67      /* ------------------------------------------------------------ */
68      public static boolean getDefaultUseCaches ()
69      {
70          return __defaultUseCaches;
71      }
72      
73      /* ------------------------------------------------------------ */
74      /** Construct a resource from a uri.
75       * @param uri A URI.
76       * @return A Resource object.
77       * @throws IOException Problem accessing URI
78       */
79      public static Resource newResource(URI uri)
80          throws IOException
81      {
82          return newResource(uri.toURL());
83      }
84      
85      /* ------------------------------------------------------------ */
86      /** Construct a resource from a url.
87       * @param url A URL.
88       * @return A Resource object.
89       * @throws IOException Problem accessing URL
90       */
91      public static Resource newResource(URL url)
92          throws IOException
93      {
94          return newResource(url, __defaultUseCaches);
95      }
96      
97      /* ------------------------------------------------------------ */   
98      /**
99       * Construct a resource from a url.
100      * @param url the url for which to make the resource
101      * @param useCaches true enables URLConnection caching if applicable to the type of resource
102      * @return
103      */
104     static Resource newResource(URL url, boolean useCaches)
105     {
106         if (url==null)
107             return null;
108 
109         String url_string=url.toExternalForm();
110         if( url_string.startsWith( "file:"))
111         {
112             try
113             {
114                 FileResource fileResource= new FileResource(url);
115                 return fileResource;
116             }
117             catch(Exception e)
118             {
119                 LOG.warn(e.toString());
120                 LOG.debug(Log.EXCEPTION,e);
121                 return new BadResource(url,e.toString());
122             }
123         }
124         else if( url_string.startsWith( "jar:file:"))
125         {
126             return new JarFileResource(url, useCaches);
127         }
128         else if( url_string.startsWith( "jar:"))
129         {
130             return new JarResource(url, useCaches);
131         }
132 
133         return new URLResource(url,null,useCaches);
134     }
135 
136     
137     
138     /* ------------------------------------------------------------ */
139     /** Construct a resource from a string.
140      * @param resource A URL or filename.
141      * @return A Resource object.
142      */
143     public static Resource newResource(String resource)
144         throws MalformedURLException, IOException
145     {
146         return newResource(resource, __defaultUseCaches);
147     }
148     
149     /* ------------------------------------------------------------ */
150     /** Construct a resource from a string.
151      * @param resource A URL or filename.
152      * @param useCaches controls URLConnection caching
153      * @return A Resource object.
154      */
155     public static Resource newResource(String resource, boolean useCaches)       
156     throws MalformedURLException, IOException
157     {
158         URL url=null;
159         try
160         {
161             // Try to format as a URL?
162             url = new URL(resource);
163         }
164         catch(MalformedURLException e)
165         {
166             if(!resource.startsWith("ftp:") &&
167                !resource.startsWith("file:") &&
168                !resource.startsWith("jar:"))
169             {
170                 try
171                 {
172                     // It's a file.
173                     if (resource.startsWith("./"))
174                         resource=resource.substring(2);
175                     
176                     File file=new File(resource).getCanonicalFile();
177                     return new FileResource(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     {
198         return new FileResource(file);
199     }
200 
201     /* ------------------------------------------------------------ */
202     /** Construct a system resource from a string.
203      * The resource is tried as classloader resource before being
204      * treated as a normal resource.
205      * @param resource Resource as string representation 
206      * @return The new Resource
207      * @throws IOException Problem accessing resource.
208      */
209     public static Resource newSystemResource(String resource)
210         throws IOException
211     {
212         URL url=null;
213         // Try to format as a URL?
214         ClassLoader loader=Thread.currentThread().getContextClassLoader();
215         if (loader!=null)
216         {
217             try
218             {
219                 url = loader.getResource(resource);
220                 if (url == null && resource.startsWith("/"))
221                     url = loader.getResource(resource.substring(1));
222             }
223             catch (IllegalArgumentException e)
224             {
225                 // Catches scenario where a bad Windows path like "C:\dev" is
226                 // improperly escaped, which various downstream classloaders
227                 // tend to have a problem with
228                 url = null;
229             }
230         }
231         if (url==null)
232         {
233             loader=Resource.class.getClassLoader();
234             if (loader!=null)
235             {
236                 url=loader.getResource(resource);
237                 if (url==null && resource.startsWith("/"))
238                     url=loader.getResource(resource.substring(1));
239             }
240         }
241         
242         if (url==null)
243         {
244             url=ClassLoader.getSystemResource(resource);
245             if (url==null && resource.startsWith("/"))
246                 url=ClassLoader.getSystemResource(resource.substring(1));
247         }
248         
249         if (url==null)
250             return null;
251         
252         return newResource(url);
253     }
254 
255     /* ------------------------------------------------------------ */
256     /** Find a classpath resource.
257      */
258     public static Resource newClassPathResource(String resource)
259     {
260         return newClassPathResource(resource,true,false);
261     }
262 
263     /* ------------------------------------------------------------ */
264     /** Find a classpath resource.
265      * The {@link java.lang.Class#getResource(String)} method is used to lookup the resource. If it is not
266      * found, then the {@link Loader#getResource(Class, String)} method is used.
267      * If it is still not found, then {@link ClassLoader#getSystemResource(String)} is used.
268      * Unlike {@link ClassLoader#getSystemResource(String)} this method does not check for normal resources.
269      * @param name The relative name of the resource
270      * @param useCaches True if URL caches are to be used.
271      * @param checkParents True if forced searching of parent Classloaders is performed to work around 
272      * loaders with inverted priorities
273      * @return Resource or null
274      */
275     public static Resource newClassPathResource(String name,boolean useCaches,boolean checkParents)
276     {
277         URL url=Resource.class.getResource(name);
278         
279         if (url==null)
280             url=Loader.getResource(Resource.class,name);
281         if (url==null)
282             return null;
283         return newResource(url,useCaches);
284     }
285     
286     /* ------------------------------------------------------------ */
287     public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
288     {
289         return r.isContainedIn(containingResource);
290     }
291 
292     /* ------------------------------------------------------------ */
293     @Override
294     protected void finalize()
295     {
296         close();
297     }
298     
299     /* ------------------------------------------------------------ */
300     public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
301     
302     
303     /* ------------------------------------------------------------ */
304     /** Release any temporary resources held by the resource.
305      * @deprecated use {@link #close()}
306      */
307     public final void release()
308     {
309         close();
310     }
311 
312     /* ------------------------------------------------------------ */
313     /** Release any temporary resources held by the resource.
314      */
315     @Override
316     public abstract void close();
317 
318     /* ------------------------------------------------------------ */
319     /**
320      * Returns true if the respresened resource exists.
321      */
322     public abstract boolean exists();
323     
324 
325     /* ------------------------------------------------------------ */
326     /**
327      * Returns true if the respresenetd resource is a container/directory.
328      * If the resource is not a file, resources ending with "/" are
329      * considered directories.
330      */
331     public abstract boolean isDirectory();
332 
333     /* ------------------------------------------------------------ */
334     /**
335      * Returns the last modified time
336      */
337     public abstract long lastModified();
338 
339 
340     /* ------------------------------------------------------------ */
341     /**
342      * Return the length of the resource
343      */
344     public abstract long length();
345     
346 
347     /* ------------------------------------------------------------ */
348     /**
349      * Returns an URL representing the given resource
350      */
351     public abstract URL getURL();
352 
353     /* ------------------------------------------------------------ */
354     /**
355      * Returns an URI representing the given resource
356      */
357     public URI getURI()
358     {
359         try
360         {
361             return getURL().toURI();
362         }
363         catch(Exception e)
364         {
365             throw new RuntimeException(e);
366         }
367     }
368     
369 
370     /* ------------------------------------------------------------ */
371     /**
372      * Returns an File representing the given resource or NULL if this
373      * is not possible.
374      */
375     public abstract File getFile()
376         throws IOException;
377     
378 
379     /* ------------------------------------------------------------ */
380     /**
381      * Returns the name of the resource
382      */
383     public abstract String getName();
384     
385 
386     /* ------------------------------------------------------------ */
387     /**
388      * Returns an input stream to the resource
389      */
390     public abstract InputStream getInputStream()
391         throws java.io.IOException;
392     
393     /* ------------------------------------------------------------ */
394     /**
395      * Returns an readable bytechannel to the resource or null if one is not available.
396      */
397     public abstract ReadableByteChannel getReadableByteChannel()
398         throws java.io.IOException;
399 
400     /* ------------------------------------------------------------ */
401     /**
402      * Deletes the given resource
403      */
404     public abstract boolean delete()
405         throws SecurityException;
406     
407     /* ------------------------------------------------------------ */
408     /**
409      * Rename the given resource
410      */
411     public abstract boolean renameTo( Resource dest)
412         throws SecurityException;
413     
414     /* ------------------------------------------------------------ */
415     /**
416      * Returns a list of resource names contained in the given resource
417      * The resource names are not URL encoded.
418      */
419     public abstract String[] list();
420 
421     /* ------------------------------------------------------------ */
422     /**
423      * Returns the resource contained inside the current resource with the
424      * given name.
425      * @param path The path segment to add, which is not encoded
426      */
427     public abstract Resource addPath(String path)
428         throws IOException,MalformedURLException;
429 
430     /* ------------------------------------------------------------ */
431     /** Get a resource from within this resource.
432      * <p>
433      * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
434      * This method satisfied the {@link ResourceFactory} interface.
435      * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
436      */
437     @Override
438     public Resource getResource(String path)
439     {
440         try
441         {
442             return addPath(path);
443         }
444         catch(Exception e)
445         {
446             LOG.debug(e);
447             return null;
448         }
449     }
450 
451     /* ------------------------------------------------------------ */
452     /** 
453      * @deprecated
454      */
455     public String encode(String uri)
456     {
457         return null;
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 URI 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         try (InputStream in = getInputStream())
621         {
622             in.skip(start);
623             if (count<0)
624                 IO.copy(in,out);
625             else
626                 IO.copy(in,out,count);
627         }
628     }    
629     
630     /* ------------------------------------------------------------ */
631     public void copyTo(File destination)
632         throws IOException
633     {
634         if (destination.exists())
635             throw new IllegalArgumentException(destination+" exists");
636         try (OutputStream out = new FileOutputStream(destination))
637         {
638             writeTo(out,0,-1);
639         }
640     }
641 
642     /* ------------------------------------------------------------ */
643     public String getWeakETag()
644     {
645         try
646         {
647             StringBuilder b = new StringBuilder(32);
648             b.append("W/\"");
649             
650             String name=getName();
651             int length=name.length();
652             long lhash=0;
653             for (int i=0; i<length;i++)
654                 lhash=31*lhash+name.charAt(i);
655             
656             B64Code.encode(lastModified()^lhash,b);
657             B64Code.encode(length()^lhash,b);
658             b.append('"');
659             return b.toString();
660         } 
661         catch (IOException e)
662         {
663             throw new RuntimeException(e);
664         }
665     }
666     
667     /* ------------------------------------------------------------ */
668     public Collection<Resource> getAllResources()
669     {
670         try
671         {
672             ArrayList<Resource> deep=new ArrayList<>();
673             {
674                 String[] list=list();
675                 if (list!=null)
676                 {
677                     for (String i:list)
678                     {
679                         Resource r=addPath(i);
680                         if (r.isDirectory())
681                             deep.addAll(r.getAllResources());
682                         else
683                             deep.add(r);
684                     }
685                 }
686             }
687             return deep;
688         }
689         catch(Exception e)
690         {
691             throw new IllegalStateException(e);
692         }
693     }
694     
695     /* ------------------------------------------------------------ */
696     /** Generate a properly encoded URL from a {@link File} instance.
697      * @param file Target file. 
698      * @return URL of the target file.
699      * @throws MalformedURLException 
700      */
701     public static URL toURL(File file) throws MalformedURLException
702     {
703         return file.toURI().toURL();
704     }
705 }