View Javadoc

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