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 is shared across URLConnections and must not be closed.
    107     private JarFile connectionJarFile;
    108 
    109     private ZipEntry jarEntry;
    110     private InputStream jarInput;
    111     private boolean closed;
    112 
    113     /**
    114      * Indicates the behavior of the {@link #jarFile}. If true, the reference is shared and should
    115      * not be closed. If false, it must be closed.
    116      */
    117     private boolean useCachedJarFile;
    118 
    119 
    120     public ClassPathURLConnection(URL url) throws MalformedURLException {
    121       super(url);
    122     }
    123 
    124     @Override
    125     public void connect() throws IOException {
    126       if (!connected) {
    127         this.jarEntry = findEntryWithDirectoryFallback(ClassPathURLStreamHandler.this.jarFile,
    128             getEntryName());
    129         if (jarEntry == null) {
    130           throw new FileNotFoundException(
    131               "URL does not correspond to an entry in the zip file. URL=" + url
    132               + ", zipfile=" + jarFile.getName());
    133         }
    134         useCachedJarFile = getUseCaches();
    135         connected = true;
    136       }
    137     }
    138 
    139     @Override
    140     public JarFile getJarFile() throws IOException {
    141       connect();
    142 
    143       // We do cache in the surrounding class if useCachedJarFile is true to
    144       // preserve garbage collection semantics to avoid leak warnings.
    145       if (useCachedJarFile) {
    146         connectionJarFile = jarFile;
    147       } else {
    148         connectionJarFile = new JarFile(jarFile.getName());
    149       }
    150       return connectionJarFile;
    151     }
    152 
    153     @Override
    154     public InputStream getInputStream() throws IOException {
    155       if (closed) {
    156         throw new IllegalStateException("JarURLConnection InputStream has been closed");
    157       }
    158       connect();
    159       if (jarInput != null) {
    160         return jarInput;
    161       }
    162       return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) {
    163         @Override
    164         public void close() throws IOException {
    165           super.close();
    166           // If the jar file is not cached closing the input stream will close the URLConnection and
    167           // any JarFile returned from getJarFile().
    168           if (connectionJarFile != null && !useCachedJarFile) {
    169             connectionJarFile.close();
    170             closed = true;
    171           }
    172         }
    173       };
    174     }
    175 
    176     /**
    177      * Returns the content type of the entry based on the name of the entry. Returns
    178      * non-null results ("content/unknown" for unknown types).
    179      *
    180      * @return the content type
    181      */
    182     @Override
    183     public String getContentType() {
    184       String cType = guessContentTypeFromName(getEntryName());
    185       if (cType == null) {
    186         cType = "content/unknown";
    187       }
    188       return cType;
    189     }
    190 
    191     @Override
    192     public int getContentLength() {
    193       try {
    194         connect();
    195         return (int) getJarEntry().getSize();
    196       } catch (IOException e) {
    197         // Ignored
    198       }
    199       return -1;
    200     }
    201   }
    202 }
    203