ClassPathURLStreamHandler.java revision e8e4da2f84da30bbc11a63b7a87a153f62b1ce65
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
17package libcore.io;
18
19import java.io.File;
20import java.io.FileNotFoundException;
21import java.io.FilterInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.net.JarURLConnection;
25import java.net.MalformedURLException;
26import java.net.URL;
27import java.net.URLConnection;
28import java.net.URLStreamHandler;
29import java.util.jar.JarFile;
30import java.util.zip.ZipEntry;
31import sun.net.www.protocol.jar.Handler;
32
33/**
34 * A {@link URLStreamHandler} for a specific class path {@link JarFile}. This class avoids the need
35 * to open a jar file multiple times to read resources if the jar file can be held open. The
36 * {@link URLConnection} objects created are a subclass of {@link JarURLConnection}.
37 *
38 * <p>Use {@link #getEntryUrlOrNull(String)} to obtain a URL backed by this stream handler.
39 */
40public class ClassPathURLStreamHandler extends Handler {
41  private final String fileUri;
42  private final JarFile jarFile;
43
44  public ClassPathURLStreamHandler(String jarFileName) throws IOException {
45    jarFile = new JarFile(jarFileName);
46
47    // File.toURI() is compliant with RFC 1738 in always creating absolute path names. If we
48    // construct the URL by concatenating strings, we might end up with illegal URLs for relative
49    // names.
50    this.fileUri = new File(jarFileName).toURI().toString();
51  }
52
53  /**
54   * Returns a URL backed by this stream handler for the named resource, or {@code null} if the
55   * entry cannot be found under the exact name presented.
56   */
57  public URL getEntryUrlOrNull(String entryName) {
58    if (findEntryWithDirectoryFallback(jarFile, entryName) != null) {
59      try {
60        // We rely on the URL/the stream handler to deal with any url encoding necessary here, and
61        // we assume it is completely reversible.
62        return new URL("jar", null, -1, fileUri + "!/" + entryName, this);
63      } catch (MalformedURLException e) {
64        throw new RuntimeException("Invalid entry name", e);
65      }
66    }
67    return null;
68  }
69
70  /**
71   * Returns true if an entry with the specified name exists and is stored (not compressed),
72   * and false otherwise.
73   */
74  public boolean isEntryStored(String entryName) {
75    ZipEntry entry = jarFile.getEntry(entryName);
76    return entry != null && entry.getMethod() == ZipEntry.STORED;
77  }
78
79  @Override
80  protected URLConnection openConnection(URL url) throws IOException {
81    return new ClassPathURLConnection(url);
82  }
83
84  /** Used from tests to indicate this stream handler is finished with. */
85  public void close() throws IOException {
86    jarFile.close();
87  }
88
89  /**
90   * Finds an entry with the specified name in the {@code jarFile}. If an exact match isn't found it
91   * will also try with "/" appended, if appropriate. This is to maintain compatibility with
92   * {@link sun.net.www.protocol.jar.Handler} and its treatment of directory entries.
93   */
94  static ZipEntry findEntryWithDirectoryFallback(JarFile jarFile, String entryName) {
95    ZipEntry entry = jarFile.getEntry(entryName);
96    if (entry == null && !entryName.endsWith("/") ) {
97      entry = jarFile.getEntry(entryName + "/");
98    }
99    return entry;
100  }
101
102  private class ClassPathURLConnection extends JarURLConnection {
103    // The JarFile instance is shared across URLConnections and must not be closed.
104    private JarFile connectionJarFile;
105
106    private ZipEntry jarEntry;
107    private InputStream jarInput;
108    private boolean closed;
109
110    /**
111     * Indicates the behavior of the {@link #jarFile}. If true, the reference is shared and should
112     * not be closed. If false, it must be closed.
113     */
114    private boolean useCachedJarFile;
115
116
117    public ClassPathURLConnection(URL url) throws MalformedURLException {
118      super(url);
119    }
120
121    @Override
122    public void connect() throws IOException {
123      if (!connected) {
124        this.jarEntry = findEntryWithDirectoryFallback(ClassPathURLStreamHandler.this.jarFile,
125            getEntryName());
126        if (jarEntry == null) {
127          throw new FileNotFoundException(
128              "URL does not correspond to an entry in the zip file. URL=" + url
129              + ", zipfile=" + jarFile.getName());
130        }
131        useCachedJarFile = getUseCaches();
132        connected = true;
133      }
134    }
135
136    @Override
137    public JarFile getJarFile() throws IOException {
138      connect();
139
140      // We do cache in the surrounding class if useCachedJarFile is true to
141      // preserve garbage collection semantics to avoid leak warnings.
142      if (useCachedJarFile) {
143        connectionJarFile = jarFile;
144      } else {
145        connectionJarFile = new JarFile(jarFile.getName());
146      }
147      return connectionJarFile;
148    }
149
150    @Override
151    public InputStream getInputStream() throws IOException {
152      if (closed) {
153        throw new IllegalStateException("JarURLConnection InputStream has been closed");
154      }
155      connect();
156      if (jarInput != null) {
157        return jarInput;
158      }
159      return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) {
160        @Override
161        public void close() throws IOException {
162          super.close();
163          // If the jar file is not cached closing the input stream will close the URLConnection and
164          // any JarFile returned from getJarFile().
165          if (connectionJarFile != null && !useCachedJarFile) {
166            connectionJarFile.close();
167            closed = true;
168          }
169        }
170      };
171    }
172
173    /**
174     * Returns the content type of the entry based on the name of the entry. Returns
175     * non-null results ("content/unknown" for unknown types).
176     *
177     * @return the content type
178     */
179    @Override
180    public String getContentType() {
181      String cType = guessContentTypeFromName(getEntryName());
182      if (cType == null) {
183        cType = "content/unknown";
184      }
185      return cType;
186    }
187
188    @Override
189    public int getContentLength() {
190      try {
191        connect();
192        return (int) getJarEntry().getSize();
193      } catch (IOException e) {
194        // Ignored
195      }
196      return -1;
197    }
198  }
199}
200