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(" ");
470 buf.append("</TD><TD ALIGN=right>");
471 buf.append(item.length());
472 buf.append(" bytes </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,"<","<"), ">", ">");
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 }