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     protected void finalize()
299     {
300         release();
301     }
302 
303     public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
304     
305     
306     /* ------------------------------------------------------------ */
307     /** Release any resources held by the resource.
308      */
309     public abstract void release();
310     
311 
312     /* ------------------------------------------------------------ */
313     /**
314      * Returns true if the respresened resource exists.
315      */
316     public abstract boolean exists();
317     
318 
319     /* ------------------------------------------------------------ */
320     /**
321      * Returns true if the respresenetd resource is a container/directory.
322      * If the resource is not a file, resources ending with "/" are
323      * considered directories.
324      */
325     public abstract boolean isDirectory();
326 
327     /* ------------------------------------------------------------ */
328     /**
329      * Returns the last modified time
330      */
331     public abstract long lastModified();
332 
333 
334     /* ------------------------------------------------------------ */
335     /**
336      * Return the length of the resource
337      */
338     public abstract long length();
339     
340 
341     /* ------------------------------------------------------------ */
342     /**
343      * Returns an URL representing the given resource
344      */
345     public abstract URL getURL();
346 
347     /* ------------------------------------------------------------ */
348     /**
349      * Returns an URI representing the given resource
350      */
351     public URI getURI()
352     {
353         try
354         {
355             return getURL().toURI();
356         }
357         catch(Exception e)
358         {
359             throw new RuntimeException(e);
360         }
361     }
362     
363 
364     /* ------------------------------------------------------------ */
365     /**
366      * Returns an File representing the given resource or NULL if this
367      * is not possible.
368      */
369     public abstract File getFile()
370         throws IOException;
371     
372 
373     /* ------------------------------------------------------------ */
374     /**
375      * Returns the name of the resource
376      */
377     public abstract String getName();
378     
379 
380     /* ------------------------------------------------------------ */
381     /**
382      * Returns an input stream to the resource
383      */
384     public abstract InputStream getInputStream()
385         throws java.io.IOException;
386 
387     /* ------------------------------------------------------------ */
388     /**
389      * Returns an output stream to the resource
390      */
391     public abstract OutputStream getOutputStream()
392         throws java.io.IOException, SecurityException;
393     
394     /* ------------------------------------------------------------ */
395     /**
396      * Deletes the given resource
397      */
398     public abstract boolean delete()
399         throws SecurityException;
400     
401     /* ------------------------------------------------------------ */
402     /**
403      * Rename the given resource
404      */
405     public abstract boolean renameTo( Resource dest)
406         throws SecurityException;
407     
408     /* ------------------------------------------------------------ */
409     /**
410      * Returns a list of resource names contained in the given resource
411      * The resource names are not URL encoded.
412      */
413     public abstract String[] list();
414 
415     /* ------------------------------------------------------------ */
416     /**
417      * Returns the resource contained inside the current resource with the
418      * given name.
419      * @param path The path segment to add, which should be encoded by the
420      * encode method. 
421      */
422     public abstract Resource addPath(String path)
423         throws IOException,MalformedURLException;
424     
425 
426     /* ------------------------------------------------------------ */
427     /** Encode according to this resource type.
428      * The default implementation calls URI.encodePath(uri)
429      * @param uri 
430      * @return String encoded for this resource type.
431      */
432     public String encode(String uri)
433     {
434         return URIUtil.encodePath(uri);
435     }
436         
437     /* ------------------------------------------------------------ */
438     public Object getAssociate()
439     {
440         return _associate;
441     }
442 
443     /* ------------------------------------------------------------ */
444     public void setAssociate(Object o)
445     {
446         _associate=o;
447     }
448     
449     /* ------------------------------------------------------------ */
450     /**
451      * @return The canonical Alias of this resource or null if none.
452      */
453     public URL getAlias()
454     {
455         return null;
456     }
457     
458     /* ------------------------------------------------------------ */
459     /** Get the resource list as a HTML directory listing.
460      * @param base The base URL
461      * @param parent True if the parent directory should be included
462      * @return String of HTML
463      */
464     public String getListHTML(String base,boolean parent)
465         throws IOException
466     {
467         base=URIUtil.canonicalPath(base);
468         if (base==null || !isDirectory())
469             return null;
470         
471         String[] ls = list();
472         if (ls==null)
473             return null;
474         Arrays.sort(ls);
475         
476         String decodedBase = URIUtil.decodePath(base);
477         String title = "Directory: "+deTag(decodedBase);
478 
479         StringBuilder buf=new StringBuilder(4096);
480         buf.append("<HTML><HEAD><TITLE>");
481         buf.append(title);
482         buf.append("</TITLE></HEAD><BODY>\n<H1>");
483         buf.append(title);
484         buf.append("</H1>\n<TABLE BORDER=0>\n");
485         
486         if (parent)
487         {
488             buf.append("<TR><TD><A HREF=\"");
489             buf.append(URIUtil.addPaths(base,"../"));
490             buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
491         }
492         
493         String defangedBase = defangURI(base);
494         
495         DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
496                                                        DateFormat.MEDIUM);
497         for (int i=0 ; i< ls.length ; i++)
498         {
499             Resource item = addPath(ls[i]);
500             
501             buf.append("\n<TR><TD><A HREF=\"");
502             String path=URIUtil.addPaths(defangedBase,URIUtil.encodePath(ls[i]));
503             
504             buf.append(path);
505             
506             if (item.isDirectory() && !path.endsWith("/"))
507                 buf.append(URIUtil.SLASH);
508             
509             // URIUtil.encodePath(buf,path);
510             buf.append("\">");
511             buf.append(deTag(ls[i]));
512             buf.append("&nbsp;");
513             buf.append("</TD><TD ALIGN=right>");
514             buf.append(item.length());
515             buf.append(" bytes&nbsp;</TD><TD>");
516             buf.append(dfmt.format(new Date(item.lastModified())));
517             buf.append("</TD></TR>");
518         }
519         buf.append("</TABLE>\n");
520 	buf.append("</BODY></HTML>\n");
521         
522         return buf.toString();
523     }
524     
525     /**
526      * Defang any characters that could break the URI string in an HREF.
527      * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
528      * 
529      * The above example would parse incorrectly on various browsers as the "<" or '"' characters
530      * would end the href attribute value string prematurely.
531      * 
532      * @param raw the raw text to defang.
533      * @return the defanged text.
534      */
535     private static String defangURI(String raw) 
536     {
537         StringBuffer buf = null;
538         
539         if (buf==null)
540         {
541             for (int i=0;i<raw.length();i++)
542             {
543                 char c=raw.charAt(i);
544                 switch(c)
545                 {
546                     case '\'':
547                     case '"':
548                     case '<':
549                     case '>':
550                         buf=new StringBuffer(raw.length()<<1);
551                         break;
552                 }
553             }
554             if (buf==null)
555                 return raw;
556         }
557         
558         for (int i=0;i<raw.length();i++)
559         {
560             char c=raw.charAt(i);       
561             switch(c)
562             {
563               case '"':
564                   buf.append("%22");
565                   continue;
566               case '\'':
567                   buf.append("%27");
568                   continue;
569               case '<':
570                   buf.append("%3C");
571                   continue;
572               case '>':
573                   buf.append("%3E");
574                   continue;
575               default:
576                   buf.append(c);
577                   continue;
578             }
579         }
580 
581         return buf.toString();
582     }
583     
584     private static String deTag(String raw) 
585     {
586         return StringUtil.replace( StringUtil.replace(raw,"<","&lt;"), ">", "&gt;");
587     }
588     
589     /* ------------------------------------------------------------ */
590     /** 
591      * @param out 
592      * @param start First byte to write
593      * @param count Bytes to write or -1 for all of them.
594      */
595     public void writeTo(OutputStream out,long start,long count)
596         throws IOException
597     {
598         InputStream in = getInputStream();
599         try
600         {
601             in.skip(start);
602             if (count<0)
603                 IO.copy(in,out);
604             else
605                 IO.copy(in,out,count);
606         }
607         finally
608         {
609             in.close();
610         }
611     }    
612     
613     /* ------------------------------------------------------------ */
614     public void copyTo(File destination)
615         throws IOException
616     {
617         if (destination.exists())
618             throw new IllegalArgumentException(destination+" exists");
619         writeTo(new FileOutputStream(destination),0,-1);
620     }
621    
622 }