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.URLStreamHandler;
29 import java.util.jar.JarFile;
30 import java.util.zip.ZipEntry;
31 import sun.net.www.ParseUtil;
32 import sun.net.www.protocol.jar.Handler;
33 
34 /**
35  * A {@link URLStreamHandler} for a specific class path {@link JarFile}. This class avoids the need
36  * to open a jar file multiple times to read resources if the jar file can be held open. The
37  * {@link URLConnection} objects created are a subclass of {@link JarURLConnection}.
38  *
39  * <p>Use {@link #getEntryUrlOrNull(String)} to obtain a URL backed by this stream handler.
40  */
41 public class ClassPathURLStreamHandler extends Handler {
42   private final String fileUri;
43   private final JarFile jarFile;
44 
ClassPathURLStreamHandler(String jarFileName)45   public ClassPathURLStreamHandler(String jarFileName) throws IOException {
46     jarFile = new JarFile(jarFileName);
47 
48     // File.toURI() is compliant with RFC 1738 in always creating absolute path names. If we
49     // construct the URL by concatenating strings, we might end up with illegal URLs for relative
50     // names.
51     this.fileUri = new File(jarFileName).toURI().toString();
52   }
53 
54   /**
55    * Returns a URL backed by this stream handler for the named resource, or {@code null} if the
56    * entry cannot be found under the exact name presented.
57    */
getEntryUrlOrNull(String entryName)58   public URL getEntryUrlOrNull(String entryName) {
59     if (jarFile.getEntry(entryName) != null) {
60       try {
61         // Encode the path to ensure that any special characters like # survive their trip through
62         // the URL. Entry names must use / as the path separator.
63         String encodedName = ParseUtil.encodePath(entryName, false);
64         return new URL("jar", null, -1, fileUri + "!/" + encodedName, this);
65       } catch (MalformedURLException e) {
66         throw new RuntimeException("Invalid entry name", e);
67       }
68     }
69     return null;
70   }
71 
72   /**
73    * Returns true if an entry with the specified name exists and is stored (not compressed),
74    * and false otherwise.
75    */
isEntryStored(String entryName)76   public boolean isEntryStored(String entryName) {
77     ZipEntry entry = jarFile.getEntry(entryName);
78     return entry != null && entry.getMethod() == ZipEntry.STORED;
79   }
80 
81   @Override
openConnection(URL url)82   protected URLConnection openConnection(URL url) throws IOException {
83     return new ClassPathURLConnection(url);
84   }
85 
86   /** Used from tests to indicate this stream handler is finished with. */
close()87   public void close() throws IOException {
88     jarFile.close();
89   }
90 
91   private class ClassPathURLConnection extends JarURLConnection {
92     // The JarFile instance can be shared across URLConnections and should not be closed when it is:
93     //
94     // Sharing occurs if getUseCaches() is true when connect() is called (which can take place
95     // implicitly). useCachedJarFile records the state of sharing at connect() time.
96     // useCachedJarFile == true is the common case. If developers call getJarFile().close() when
97     // sharing is enabled then it will affect other users (current and future) of the shared
98     // JarFile.
99     //
100     // Developers could call ClassLoader.findResource().openConnection() to get a URLConnection and
101     // then call setUseCaches(false) before connect() to prevent sharing. The developer must then
102     // call getJarFile().close() or close() on the inputStream from getInputStream() will do it
103     // automatically. This is likely to be an extremely rare case.
104     //
105     // Most developers are not expecting to deal with the lifecycle of the underlying JarFile object
106     // at all. The presence of the getJarFile() method and setUseCaches() forces us to consider /
107     // handle it.
108     private JarFile connectionJarFile;
109 
110     private ZipEntry jarEntry;
111     private InputStream jarInput;
112     private boolean closed;
113 
114     /**
115      * Indicates the behavior of the {@link #jarFile}. If true, the reference is shared and should
116      * not be closed. If false, it must be closed.
117      */
118     private boolean useCachedJarFile;
119 
120 
ClassPathURLConnection(URL url)121     public ClassPathURLConnection(URL url) throws MalformedURLException {
122       super(url);
123     }
124 
125     @Override
connect()126     public void connect() throws IOException {
127       if (!connected) {
128         this.jarEntry = jarFile.getEntry(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
getJarFile()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 and 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
getInputStream()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 then closing the input stream will close the
167           // URLConnection and any JarFile returned from getJarFile(). If the jar file is cached
168           // we must not close it because it will affect other URLConnections.
169           if (connectionJarFile != null && !useCachedJarFile) {
170             connectionJarFile.close();
171             closed = true;
172           }
173         }
174       };
175     }
176 
177     /**
178      * Returns the content type of the entry based on the name of the entry. Returns
179      * non-null results ("content/unknown" for unknown types).
180      *
181      * @return the content type
182      */
183     @Override
getContentType()184     public String getContentType() {
185       String cType = guessContentTypeFromName(getEntryName());
186       if (cType == null) {
187         cType = "content/unknown";
188       }
189       return cType;
190     }
191 
192     @Override
getContentLength()193     public int getContentLength() {
194       try {
195         connect();
196         return (int) getJarEntry().getSize();
197       } catch (IOException e) {
198         // Ignored
199       }
200       return -1;
201     }
202   }
203 }
204