blob: 754cedf79dde0f5512d55c2b34cd16e7abc9f01a [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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 com.android.ide.eclipse.adt.internal.sdk;
import com.android.SdkConstants;
import com.google.common.io.Closeables;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.management.InvalidAttributeValueException;
/**
* Custom class loader able to load a class from the SDK jar file.
*/
public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader {
/**
* Wrapper around a {@link Class} to provide the methods of
* {@link IAndroidClassLoader.IClassDescriptor}.
*/
public final static class ClassWrapper implements IClassDescriptor {
private Class<?> mClass;
public ClassWrapper(Class<?> clazz) {
mClass = clazz;
}
@Override
public String getFullClassName() {
return mClass.getCanonicalName();
}
@Override
public IClassDescriptor[] getDeclaredClasses() {
Class<?>[] classes = mClass.getDeclaredClasses();
IClassDescriptor[] iclasses = new IClassDescriptor[classes.length];
for (int i = 0 ; i < classes.length ; i++) {
iclasses[i] = new ClassWrapper(classes[i]);
}
return iclasses;
}
@Override
public IClassDescriptor getEnclosingClass() {
return new ClassWrapper(mClass.getEnclosingClass());
}
@Override
public String getSimpleName() {
return mClass.getSimpleName();
}
@Override
public IClassDescriptor getSuperclass() {
return new ClassWrapper(mClass.getSuperclass());
}
@Override
public boolean equals(Object clazz) {
if (clazz instanceof ClassWrapper) {
return mClass.equals(((ClassWrapper)clazz).mClass);
}
return super.equals(clazz);
}
@Override
public int hashCode() {
return mClass.hashCode();
}
@Override
public boolean isInstantiable() {
int modifiers = mClass.getModifiers();
return Modifier.isAbstract(modifiers) == false && Modifier.isPublic(modifiers) == true;
}
public Class<?> wrappedClass() {
return mClass;
}
}
private String mOsFrameworkLocation;
/** A cache for binary data extracted from the zip */
private final HashMap<String, byte[]> mEntryCache = new HashMap<String, byte[]>();
/** A cache for already defined Classes */
private final HashMap<String, Class<?> > mClassCache = new HashMap<String, Class<?> >();
/**
* Creates the class loader by providing the os path to the framework jar archive
*
* @param osFrameworkLocation OS Path of the framework JAR file
*/
public AndroidJarLoader(String osFrameworkLocation) {
super();
mOsFrameworkLocation = osFrameworkLocation;
}
@Override
public String getSource() {
return mOsFrameworkLocation;
}
/**
* Pre-loads all class binary data that belong to the given package by reading the archive
* once and caching them internally.
* <p/>
* This does not actually preload "classes", it just reads the unzipped bytes for a given
* class. To obtain a class, one must call {@link #findClass(String)} later.
* <p/>
* All classes which package name starts with "packageFilter" will be included and can be
* found later.
* <p/>
* May throw some exceptions if the framework JAR cannot be read.
*
* @param packageFilter The package that contains all the class data to preload, using a fully
* qualified binary name (.e.g "com.my.package."). The matching algorithm
* is simple "startsWith". Use an empty string to include everything.
* @param taskLabel An optional task name for the sub monitor. Can be null.
* @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
* @throws IOException
* @throws InvalidAttributeValueException
* @throws ClassFormatError
*/
public void preLoadClasses(String packageFilter, String taskLabel, IProgressMonitor monitor)
throws IOException, InvalidAttributeValueException, ClassFormatError {
// Transform the package name into a zip entry path
String pathFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
SubMonitor progress = SubMonitor.convert(monitor, taskLabel == null ? "" : taskLabel, 100);
// create streams to read the intermediary archive
FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
ZipInputStream zis = new ZipInputStream(fis);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// get the name of the entry.
String entryPath = entry.getName();
if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) {
// only accept class files
continue;
}
// check if it is part of the package to preload
if (pathFilter.length() > 0 && !entryPath.startsWith(pathFilter)) {
continue;
}
String className = entryPathToClassName(entryPath);
if (!mEntryCache.containsKey(className)) {
long entrySize = entry.getSize();
if (entrySize > Integer.MAX_VALUE) {
throw new InvalidAttributeValueException();
}
byte[] data = readZipData(zis, (int)entrySize);
mEntryCache.put(className, data);
}
// advance 5% of whatever is allocated on the progress bar
progress.setWorkRemaining(100);
progress.worked(5);
progress.subTask(String.format("Preload %1$s", className));
}
}
/**
* Finds and loads all classes that derive from a given set of super classes.
* <p/>
* As a side-effect this will load and cache most, if not all, classes in the input JAR file.
*
* @param packageFilter Base name of package of classes to find.
* Use an empty string to find everyting.
* @param superClasses The super classes of all the classes to find.
* @return An hash map which keys are the super classes looked for and which values are
* ArrayList of the classes found. The array lists are always created for all the
* valid keys, they are simply empty if no deriving class is found for a given
* super class.
* @throws IOException
* @throws InvalidAttributeValueException
* @throws ClassFormatError
*/
@SuppressWarnings("resource") // Eclipse doesn't understand Closeables.closeQuietly
@Override
public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(
String packageFilter,
String[] superClasses)
throws IOException, InvalidAttributeValueException, ClassFormatError {
packageFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
HashMap<String, ArrayList<IClassDescriptor>> mClassesFound =
new HashMap<String, ArrayList<IClassDescriptor>>();
for (String className : superClasses) {
mClassesFound.put(className, new ArrayList<IClassDescriptor>());
}
// create streams to read the intermediary archive
FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
ZipInputStream zis = new ZipInputStream(fis);
try {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// get the name of the entry and convert to a class binary name
String entryPath = entry.getName();
if (!entryPath.endsWith(SdkConstants.DOT_CLASS)) {
// only accept class files
continue;
}
if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) {
// only accept stuff from the requested root package.
continue;
}
String className = entryPathToClassName(entryPath);
Class<?> loaded_class = mClassCache.get(className);
if (loaded_class == null) {
byte[] data = mEntryCache.get(className);
if (data == null) {
// Get the class and cache it
long entrySize = entry.getSize();
if (entrySize > Integer.MAX_VALUE) {
throw new InvalidAttributeValueException();
}
data = readZipData(zis, (int)entrySize);
}
try {
loaded_class = defineAndCacheClass(className, data);
} catch (NoClassDefFoundError error) {
if (error.getMessage().startsWith("java/")) {
// Can't define these; we just need to stop
// iteration here
continue;
}
throw error;
}
}
for (Class<?> superClass = loaded_class.getSuperclass();
superClass != null;
superClass = superClass.getSuperclass()) {
String superName = superClass.getCanonicalName();
if (mClassesFound.containsKey(superName)) {
mClassesFound.get(superName).add(new ClassWrapper(loaded_class));
break;
}
}
}
} finally {
Closeables.closeQuietly(zis);
}
return mClassesFound;
}
/** Helper method that converts a Zip entry path into a corresponding
* Java full qualified binary class name.
* <p/>
* F.ex, this converts "com/my/package/Foo.class" into "com.my.package.Foo".
*/
private String entryPathToClassName(String entryPath) {
return entryPath.replaceFirst("\\.class$", "").replaceAll("[/\\\\]", "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
/**
* Finds the class with the specified binary name.
*
* {@inheritDoc}
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// try to find the class in the cache
Class<?> cached_class = mClassCache.get(name);
if (cached_class == ClassNotFoundException.class) {
// we already know we can't find this class, don't try again
throw new ClassNotFoundException(name);
} else if (cached_class != null) {
return cached_class;
}
// if not found, look it up and cache it
byte[] data = loadClassData(name);
if (data != null) {
return defineAndCacheClass(name, data);
} else {
// if the class can't be found, record a CNFE class in the map so
// that we don't try to reload it next time
mClassCache.put(name, ClassNotFoundException.class);
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
throw e;
} catch (Exception e) {
throw new ClassNotFoundException(e.getMessage());
}
}
/**
* Defines a class based on its binary data and caches the resulting class object.
*
* @param name The binary name of the class (i.e. package.class1$class2)
* @param data The binary data from the loader.
* @return The class defined
* @throws ClassFormatError if defineClass failed.
*/
private Class<?> defineAndCacheClass(String name, byte[] data) throws ClassFormatError {
Class<?> cached_class;
cached_class = defineClass(null, data, 0, data.length);
if (cached_class != null) {
// Add new class to the cache class and remove it from the zip entry data cache
mClassCache.put(name, cached_class);
mEntryCache.remove(name);
}
return cached_class;
}
/**
* Loads a class data from its binary name.
* <p/>
* This uses the class binary data that has been preloaded earlier by the preLoadClasses()
* method if possible.
*
* @param className the binary name
* @return an array of bytes representing the class data or null if not found
* @throws InvalidAttributeValueException
* @throws IOException
*/
private synchronized byte[] loadClassData(String className)
throws InvalidAttributeValueException, IOException {
byte[] data = mEntryCache.get(className);
if (data != null) {
return data;
}
// The name is a binary name. Something like "android.R", or "android.R$id".
// Make a path out of it.
String entryName = className.replaceAll("\\.", "/") + SdkConstants.DOT_CLASS; //$NON-NLS-1$ //$NON-NLS-2$
// create streams to read the intermediary archive
FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
ZipInputStream zis = new ZipInputStream(fis);
try {
// loop on the entries of the intermediary package and put them in the final package.
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
// get the name of the entry.
String currEntryName = entry.getName();
if (currEntryName.equals(entryName)) {
long entrySize = entry.getSize();
if (entrySize > Integer.MAX_VALUE) {
throw new InvalidAttributeValueException();
}
data = readZipData(zis, (int)entrySize);
return data;
}
}
return null;
} finally {
zis.close();
}
}
/**
* Reads data for the <em>current</em> entry from the zip input stream.
*
* @param zis The Zip input stream
* @param entrySize The entry size. -1 if unknown.
* @return The new data for the <em>current</em> entry.
* @throws IOException If ZipInputStream.read() fails.
*/
private byte[] readZipData(ZipInputStream zis, int entrySize) throws IOException {
int block_size = 1024;
int data_size = entrySize < 1 ? block_size : entrySize;
int offset = 0;
byte[] data = new byte[data_size];
while(zis.available() != 0) {
int count = zis.read(data, offset, data_size - offset);
if (count < 0) { // read data is done
break;
}
offset += count;
if (entrySize >= 1 && offset >= entrySize) { // we know the size and we're done
break;
}
// if we don't know the entry size and we're not done reading,
// expand the data buffer some more.
if (offset >= data_size) {
byte[] temp = new byte[data_size + block_size];
System.arraycopy(data, 0, temp, 0, data_size);
data_size += block_size;
data = temp;
block_size *= 2;
}
}
if (offset < data_size) {
// buffer was allocated too large, trim it
byte[] temp = new byte[offset];
if (offset > 0) {
System.arraycopy(data, 0, temp, 0, offset);
}
data = temp;
}
return data;
}
/**
* Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name.
* @param className the fully-qualified name of the class to return.
* @throws ClassNotFoundException
*/
@Override
public IClassDescriptor getClass(String className) throws ClassNotFoundException {
try {
return new ClassWrapper(loadClass(className));
} catch (ClassNotFoundException e) {
throw e; // useful for debugging
}
}
}