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 this(jarFileName, /* enableZipPathValidator */ true); 47 } 48 49 /** @hide */ ClassPathURLStreamHandler(String jarFileName, boolean enableZipPathValidator)50 public ClassPathURLStreamHandler(String jarFileName, boolean enableZipPathValidator) throws 51 IOException { 52 jarFile = new JarFile(jarFileName, enableZipPathValidator, /* verify */ true); 53 54 // File.toURI() is compliant with RFC 1738 in always creating absolute path names. If we 55 // construct the URL by concatenating strings, we might end up with illegal URLs for relative 56 // names. 57 this.fileUri = new File(jarFileName).toURI().toString(); 58 } 59 60 /** 61 * Returns a URL backed by this stream handler for the named resource, or {@code null} if the 62 * entry cannot be found under the exact name presented. 63 */ getEntryUrlOrNull(String entryName)64 public URL getEntryUrlOrNull(String entryName) { 65 if (jarFile.getEntry(entryName) != null) { 66 try { 67 // Encode the path to ensure that any special characters like # survive their trip through 68 // the URL. Entry names must use / as the path separator. 69 String encodedName = ParseUtil.encodePath(entryName, false); 70 return new URL("jar", null, -1, fileUri + "!/" + encodedName, this); 71 } catch (MalformedURLException e) { 72 throw new RuntimeException("Invalid entry name", e); 73 } 74 } 75 return null; 76 } 77 78 /** 79 * Returns true if an entry with the specified name exists and is stored (not compressed), 80 * and false otherwise. 81 */ isEntryStored(String entryName)82 public boolean isEntryStored(String entryName) { 83 ZipEntry entry = jarFile.getEntry(entryName); 84 return entry != null && entry.getMethod() == ZipEntry.STORED; 85 } 86 87 @Override openConnection(URL url)88 protected URLConnection openConnection(URL url) throws IOException { 89 return new ClassPathURLConnection(url); 90 } 91 92 /** Used from tests to indicate this stream handler is finished with. */ close()93 public void close() throws IOException { 94 jarFile.close(); 95 } 96 97 private class ClassPathURLConnection extends JarURLConnection { 98 // The JarFile instance can be shared across URLConnections and should not be closed when it is: 99 // 100 // Sharing occurs if getUseCaches() is true when connect() is called (which can take place 101 // implicitly). useCachedJarFile records the state of sharing at connect() time. 102 // useCachedJarFile == true is the common case. If developers call getJarFile().close() when 103 // sharing is enabled then it will affect other users (current and future) of the shared 104 // JarFile. 105 // 106 // Developers could call ClassLoader.findResource().openConnection() to get a URLConnection and 107 // then call setUseCaches(false) before connect() to prevent sharing. The developer must then 108 // call getJarFile().close() or close() on the inputStream from getInputStream() will do it 109 // automatically. This is likely to be an extremely rare case. 110 // 111 // Most developers are not expecting to deal with the lifecycle of the underlying JarFile object 112 // at all. The presence of the getJarFile() method and setUseCaches() forces us to consider / 113 // handle it. 114 private JarFile connectionJarFile; 115 116 private ZipEntry jarEntry; 117 private InputStream jarInput; 118 private boolean closed; 119 120 /** 121 * Indicates the behavior of the {@link #jarFile}. If true, the reference is shared and should 122 * not be closed. If false, it must be closed. 123 */ 124 private boolean useCachedJarFile; 125 126 ClassPathURLConnection(URL url)127 public ClassPathURLConnection(URL url) throws MalformedURLException { 128 super(url); 129 } 130 131 @Override connect()132 public void connect() throws IOException { 133 if (!connected) { 134 this.jarEntry = jarFile.getEntry(getEntryName()); 135 if (jarEntry == null) { 136 throw new FileNotFoundException( 137 "URL does not correspond to an entry in the zip file. URL=" + url 138 + ", zipfile=" + jarFile.getName()); 139 } 140 useCachedJarFile = getUseCaches(); 141 connected = true; 142 } 143 } 144 145 @Override getJarFile()146 public JarFile getJarFile() throws IOException { 147 connect(); 148 149 // We do cache in the surrounding class if useCachedJarFile is true to 150 // preserve garbage collection semantics and to avoid leak warnings. 151 if (useCachedJarFile) { 152 connectionJarFile = jarFile; 153 } else { 154 connectionJarFile = new JarFile(jarFile.getName()); 155 } 156 return connectionJarFile; 157 } 158 159 @Override getInputStream()160 public InputStream getInputStream() throws IOException { 161 if (closed) { 162 throw new IllegalStateException("JarURLConnection InputStream has been closed"); 163 } 164 connect(); 165 if (jarInput != null) { 166 return jarInput; 167 } 168 return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) { 169 @Override 170 public void close() throws IOException { 171 super.close(); 172 // If the jar file is not cached then closing the input stream will close the 173 // URLConnection and any JarFile returned from getJarFile(). If the jar file is cached 174 // we must not close it because it will affect other URLConnections. 175 if (connectionJarFile != null && !useCachedJarFile) { 176 connectionJarFile.close(); 177 closed = true; 178 } 179 } 180 }; 181 } 182 183 /** 184 * Returns the content type of the entry based on the name of the entry. Returns 185 * non-null results ("content/unknown" for unknown types). 186 * 187 * @return the content type 188 */ 189 @Override getContentType()190 public String getContentType() { 191 String cType = guessContentTypeFromName(getEntryName()); 192 if (cType == null) { 193 cType = "content/unknown"; 194 } 195 return cType; 196 } 197 198 @Override getContentLength()199 public int getContentLength() { 200 try { 201 connect(); 202 return (int) getJarEntry().getSize(); 203 } catch (IOException e) { 204 // Ignored 205 } 206 return -1; 207 } 208 } 209 } 210