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