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