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