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 }