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