ClassPathURLStreamHandler.java revision 384730cb57f41235f09829355d7ce67132625f7f
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.jar.StrictJarFile;
31import java.util.zip.ZipEntry;
32import libcore.net.url.JarHandler;
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 */
41public class ClassPathURLStreamHandler extends JarHandler {
42  private final String fileUri;
43  private final StrictJarFile jarFile;
44
45  public ClassPathURLStreamHandler(String jarFileName) throws IOException {
46    // We use StrictJarFile because it is much less heap memory hungry than ZipFile / JarFile.
47    jarFile = new StrictJarFile(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 (jarFile.findEntry(entryName) != null) {
61      try {
62        // We rely on the URL/the stream handler to deal with any url encoding necessary here, and
63        // we assume it is completely reversible.
64        return new URL("jar", null, -1, fileUri + "!/" + entryName, 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 entry with specified name exists and stored (not compressed),
74   * and false otherwise.
75   */
76  public boolean isEntryStored(String entryName) {
77    ZipEntry entry = jarFile.findEntry(entryName);
78    return entry != null && entry.getMethod() == ZipEntry.STORED;
79  }
80
81  @Override
82  protected URLConnection openConnection(URL url) throws IOException {
83    return new ClassPathURLConnection(url, jarFile);
84  }
85
86  public void close() throws IOException {
87    jarFile.close();
88  }
89
90  private static class ClassPathURLConnection extends JarURLConnection {
91
92    private final StrictJarFile strictJarFile;
93    private ZipEntry jarEntry;
94    private InputStream jarInput;
95    private boolean closed;
96    private JarFile jarFile;
97
98    public ClassPathURLConnection(URL url, StrictJarFile strictJarFile)
99            throws MalformedURLException {
100      super(url);
101      this.strictJarFile = strictJarFile;
102    }
103
104    @Override
105    public void connect() throws IOException {
106      if (!connected) {
107          this.jarEntry = strictJarFile.findEntry(getEntryName());
108          if (jarEntry == null) {
109              throw new FileNotFoundException(
110                      "URL does not correspond to an entry in the zip file. URL=" + url
111                              + ", zipfile=" + strictJarFile.getName());
112          }
113          connected = true;
114      }
115    }
116
117    @Override
118    public JarFile getJarFile() throws IOException {
119        connect();
120
121        // This is expensive because we only pretend that we wrap a JarFile.
122        if (jarFile == null) {
123            jarFile = new JarFile(strictJarFile.getName());
124        }
125        return jarFile;
126    }
127
128    @Override
129    public InputStream getInputStream() throws IOException {
130      if (closed) {
131        throw new IllegalStateException("JarURLConnection InputStream has been closed");
132      }
133      connect();
134      if (jarInput != null) {
135        return jarInput;
136      }
137      return jarInput = new FilterInputStream(strictJarFile.getInputStream(jarEntry)) {
138        @Override
139        public void close() throws IOException {
140          super.close();
141          closed = true;
142        }
143      };
144    }
145
146    /**
147     * Returns the content type of the entry based on the name of the entry. Returns
148     * non-null results ("content/unknown" for unknown types).
149     *
150     * @return the content type
151     */
152    @Override
153    public String getContentType() {
154      String cType = guessContentTypeFromName(getEntryName());
155      if (cType == null) {
156        cType = "content/unknown";
157      }
158      return cType;
159    }
160
161    @Override
162    public int getContentLength() {
163      try {
164        connect();
165        return (int) getJarEntry().getSize();
166      } catch (IOException e) {
167        // Ignored
168      }
169      return -1;
170    }
171  }
172}
173