Home | History | Annotate | Download | only in io
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      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  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package libcore.io;
     18 
     19 import java.io.File;
     20 import java.io.FileNotFoundException;
     21 import java.io.FilterInputStream;
     22 import java.io.IOException;
     23 import java.io.InputStream;
     24 import java.net.JarURLConnection;
     25 import java.net.MalformedURLException;
     26 import java.net.URL;
     27 import java.net.URLConnection;
     28 import java.net.URLEncoder;
     29 import java.net.URLStreamHandler;
     30 import java.util.jar.JarFile;
     31 import java.util.zip.ZipEntry;
     32 import sun.net.www.ParseUtil;
     33 import sun.net.www.protocol.jar.Handler;
     34 
     35 /**
     36  * A {@link URLStreamHandler} for a specific class path {@link JarFile}. This class avoids the need
     37  * to open a jar file multiple times to read resources if the jar file can be held open. The
     38  * {@link URLConnection} objects created are a subclass of {@link JarURLConnection}.
     39  *
     40  * <p>Use {@link #getEntryUrlOrNull(String)} to obtain a URL backed by this stream handler.
     41  */
     42 public class ClassPathURLStreamHandler extends Handler {
     43   private final String fileUri;
     44   private final JarFile jarFile;
     45 
     46   public ClassPathURLStreamHandler(String jarFileName) throws IOException {
     47     jarFile = new JarFile(jarFileName);
     48 
     49     // File.toURI() is compliant with RFC 1738 in always creating absolute path names. If we
     50     // construct the URL by concatenating strings, we might end up with illegal URLs for relative
     51     // names.
     52     this.fileUri = new File(jarFileName).toURI().toString();
     53   }
     54 
     55   /**
     56    * Returns a URL backed by this stream handler for the named resource, or {@code null} if the
     57    * entry cannot be found under the exact name presented.
     58    */
     59   public URL getEntryUrlOrNull(String entryName) {
     60     if (findEntryWithDirectoryFallback(jarFile, entryName) != null) {
     61       try {
     62         // Encode the path to ensure that any special characters like # survive their trip through
     63         // the URL. Entry names must use / as the path separator.
     64         String encodedName = ParseUtil.encodePath(entryName, false);
     65         return new URL("jar", null, -1, fileUri + "!/" + encodedName, this);
     66       } catch (MalformedURLException e) {
     67         throw new RuntimeException("Invalid entry name", e);
     68       }
     69     }
     70     return null;
     71   }
     72 
     73   /**
     74    * Returns true if an entry with the specified name exists and is stored (not compressed),
     75    * and false otherwise.
     76    */
     77   public boolean isEntryStored(String entryName) {
     78     ZipEntry entry = jarFile.getEntry(entryName);
     79     return entry != null && entry.getMethod() == ZipEntry.STORED;
     80   }
     81 
     82   @Override
     83   protected URLConnection openConnection(URL url) throws IOException {
     84     return new ClassPathURLConnection(url);
     85   }
     86 
     87   /** Used from tests to indicate this stream handler is finished with. */
     88   public void close() throws IOException {
     89     jarFile.close();
     90   }
     91 
     92   /**
     93    * Finds an entry with the specified name in the {@code jarFile}. If an exact match isn't found it
     94    * will also try with "/" appended, if appropriate. This is to maintain compatibility with
     95    * {@link sun.net.www.protocol.jar.Handler} and its treatment of directory entries.
     96    */
     97   static ZipEntry findEntryWithDirectoryFallback(JarFile jarFile, String entryName) {
     98     ZipEntry entry = jarFile.getEntry(entryName);
     99     if (entry == null && !entryName.endsWith("/") ) {
    100       entry = jarFile.getEntry(entryName + "/");
    101     }
    102     return entry;
    103   }
    104 
    105   private class ClassPathURLConnection extends JarURLConnection {
    106     // The JarFile instance can be shared across URLConnections and should not be closed when it is:
    107     //
    108     // Sharing occurs if getUseCaches() is true when connect() is called (which can take place
    109     // implicitly). useCachedJarFile records the state of sharing at connect() time.
    110     // useCachedJarFile == true is the common case. If developers call getJarFile().close() when
    111     // sharing is enabled then it will affect other users (current and future) of the shared
    112     // JarFile.
    113     //
    114     // Developers could call ClassLoader.findResource().openConnection() to get a URLConnection and
    115     // then call setUseCaches(false) before connect() to prevent sharing. The developer must then
    116     // call getJarFile().close() or close() on the inputStream from getInputStream() will do it
    117     // automatically. This is likely to be an extremely rare case.
    118     //
    119     // Most developers are not expecting to deal with the lifecycle of the underlying JarFile object
    120     // at all. The presence of the getJarFile() method and setUseCaches() forces us to consider /
    121     // handle it.
    122     private JarFile connectionJarFile;
    123 
    124     private ZipEntry jarEntry;
    125     private InputStream jarInput;
    126     private boolean closed;
    127 
    128     /**
    129      * Indicates the behavior of the {@link #jarFile}. If true, the reference is shared and should
    130      * not be closed. If false, it must be closed.
    131      */
    132     private boolean useCachedJarFile;
    133 
    134 
    135     public ClassPathURLConnection(URL url) throws MalformedURLException {
    136       super(url);
    137     }
    138 
    139     @Override
    140     public void connect() throws IOException {
    141       if (!connected) {
    142         this.jarEntry = findEntryWithDirectoryFallback(ClassPathURLStreamHandler.this.jarFile,
    143             getEntryName());
    144         if (jarEntry == null) {
    145           throw new FileNotFoundException(
    146               "URL does not correspond to an entry in the zip file. URL=" + url
    147               + ", zipfile=" + jarFile.getName());
    148         }
    149         useCachedJarFile = getUseCaches();
    150         connected = true;
    151       }
    152     }
    153 
    154     @Override
    155     public JarFile getJarFile() throws IOException {
    156       connect();
    157 
    158       // We do cache in the surrounding class if useCachedJarFile is true to
    159       // preserve garbage collection semantics and to avoid leak warnings.
    160       if (useCachedJarFile) {
    161         connectionJarFile = jarFile;
    162       } else {
    163         connectionJarFile = new JarFile(jarFile.getName());
    164       }
    165       return connectionJarFile;
    166     }
    167 
    168     @Override
    169     public InputStream getInputStream() throws IOException {
    170       if (closed) {
    171         throw new IllegalStateException("JarURLConnection InputStream has been closed");
    172       }
    173       connect();
    174       if (jarInput != null) {
    175         return jarInput;
    176       }
    177       return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) {
    178         @Override
    179         public void close() throws IOException {
    180           super.close();
    181           // If the jar file is not cached then closing the input stream will close the
    182           // URLConnection and any JarFile returned from getJarFile(). If the jar file is cached
    183           // we must not close it because it will affect other URLConnections.
    184           if (connectionJarFile != null && !useCachedJarFile) {
    185             connectionJarFile.close();
    186             closed = true;
    187           }
    188         }
    189       };
    190     }
    191 
    192     /**
    193      * Returns the content type of the entry based on the name of the entry. Returns
    194      * non-null results ("content/unknown" for unknown types).
    195      *
    196      * @return the content type
    197      */
    198     @Override
    199     public String getContentType() {
    200       String cType = guessContentTypeFromName(getEntryName());
    201       if (cType == null) {
    202         cType = "content/unknown";
    203       }
    204       return cType;
    205     }
    206 
    207     @Override
    208     public int getContentLength() {
    209       try {
    210         connect();
    211         return (int) getJarEntry().getSize();
    212       } catch (IOException e) {
    213         // Ignored
    214       }
    215       return -1;
    216     }
    217   }
    218 }
    219