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