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(" ");
552 buf.append("</A></TD><TD ALIGN=right>");
553 buf.append(item.length());
554 buf.append(" bytes </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,"<","<"), ">", ">");
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 }