View Javadoc

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