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 18 package libcore.net.url; 19 20 import java.io.File; 21 import java.io.FileNotFoundException; 22 import java.io.FileOutputStream; 23 import java.io.FilterInputStream; 24 import java.io.IOException; 25 import java.io.InputStream; 26 import java.net.ContentHandler; 27 import java.net.ContentHandlerFactory; 28 import java.net.JarURLConnection; 29 import java.net.MalformedURLException; 30 import java.net.URL; 31 import java.security.Permission; 32 import java.util.HashMap; 33 import java.util.Iterator; 34 import java.util.Map; 35 import java.util.Set; 36 import java.util.jar.JarEntry; 37 import java.util.jar.JarFile; 38 import java.util.zip.ZipFile; 39 import 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 */ 48 public class JarURLConnectionImpl extends JarURLConnection { 49 50 static 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, 71 IOException { 72 super(url); 73 jarFileURL = getJarFileURL(); 74 jarFileURLConnection = jarFileURL.openConnection(); 75 } 76 77 /** 78 * @see java.net.URLConnection#connect() 79 */ 80 @Override 81 public void connect() throws IOException { 82 if (!connected) { 83 findJarFile(); // ensure the file can be found 84 findJarEntry(); // ensure the entry, if any, can be found 85 connected = true; 86 } 87 } 88 89 /** 90 * Returns the Jar file referred by this {@code URLConnection}. 91 * 92 * @return the JAR file referenced by this connection 93 * 94 * @throws IOException 95 * thrown if an IO error occurs while connecting to the 96 * resource. 97 */ 98 @Override 99 public JarFile getJarFile() throws IOException { 100 connect(); 101 return jarFile; 102 } 103 104 /** 105 * Returns the Jar file referred by this {@code URLConnection} 106 * 107 * @throws IOException 108 * if an IO error occurs while connecting to the resource. 109 */ 110 private void findJarFile() throws IOException { 111 JarFile jar = null; 112 if (getUseCaches()) { 113 synchronized (jarCache) { 114 jarFile = jarCache.get(jarFileURL); 115 } 116 if (jarFile == null) { 117 jar = openJarFile(); 118 synchronized (jarCache) { 119 jarFile = jarCache.get(jarFileURL); 120 if (jarFile == null) { 121 jarCache.put(jarFileURL, jar); 122 jarFile = jar; 123 } else { 124 jar.close(); 125 } 126 } 127 } 128 } else { 129 jarFile = openJarFile(); 130 } 131 132 if (jarFile == null) { 133 throw new IOException(); 134 } 135 } 136 137 JarFile openJarFile() throws IOException { 138 if (jarFileURL.getProtocol().equals("file")) { 139 String decodedFile = UriCodec.decode(jarFileURL.getFile()); 140 return new JarFile(new File(decodedFile), true, ZipFile.OPEN_READ); 141 } else { 142 final InputStream is = jarFileURL.openConnection().getInputStream(); 143 try { 144 FileOutputStream fos = null; 145 JarFile result = null; 146 try { 147 File tempJar = File.createTempFile("hyjar_", ".tmp", null); 148 tempJar.deleteOnExit(); 149 fos = new FileOutputStream(tempJar); 150 byte[] buf = new byte[4096]; 151 int nbytes = 0; 152 while ((nbytes = is.read(buf)) > -1) { 153 fos.write(buf, 0, nbytes); 154 } 155 fos.close(); 156 return new JarFile(tempJar, true, ZipFile.OPEN_READ | ZipFile.OPEN_DELETE); 157 } catch (IOException e) { 158 return null; 159 } finally { 160 if (fos != null) { 161 try { 162 fos.close(); 163 } catch (IOException ex) { 164 return null; 165 } 166 } 167 } 168 } finally { 169 if (is != null) { 170 is.close(); 171 } 172 } 173 } 174 } 175 176 /** 177 * Returns the JarEntry of the entry referenced by this {@code 178 * URLConnection}. 179 * 180 * @return the JarEntry referenced 181 * 182 * @throws IOException 183 * if an IO error occurs while getting the entry 184 */ 185 @Override 186 public JarEntry getJarEntry() throws IOException { 187 connect(); 188 return jarEntry; 189 190 } 191 192 /** 193 * Look up the JarEntry of the entry referenced by this {@code 194 * URLConnection}. 195 */ 196 private void findJarEntry() throws IOException { 197 if (getEntryName() == null) { 198 return; 199 } 200 jarEntry = jarFile.getJarEntry(getEntryName()); 201 if (jarEntry == null) { 202 throw new FileNotFoundException(getEntryName()); 203 } 204 } 205 206 /** 207 * Creates an input stream for reading from this URL Connection. 208 * 209 * @return the input stream 210 * 211 * @throws IOException 212 * if an IO error occurs while connecting to the resource. 213 */ 214 @Override 215 public InputStream getInputStream() throws IOException { 216 if (closed) { 217 throw new IllegalStateException("JarURLConnection InputStream has been closed"); 218 } 219 connect(); 220 if (jarInput != null) { 221 return jarInput; 222 } 223 if (jarEntry == null) { 224 throw new IOException("Jar entry not specified"); 225 } 226 return jarInput = new JarURLConnectionInputStream(jarFile 227 .getInputStream(jarEntry), jarFile); 228 } 229 230 /** 231 * Returns the content type of the resource. For jar file itself 232 * "x-java/jar" should be returned, for jar entries the content type of the 233 * entry should be returned. Returns non-null results ("content/unknown" for 234 * unknown types). 235 * 236 * @return the content type 237 */ 238 @Override 239 public String getContentType() { 240 if (url.getFile().endsWith("!/")) { 241 // the type for jar file itself is always "x-java/jar" 242 return "x-java/jar"; 243 } 244 String cType = null; 245 String entryName = getEntryName(); 246 247 if (entryName != null) { 248 // if there is an Jar Entry, get the content type from the name 249 cType = guessContentTypeFromName(entryName); 250 } else { 251 try { 252 connect(); 253 cType = jarFileURLConnection.getContentType(); 254 } catch (IOException ioe) { 255 // Ignore 256 } 257 } 258 if (cType == null) { 259 cType = "content/unknown"; 260 } 261 return cType; 262 } 263 264 /** 265 * Returns the content length of the resource. Test cases reveal that if the 266 * URL is referring to a Jar file, this method answers a content-length 267 * returned by URLConnection. For jar entry it should return it's size. 268 * Otherwise, it will return -1. 269 * 270 * @return the content length 271 */ 272 @Override 273 public int getContentLength() { 274 try { 275 connect(); 276 if (jarEntry == null) { 277 return jarFileURLConnection.getContentLength(); 278 } 279 return (int) getJarEntry().getSize(); 280 } catch (IOException e) { 281 // Ignored 282 } 283 return -1; 284 } 285 286 /** 287 * Returns the object pointed by this {@code URL}. If this URLConnection is 288 * pointing to a Jar File (no Jar Entry), this method will return a {@code 289 * JarFile} If there is a Jar Entry, it will return the object corresponding 290 * to the Jar entry content type. 291 * 292 * @return a non-null object 293 * 294 * @throws IOException 295 * if an IO error occurred 296 * 297 * @see ContentHandler 298 * @see ContentHandlerFactory 299 * @see java.io.IOException 300 * @see #setContentHandlerFactory(ContentHandlerFactory) 301 */ 302 @Override 303 public Object getContent() throws IOException { 304 connect(); 305 // if there is no Jar Entry, return a JarFile 306 if (jarEntry == null) { 307 return jarFile; 308 } 309 return super.getContent(); 310 } 311 312 /** 313 * Returns the permission, in this case the subclass, FilePermission object 314 * which represents the permission necessary for this URLConnection to 315 * establish the connection. 316 * 317 * @return the permission required for this URLConnection. 318 * 319 * @throws IOException 320 * thrown when an IO exception occurs while creating the 321 * permission. 322 */ 323 324 @Override 325 public Permission getPermission() throws IOException { 326 return jarFileURLConnection.getPermission(); 327 } 328 329 @Override 330 public boolean getUseCaches() { 331 return jarFileURLConnection.getUseCaches(); 332 } 333 334 @Override 335 public void setUseCaches(boolean usecaches) { 336 jarFileURLConnection.setUseCaches(usecaches); 337 } 338 339 @Override 340 public boolean getDefaultUseCaches() { 341 return jarFileURLConnection.getDefaultUseCaches(); 342 } 343 344 @Override 345 public void setDefaultUseCaches(boolean defaultusecaches) { 346 jarFileURLConnection.setDefaultUseCaches(defaultusecaches); 347 } 348 349 /** 350 * Closes the cached files. 351 */ 352 public static void closeCachedFiles() { 353 Set<Map.Entry<URL, JarFile>> s = jarCache.entrySet(); 354 synchronized (jarCache) { 355 Iterator<Map.Entry<URL, JarFile>> i = s.iterator(); 356 while (i.hasNext()) { 357 try { 358 ZipFile zip = i.next().getValue(); 359 if (zip != null) { 360 zip.close(); 361 } 362 } catch (IOException e) { 363 // Ignored 364 } 365 } 366 } 367 } 368 369 private class JarURLConnectionInputStream extends FilterInputStream { 370 final JarFile jarFile; 371 372 protected JarURLConnectionInputStream(InputStream in, JarFile file) { 373 super(in); 374 jarFile = file; 375 } 376 377 @Override 378 public void close() throws IOException { 379 super.close(); 380 if (!getUseCaches()) { 381 closed = true; 382 jarFile.close(); 383 } 384 } 385 } 386 } 387