1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package libcore.net.url; 19 20import java.io.File; 21import java.io.FileNotFoundException; 22import java.io.FileOutputStream; 23import java.io.FilterInputStream; 24import java.io.IOException; 25import java.io.InputStream; 26import java.net.ContentHandler; 27import java.net.ContentHandlerFactory; 28import java.net.JarURLConnection; 29import java.net.MalformedURLException; 30import java.net.URL; 31import java.security.Permission; 32import java.util.HashMap; 33import java.util.Iterator; 34import java.util.Map; 35import java.util.Set; 36import java.util.jar.JarEntry; 37import java.util.jar.JarFile; 38import java.util.zip.ZipFile; 39import libcore.net.UriCodec; 40 41/** 42 * This subclass extends {@code URLConnection}. 43 * <p> 44 * 45 * This class is responsible for connecting and retrieving resources from a Jar 46 * file which can be anywhere that can be referred to by an URL. 47 */ 48public class JarURLConnectionImpl extends JarURLConnection { 49 50 private static final HashMap<URL, JarFile> jarCache = new HashMap<URL, JarFile>(); 51 52 private URL jarFileURL; 53 54 private InputStream jarInput; 55 56 private JarFile jarFile; 57 58 private JarEntry jarEntry; 59 60 private boolean closed; 61 62 /** 63 * @param url 64 * the URL of the JAR 65 * @throws MalformedURLException 66 * if the URL is malformed 67 * @throws IOException 68 * if there is a problem opening the connection. 69 */ 70 public JarURLConnectionImpl(URL url) throws MalformedURLException, IOException { 71 super(url); 72 jarFileURL = getJarFileURL(); 73 jarFileURLConnection = jarFileURL.openConnection(); 74 } 75 76 /** 77 * @see java.net.URLConnection#connect() 78 */ 79 @Override 80 public void connect() throws IOException { 81 if (!connected) { 82 findJarFile(); // ensure the file can be found 83 findJarEntry(); // ensure the entry, if any, can be found 84 connected = true; 85 } 86 } 87 88 /** 89 * Returns the Jar file referred by this {@code URLConnection}. 90 * 91 * @throws IOException 92 * thrown if an IO error occurs while connecting to the 93 * resource. 94 */ 95 @Override 96 public JarFile getJarFile() throws IOException { 97 connect(); 98 return jarFile; 99 } 100 101 /** 102 * Returns the Jar file referred by this {@code URLConnection} 103 * 104 * @throws IOException 105 * if an IO error occurs while connecting to the resource. 106 */ 107 private void findJarFile() throws IOException { 108 if (getUseCaches()) { 109 synchronized (jarCache) { 110 jarFile = jarCache.get(jarFileURL); 111 } 112 if (jarFile == null) { 113 JarFile jar = openJarFile(); 114 synchronized (jarCache) { 115 jarFile = jarCache.get(jarFileURL); 116 if (jarFile == null) { 117 jarCache.put(jarFileURL, jar); 118 jarFile = jar; 119 } else { 120 jar.close(); 121 } 122 } 123 } 124 } else { 125 jarFile = openJarFile(); 126 } 127 128 if (jarFile == null) { 129 throw new IOException(); 130 } 131 } 132 133 private JarFile openJarFile() throws IOException { 134 if (jarFileURL.getProtocol().equals("file")) { 135 String decodedFile = UriCodec.decode(jarFileURL.getFile()); 136 return new JarFile(new File(decodedFile), true, ZipFile.OPEN_READ); 137 } else { 138 final InputStream is = jarFileURL.openConnection().getInputStream(); 139 try { 140 FileOutputStream fos = null; 141 JarFile result = null; 142 try { 143 File tempJar = File.createTempFile("hyjar_", ".tmp", null); 144 tempJar.deleteOnExit(); 145 fos = new FileOutputStream(tempJar); 146 byte[] buf = new byte[4096]; 147 int nbytes = 0; 148 while ((nbytes = is.read(buf)) > -1) { 149 fos.write(buf, 0, nbytes); 150 } 151 fos.close(); 152 return new JarFile(tempJar, true, ZipFile.OPEN_READ | ZipFile.OPEN_DELETE); 153 } catch (IOException e) { 154 return null; 155 } finally { 156 if (fos != null) { 157 try { 158 fos.close(); 159 } catch (IOException ex) { 160 return null; 161 } 162 } 163 } 164 } finally { 165 if (is != null) { 166 is.close(); 167 } 168 } 169 } 170 } 171 172 /** 173 * Returns the JarEntry of the entry referenced by this {@code 174 * URLConnection}. 175 * 176 * @return the JarEntry referenced 177 * 178 * @throws IOException 179 * if an IO error occurs while getting the entry 180 */ 181 @Override 182 public JarEntry getJarEntry() throws IOException { 183 connect(); 184 return jarEntry; 185 186 } 187 188 /** 189 * Look up the JarEntry of the entry referenced by this {@code 190 * URLConnection}. 191 */ 192 private void findJarEntry() throws IOException { 193 if (getEntryName() == null) { 194 return; 195 } 196 jarEntry = jarFile.getJarEntry(getEntryName()); 197 if (jarEntry == null) { 198 throw new FileNotFoundException(getEntryName()); 199 } 200 } 201 202 /** 203 * Creates an input stream for reading from this URL Connection. 204 * 205 * @return the input stream 206 * 207 * @throws IOException 208 * if an IO error occurs while connecting to the resource. 209 */ 210 @Override 211 public InputStream getInputStream() throws IOException { 212 if (closed) { 213 throw new IllegalStateException("JarURLConnection InputStream has been closed"); 214 } 215 connect(); 216 if (jarInput != null) { 217 return jarInput; 218 } 219 if (jarEntry == null) { 220 throw new IOException("Jar entry not specified"); 221 } 222 return jarInput = new JarURLConnectionInputStream(jarFile 223 .getInputStream(jarEntry), jarFile); 224 } 225 226 /** 227 * Returns the content type of the resource. For jar file itself 228 * "x-java/jar" should be returned, for jar entries the content type of the 229 * entry should be returned. Returns non-null results ("content/unknown" for 230 * unknown types). 231 * 232 * @return the content type 233 */ 234 @Override 235 public String getContentType() { 236 if (url.getFile().endsWith("!/")) { 237 // the type for jar file itself is always "x-java/jar" 238 return "x-java/jar"; 239 } 240 String cType = null; 241 String entryName = getEntryName(); 242 243 if (entryName != null) { 244 // if there is an Jar Entry, get the content type from the name 245 cType = guessContentTypeFromName(entryName); 246 } else { 247 try { 248 connect(); 249 cType = jarFileURLConnection.getContentType(); 250 } catch (IOException ioe) { 251 // Ignore 252 } 253 } 254 if (cType == null) { 255 cType = "content/unknown"; 256 } 257 return cType; 258 } 259 260 /** 261 * Returns the content length of the resource. Test cases reveal that if the 262 * URL is referring to a Jar file, this method answers a content-length 263 * returned by URLConnection. For jar entry it should return it's size. 264 * Otherwise, it will return -1. 265 * 266 * @return the content length 267 */ 268 @Override 269 public int getContentLength() { 270 try { 271 connect(); 272 if (jarEntry == null) { 273 return jarFileURLConnection.getContentLength(); 274 } 275 return (int) getJarEntry().getSize(); 276 } catch (IOException e) { 277 // Ignored 278 } 279 return -1; 280 } 281 282 /** 283 * Returns the object pointed by this {@code URL}. If this URLConnection is 284 * pointing to a Jar File (no Jar Entry), this method will return a {@code 285 * JarFile} If there is a Jar Entry, it will return the object corresponding 286 * to the Jar entry content type. 287 * 288 * @return a non-null object 289 * 290 * @throws IOException 291 * if an IO error occurred 292 * 293 * @see ContentHandler 294 * @see ContentHandlerFactory 295 * @see java.io.IOException 296 * @see #setContentHandlerFactory(ContentHandlerFactory) 297 */ 298 @Override 299 public Object getContent() throws IOException { 300 connect(); 301 // if there is no Jar Entry, return a JarFile 302 if (jarEntry == null) { 303 return jarFile; 304 } 305 return super.getContent(); 306 } 307 308 /** 309 * Returns the permission, in this case the subclass, FilePermission object 310 * which represents the permission necessary for this URLConnection to 311 * establish the connection. 312 * 313 * @return the permission required for this URLConnection. 314 * 315 * @throws IOException 316 * thrown when an IO exception occurs while creating the 317 * permission. 318 */ 319 320 @Override 321 public Permission getPermission() throws IOException { 322 return jarFileURLConnection.getPermission(); 323 } 324 325 @Override 326 public boolean getUseCaches() { 327 return jarFileURLConnection.getUseCaches(); 328 } 329 330 @Override 331 public void setUseCaches(boolean usecaches) { 332 jarFileURLConnection.setUseCaches(usecaches); 333 } 334 335 @Override 336 public boolean getDefaultUseCaches() { 337 return jarFileURLConnection.getDefaultUseCaches(); 338 } 339 340 @Override 341 public void setDefaultUseCaches(boolean defaultusecaches) { 342 jarFileURLConnection.setDefaultUseCaches(defaultusecaches); 343 } 344 345 private class JarURLConnectionInputStream extends FilterInputStream { 346 final JarFile jarFile; 347 348 protected JarURLConnectionInputStream(InputStream in, JarFile file) { 349 super(in); 350 jarFile = file; 351 } 352 353 @Override 354 public void close() throws IOException { 355 super.close(); 356 if (!getUseCaches()) { 357 closed = true; 358 jarFile.close(); 359 } 360 } 361 } 362} 363