blob: 5f4896a3e43605da3dc0bbfcd2bcbbeef7de4daf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.harmony.luni.internal.net.www.protocol.jar;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.ContentHandler;
import java.net.ContentHandlerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.Permission;
import java.security.PrivilegedAction;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
// BEGIN android-removed
// import org.apache.harmony.kernel.vm.VM;
// END android-removed
import org.apache.harmony.luni.util.Msg;
import org.apache.harmony.luni.util.Util;
/**
* This subclass extends <code>URLConnection</code>.
* <p>
*
* This class is responsible for connecting and retrieving resources from a Jar
* file which can be anywhere that can be refered to by an URL.
*/
public class JarURLConnection extends java.net.JarURLConnection {
static Hashtable<String, CacheEntry<? extends JarFile>> jarCache = new Hashtable<String, CacheEntry<?>>();
InputStream jarInput;
private JarFile jarFile;
private JarEntry jarEntry;
private boolean closed;
ReferenceQueue<JarFile> cacheQueue = new ReferenceQueue<JarFile>();
static TreeSet<LRUKey> lru = new TreeSet<LRUKey>(
new LRUComparator<LRUKey>());
static int Limit;
static {
Limit = AccessController.doPrivileged(new PrivilegedAction<Integer>() {
public Integer run() {
return Integer.getInteger("jar.cacheSize", 500); //$NON-NLS-1$
}
});
// BEGIN android-removed
// TODO this needs to be implemented once this is available.
// VM.closeJars();
// END android-removed
}
static final class CacheEntry<T extends JarFile> extends WeakReference<T> {
Object key;
CacheEntry(T jar, String key, ReferenceQueue<JarFile> queue) {
super(jar, queue);
this.key = key;
}
}
static final class LRUKey {
JarFile jar;
long ts;
LRUKey(JarFile file, long time) {
jar = file;
ts = time;
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
return (obj instanceof LRUKey) &&
(jar == ((LRUKey) obj).jar);
}
@Override
public int hashCode() {
return jar.hashCode();
}
}
static final class LRUComparator<T> implements Comparator<LRUKey> {
LRUComparator() {
}
/**
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(LRUKey o1, LRUKey o2) {
if ((o1).ts > (o2).ts) {
return 1;
}
return (o1).ts == (o2).ts ? 0 : -1;
}
/**
* @param o1
* an object to compare
* @param o2
* an object to compare
* @return <code>true</code> if the objects are equal,
* <code>false</code> otherwise.
*/
public boolean equals(Object o1, Object o2) {
return o1.equals(o2);
}
}
/**
* @param url
* the URL of the JAR
* @throws MalformedURLException
* if the URL is malformed
*/
public JarURLConnection(java.net.URL url) throws MalformedURLException {
super(url);
}
/**
* @see java.net.URLConnection#connect()
*/
@Override
public void connect() throws IOException {
jarFileURLConnection = getJarFileURL().openConnection();
findJarFile(); // ensure the file can be found
findJarEntry(); // ensure the entry, if any, can be found
connected = true;
}
/**
* Returns the Jar file refered by this <code>URLConnection</code>
*
* @return the JAR file referenced by this connection
*
* @throws IOException
* thrown if an IO error occurs while connecting to the
* resource.
*/
@Override
public JarFile getJarFile() throws IOException {
if (!connected) {
connect();
}
return jarFile;
}
/**
* Returns the Jar file refered by this <code>URLConnection</code>
*
* @throws IOException
* if an IO error occurs while connecting to the resource.
*/
private void findJarFile() throws IOException {
URL jarFileURL = getJarFileURL();
if (jarFileURL.getProtocol().equals("file")) { //$NON-NLS-1$
String fileName = jarFileURL.getFile();
if(!new File(Util.decode(fileName,false)).exists()){
// KA026=JAR entry {0} not found in {1}
throw new FileNotFoundException(Msg.getString("KA026", //$NON-NLS-1$
getEntryName(), fileName));
}
String host = jarFileURL.getHost();
if (host != null && host.length() > 0) {
fileName = "//" + host + fileName; //$NON-NLS-1$
}
jarFile = openJarFile(fileName, fileName, false);
return;
}
final String externalForm = jarFileURLConnection.getURL()
.toExternalForm();
jarFile = AccessController
.doPrivileged(new PrivilegedAction<JarFile>() {
public JarFile run() {
try {
return openJarFile(null, externalForm, false);
} catch (IOException e) {
return null;
}
}
});
if (jarFile != null) {
return;
}
// Build a temp jar file
final InputStream is = jarFileURLConnection.getInputStream();
try {
jarFile = AccessController
.doPrivileged(new PrivilegedAction<JarFile>() {
public JarFile run() {
try {
File tempJar = File.createTempFile("hyjar_", //$NON-NLS-1$
".tmp", null); //$NON-NLS-1$
FileOutputStream fos = new FileOutputStream(
tempJar);
byte[] buf = new byte[4096];
int nbytes = 0;
while ((nbytes = is.read(buf)) > -1) {
fos.write(buf, 0, nbytes);
}
fos.close();
String path = tempJar.getPath();
return openJarFile(path, externalForm, true);
} catch (IOException e) {
return null;
}
}
});
} finally {
is.close();
}
if (jarFile == null) {
throw new IOException();
}
}
JarFile openJarFile(String fileString, String key, boolean temp)
throws IOException {
JarFile jar = null;
if (useCaches) {
CacheEntry<? extends JarFile> entry;
while ((entry = (CacheEntry<? extends JarFile>) cacheQueue.poll()) != null) {
jarCache.remove(entry.key);
}
entry = jarCache.get(key);
if (entry != null) {
jar = entry.get();
}
if (jar == null && fileString != null) {
int flags = ZipFile.OPEN_READ
+ (temp ? ZipFile.OPEN_DELETE : 0);
jar = new JarFile(new File(Util.decode(fileString, false)),
true, flags);
jarCache
.put(key, new CacheEntry<JarFile>(jar, key, cacheQueue));
} else {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkPermission(getPermission());
}
if (temp) {
lru.remove(new LRUKey(jar, 0));
}
}
} else if (fileString != null) {
int flags = ZipFile.OPEN_READ + (temp ? ZipFile.OPEN_DELETE : 0);
jar = new JarFile(new File(Util.decode(fileString, false)), true,
flags);
}
if (temp) {
lru.add(new LRUKey(jar, new Date().getTime()));
if (lru.size() > Limit) {
lru.remove(lru.first());
}
}
return jar;
}
/**
* Returns the JarEntry of the entry referenced by this
* <code>URLConnection</code>.
*
* @return java.util.jar.JarEntry the JarEntry referenced
*
* @throws IOException
* if an IO error occurs while getting the entry
*/
@Override
public JarEntry getJarEntry() throws IOException {
if (!connected) {
connect();
}
return jarEntry;
}
/**
* Look up the JarEntry of the entry referenced by this
* <code>URLConnection</code>.
*/
private void findJarEntry() throws IOException {
if (getEntryName() == null) {
return;
}
jarEntry = jarFile.getJarEntry(getEntryName());
if (jarEntry == null) {
throw new FileNotFoundException(getEntryName());
}
}
/**
* Creates an input stream for reading from this URL Connection.
*
* @return the input stream
*
* @throws IOException
* if an IO error occurs while connecting to the resource.
*/
@Override
public InputStream getInputStream() throws IOException {
if (closed) {
throw new IllegalStateException(Msg.getString("KA027"));
}
if (!connected) {
connect();
}
if (jarInput != null) {
return jarInput;
}
if (jarEntry == null) {
throw new IOException(Msg.getString("K00fc")); //$NON-NLS-1$
}
return jarInput = new JarURLConnectionInputStream(jarFile
.getInputStream(jarEntry), jarFile);
}
/**
* Returns the content type of the resource. Test cases reveal that only if
* the URL is refering to a Jar file, that this method returns a non-null
* value - x-java/jar.
*
* @return the content type
*/
@Override
public String getContentType() {
// it could also return "x-java/jar" which jdk returns but here, we get
// it from the URLConnection
try {
if (url.getFile().endsWith("!/")) { //$NON-NLS-1$
return getJarFileURL().openConnection().getContentType();
}
} catch (IOException ioe) {
// Ignore
}
// if there is an Jar Entry, get the content type from the name
return guessContentTypeFromName(url.getFile());
}
/**
* Returns the content length of the resource. Test cases reveal that if the
* URL is refering to a Jar file, this method returns a content-length
* returned by URLConnection. If not, it will return -1.
*
* @return the content length
*/
@Override
public int getContentLength() {
try {
if (url.getFile().endsWith("!/")) { //$NON-NLS-1$
return getJarFileURL().openConnection().getContentLength();
}
} catch (IOException e) {
//Ignored
}
return -1;
}
/**
* Returns the object pointed by this <code>URL</code>. If this
* URLConnection is pointing to a Jar File (no Jar Entry), this method will
* return a <code>JarFile</code> If there is a Jar Entry, it will return
* the object corresponding to the Jar entry content type.
*
* @return a non-null object
*
* @throws IOException
* if an IO error occured
*
* @see ContentHandler
* @see ContentHandlerFactory
* @see java.io.IOException
* @see #setContentHandlerFactory(ContentHandlerFactory)
*/
@Override
public Object getContent() throws IOException {
if (!connected) {
connect();
}
// if there is no Jar Entry, return a JarFile
if (jarEntry == null) {
return jarFile;
}
return super.getContent();
}
/**
* Returns the permission, in this case the subclass, FilePermission object
* which represents the permission necessary for this URLConnection to
* establish the connection.
*
* @return the permission required for this URLConnection.
*
* @throws IOException
* thrown when an IO exception occurs while creating the
* permission.
*/
@Override
public Permission getPermission() throws IOException {
if (jarFileURLConnection != null) {
return jarFileURLConnection.getPermission();
}
return getJarFileURL().openConnection().getPermission();
}
/**
* Closes the cached files.
*/
public static void closeCachedFiles() {
Enumeration<CacheEntry<? extends JarFile>> elemEnum = jarCache
.elements();
while (elemEnum.hasMoreElements()) {
try {
ZipFile zip = elemEnum.nextElement().get();
if (zip != null) {
zip.close();
}
} catch (IOException e) {
// Ignored
}
}
}
private class JarURLConnectionInputStream extends FilterInputStream {
InputStream inputStream;
JarFile jarFile;
protected JarURLConnectionInputStream(InputStream in, JarFile file) {
super(in);
inputStream = in;
jarFile = file;
}
@Override
public void close() throws IOException {
super.close();
if (!useCaches) {
closed = true;
jarFile.close();
}
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public int read(byte[] buf, int off, int nbytes) throws IOException {
return inputStream.read(buf, off, nbytes);
}
@Override
public long skip(long nbytes) throws IOException {
return inputStream.skip(nbytes);
}
}
}