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.net.MalformedURLException;
21  import java.net.URI;
22  import java.net.URL;
23  import java.net.URLConnection;
24  import java.text.DateFormat;
25  import java.util.Arrays;
26  import java.util.Date;
27  
28  import org.eclipse.jetty.util.IO;
29  import org.eclipse.jetty.util.Loader;
30  import org.eclipse.jetty.util.StringUtil;
31  import org.eclipse.jetty.util.URIUtil;
32  import org.eclipse.jetty.util.log.Log;
33  
34  
35  /* ------------------------------------------------------------ */
36  /** 
37   * Abstract resource class.
38   */
39  public abstract class Resource implements ResourceFactory
40  {
41      public static boolean __defaultUseCaches = true;
42      volatile Object _associate;
43      
44      /**
45       * Change the default setting for url connection caches.
46       * Subsequent URLConnections will use this default.
47       * @param useCaches
48       */
49      public static void setDefaultUseCaches (boolean useCaches)
50      {
51          __defaultUseCaches=useCaches;
52      }
53  
54      /* ------------------------------------------------------------ */
55      public static boolean getDefaultUseCaches ()
56      {
57          return __defaultUseCaches;
58      }
59      
60      /* ------------------------------------------------------------ */
61      /** Construct a resource from a uri.
62       * @param uri A URI.
63       * @return A Resource object.
64       * @throws IOException Problem accessing URI
65       */
66      public static Resource newResource(URI uri)
67          throws IOException
68      {
69          return newResource(uri.toURL());
70      }
71      
72      /* ------------------------------------------------------------ */
73      /** Construct a resource from a url.
74       * @param url A URL.
75       * @return A Resource object.
76       * @throws IOException Problem accessing URL
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      * @param resource Resource as string representation 
205      * @return The new Resource
206      * @throws IOException Problem accessing resource.
207      */
208     public static Resource newSystemResource(String resource)
209         throws IOException
210     {
211         URL url=null;
212         // Try to format as a URL?
213         ClassLoader
214             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=loader.getResource(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, boolean)} 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         {
281             try
282             {
283                 url=Loader.getResource(Resource.class,name,checkParents);
284             }
285             catch(ClassNotFoundException e)
286             {
287                 url=ClassLoader.getSystemResource(name);
288             }
289         }
290         if (url==null)
291             return null;
292         return newResource(url,useCaches);
293     }
294     
295     /* ------------------------------------------------------------ */
296     public static boolean isContainedIn (Resource r, Resource containingResource) throws MalformedURLException
297     {
298         return r.isContainedIn(containingResource);
299     }
300 
301     /* ------------------------------------------------------------ */
302     @Override
303     protected void finalize()
304     {
305         release();
306     }
307     
308     /* ------------------------------------------------------------ */
309     public abstract boolean isContainedIn (Resource r) throws MalformedURLException;
310     
311     
312     /* ------------------------------------------------------------ */
313     /** Release any temporary resources held by the resource.
314      */
315     public abstract void release();
316     
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 output stream to the resource
396      */
397     public abstract OutputStream getOutputStream()
398         throws java.io.IOException, SecurityException;
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 should be encoded by the
426      * encode method. 
427      */
428     public abstract Resource addPath(String path)
429         throws IOException,MalformedURLException;
430 
431     /* ------------------------------------------------------------ */
432     /** Get a resource from withing this resource.
433      * <p>
434      * This method is essentially an alias for {@link #addPath(String)}, but without checked exceptions.
435      * This method satisfied the {@link ResourceFactory} interface.
436      * @see org.eclipse.jetty.util.resource.ResourceFactory#getResource(java.lang.String)
437      */
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     /** Encode according to this resource type.
453      * The default implementation calls URI.encodePath(uri)
454      * @param uri 
455      * @return String encoded for this resource type.
456      */
457     public String encode(String uri)
458     {
459         return URIUtil.encodePath(uri);
460     }
461         
462     /* ------------------------------------------------------------ */
463     public Object getAssociate()
464     {
465         return _associate;
466     }
467 
468     /* ------------------------------------------------------------ */
469     public void setAssociate(Object o)
470     {
471         _associate=o;
472     }
473     
474     /* ------------------------------------------------------------ */
475     /**
476      * @return The canonical Alias of this resource or null if none.
477      */
478     public URL getAlias()
479     {
480         return null;
481     }
482     
483     /* ------------------------------------------------------------ */
484     /** Get the resource list as a HTML directory listing.
485      * @param base The base URL
486      * @param parent True if the parent directory should be included
487      * @return String of HTML
488      */
489     public String getListHTML(String base,boolean parent)
490         throws IOException
491     {
492         base=URIUtil.canonicalPath(base);
493         if (base==null || !isDirectory())
494             return null;
495         
496         String[] ls = list();
497         if (ls==null)
498             return null;
499         Arrays.sort(ls);
500         
501         String decodedBase = URIUtil.decodePath(base);
502         String title = "Directory: "+deTag(decodedBase);
503 
504         StringBuilder buf=new StringBuilder(4096);
505         buf.append("<HTML><HEAD>");
506         buf.append("<LINK HREF=\"").append("jetty-dir.css").append("\" REL=\"stylesheet\" TYPE=\"text/css\"/><TITLE>");
507         buf.append(title);
508         buf.append("</TITLE></HEAD><BODY>\n<H1>");
509         buf.append(title);
510         buf.append("</H1>\n<TABLE BORDER=0>\n");
511         
512         if (parent)
513         {
514             buf.append("<TR><TD><A HREF=\"");
515             buf.append(URIUtil.addPaths(base,"../"));
516             buf.append("\">Parent Directory</A></TD><TD></TD><TD></TD></TR>\n");
517         }
518         
519         String encodedBase = hrefEncodeURI(base);
520         
521         DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM,
522                                                        DateFormat.MEDIUM);
523         for (int i=0 ; i< ls.length ; i++)
524         {
525             Resource item = addPath(ls[i]);
526             
527             buf.append("\n<TR><TD><A HREF=\"");
528             String path=URIUtil.addPaths(encodedBase,URIUtil.encodePath(ls[i]));
529             
530             buf.append(path);
531             
532             if (item.isDirectory() && !path.endsWith("/"))
533                 buf.append(URIUtil.SLASH);
534             
535             // URIUtil.encodePath(buf,path);
536             buf.append("\">");
537             buf.append(deTag(ls[i]));
538             buf.append("&nbsp;");
539             buf.append("</TD><TD ALIGN=right>");
540             buf.append(item.length());
541             buf.append(" bytes&nbsp;</TD><TD>");
542             buf.append(dfmt.format(new Date(item.lastModified())));
543             buf.append("</TD></TR>");
544         }
545         buf.append("</TABLE>\n");
546 	buf.append("</BODY></HTML>\n");
547         
548         return buf.toString();
549     }
550     
551     /**
552      * Encode any characters that could break the URI string in an HREF.
553      * Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
554      * 
555      * The above example would parse incorrectly on various browsers as the "<" or '"' characters
556      * would end the href attribute value string prematurely.
557      * 
558      * @param raw the raw text to encode.
559      * @return the defanged text.
560      */
561     private static String hrefEncodeURI(String raw) 
562     {
563         StringBuffer buf = null;
564 
565         loop:
566         for (int i=0;i<raw.length();i++)
567         {
568             char c=raw.charAt(i);
569             switch(c)
570             {
571                 case '\'':
572                 case '"':
573                 case '<':
574                 case '>':
575                     buf=new StringBuffer(raw.length()<<1);
576                     break loop;
577             }
578         }
579         if (buf==null)
580             return raw;
581 
582         for (int i=0;i<raw.length();i++)
583         {
584             char c=raw.charAt(i);       
585             switch(c)
586             {
587               case '"':
588                   buf.append("%22");
589                   continue;
590               case '\'':
591                   buf.append("%27");
592                   continue;
593               case '<':
594                   buf.append("%3C");
595                   continue;
596               case '>':
597                   buf.append("%3E");
598                   continue;
599               default:
600                   buf.append(c);
601                   continue;
602             }
603         }
604 
605         return buf.toString();
606     }
607     
608     private static String deTag(String raw) 
609     {
610         return StringUtil.replace( StringUtil.replace(raw,"<","&lt;"), ">", "&gt;");
611     }
612     
613     /* ------------------------------------------------------------ */
614     /** 
615      * @param out 
616      * @param start First byte to write
617      * @param count Bytes to write or -1 for all of them.
618      */
619     public void writeTo(OutputStream out,long start,long count)
620         throws IOException
621     {
622         InputStream in = getInputStream();
623         try
624         {
625             in.skip(start);
626             if (count<0)
627                 IO.copy(in,out);
628             else
629                 IO.copy(in,out,count);
630         }
631         finally
632         {
633             in.close();
634         }
635     }    
636     
637     /* ------------------------------------------------------------ */
638     public void copyTo(File destination)
639         throws IOException
640     {
641         if (destination.exists())
642             throw new IllegalArgumentException(destination+" exists");
643         writeTo(new FileOutputStream(destination),0,-1);
644     }
645    
646 }