View Javadoc

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