/*
* Copyright 2004-2010 Brian S O'Neill
*
* Licensed 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.cojen.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.cojen.classfile.ClassFile;
/**
* ClassInjector allows transient classes to be loaded, where a transient class
* is defined as being dynamically created at runtime. Unless explicit, the
* name given to transient classes is randomly assigned to prevent name
* collisions and to discourage referencing the classname persistently outside
* the runtime environment.
* <p>
* Classes defined by ClassInjector may be unloaded, if no references to it
* exist. Once unloaded, they cannot be loaded again by name since the
* original bytecode was never preserved.
* <p>
* Debugging can be enabled via the java command-line option
* "-Dorg.cojen.util.ClassInjector.DEBUG=true". This causes all generated classes
* to be written to the temp directory, and a message is written to System.out
* indicating exactly where.
*
* @author Brian S O'Neill
* @deprecated use {@link org.cojen.classfile.RuntimeClassFile}
*/
@Deprecated
public class ClassInjector {
private static final boolean DEBUG;
static {
DEBUG =
Boolean.getBoolean("org.cojen.classfile.RuntimeClassFile.DEBUG") ||
Boolean.getBoolean("org.cojen.util.ClassInjector.DEBUG") ||
Boolean.getBoolean("cojen.util.ClassInjector.DEBUG");
}
private static final Random cRandom = new Random();
// Weakly maps ClassLoaders to softly referenced internal ClassLoaders.
private static Map cLoaders = new WeakIdentityMap();
/**
* Create a ClassInjector for defining one class. The parent ClassLoader
* used is the one which loaded the ClassInjector class.
*/
public static ClassInjector create() {
return create(null, null);
}
/**
* Create a ClassInjector for defining one class. The prefix is optional,
* which is used as the start of the auto-generated class name. If the
* parent ClassLoader is not specified, it will default to the ClassLoader of
* the ClassInjector class.
* <p>
* If the parent loader was used for loading injected classes, the new
* class will be loaded by it. This allows auto-generated classes access to
* package accessible members, as long as they are defined in the same
* package.
*
* @param prefix optional class name prefix
* @param parent optional parent ClassLoader
*/
public static ClassInjector create(String prefix, ClassLoader parent) {
return create(prefix, parent, false);
}
/**
* Create a ClassInjector for defining one class with an explicit name. If
* the parent ClassLoader is not specified, it will default to the
* ClassLoader of the ClassInjector class.
* <p>
* If the parent loader was used for loading injected classes, the new
* class will be loaded by it. This allows auto-generated classes access to
* package accessible members, as long as they are defined in the same
* package.
*
* @param name required class name
* @param parent optional parent ClassLoader
* @throws IllegalArgumentException if name is null
*/
public static ClassInjector createExplicit(String name, ClassLoader parent) {
if (name == null) {
throw new IllegalArgumentException("Explicit class name not provided");
}
return create(name, parent, true);
}
private static ClassInjector create(String prefix, ClassLoader parent, boolean explicit) {
if (prefix == null) {
prefix = ClassInjector.class.getName();
}
if (parent == null) {
parent = ClassInjector.class.getClassLoader();
if (parent == null) {
parent = ClassLoader.getSystemClassLoader();
}
}
String name = explicit ? prefix : null;
Loader loader;
synchronized (cRandom) {
getLoader: {
if (parent instanceof Loader) {
// Use the same loader, allowing the new class access to
// same package protected members.
loader = (Loader) parent;
break getLoader;
}
SoftReference ref = (SoftReference) cLoaders.get(parent);
if (ref != null) {
loader = (Loader) ref.get();
if (loader != null && loader.isValid()) {
break getLoader;
}
ref.clear();
}
loader = parent == null ? new Loader() : new Loader(parent);
cLoaders.put(parent, new SoftReference(loader));
}
if (explicit) {
reserveCheck: {
for (int i=0; i<2; i++) {
if (loader.reserveName(name)) {
try {
loader.loadClass(name);
} catch (ClassNotFoundException e) {
break reserveCheck;
}
}
if (i > 0) {
throw new IllegalStateException
("Class name already reserved: " + name);
}
// Make a new loader and try again.
loader = parent == null ? new Loader() : new Loader(parent);
}
// Save new loader.
cLoaders.put(parent, new SoftReference(loader));
}
} else {
for (int tryCount = 0; tryCount < 1000; tryCount++) {
name = null;
long ID = cRandom.nextInt();
// Use a small identifier if possible, making it easier to read
// stack traces and decompiled classes.
switch (tryCount) {
case 0:
ID &= 0xffL;
break;
case 1:
ID &= 0xffffL;
break;
default:
ID &= 0xffffffffL;
break;
}
name = prefix + '$' + ID;
if (!loader.reserveName(name)) {
continue;
}
try {
loader.loadClass(name);
} catch (ClassNotFoundException e) {
break;
} catch (LinkageError e) {
}
}
}
}
if (name == null) {
throw new InternalError("Unable to create unique class name");
}
return new ClassInjector(name, loader);
}
private final String mName;
private final Loader mLoader;
private ByteArrayOutputStream mData;
private Class mClass;
private ClassInjector(String name, Loader loader) {
mName = name;
mLoader = loader;
}
/**
* Returns the name that must be given to the new class.
*/
public String getClassName() {
return mName;
}
/**
* Open a stream to define the new class into.
*
* @throws IllegalStateException if new class has already been defined
* or if a stream has already been opened
*/
public OutputStream openStream() throws IllegalStateException {
if (mClass != null) {
throw new IllegalStateException("New class has already been defined");
}
ByteArrayOutputStream data = mData;
if (data != null) {
throw new IllegalStateException("Stream already opened");
}
mData = data = new ByteArrayOutputStream();
return data;
}
/**
* Define the new class from a ClassFile object.
*
* @return the newly created class
* @throws IllegalStateException if new class has already been defined
* or if a stream has already been opened
*/
public Class defineClass(ClassFile cf) {
try {
cf.writeTo(openStream());
} catch (IOException e) {
throw new InternalError(e.toString());
}
return getNewClass();
}
/**
* Returns the newly defined class.
*
* @throws IllegalStateException if class was never defined
*/
public Class getNewClass() throws IllegalStateException, ClassFormatError {
if (mClass != null) {
return mClass;
}
ByteArrayOutputStream data = mData;
if (data == null) {
throw new IllegalStateException("Class not defined yet");
}
byte[] bytes = data.toByteArray();
if (DEBUG) {
File file = new File(mName.replace('.', '/') + ".class");
try {
File tempDir = new File(System.getProperty("java.io.tmpdir"));
file = new File(tempDir, file.getPath());
} catch (SecurityException e) {
}
try {
file.getParentFile().mkdirs();
System.out.println("ClassInjector writing to " + file);
OutputStream out = new FileOutputStream(file);
out.write(bytes);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
mClass = mLoader.define(mName, bytes);
mData = null;
return mClass;
}
private static final class Loader extends ClassLoader {
private Set mReservedNames = new HashSet();
Loader(ClassLoader parent) {
super(parent);
}
Loader() {
super();
}
// Prevent name collisions while multiple threads are injecting classes
// by reserving the name.
synchronized boolean reserveName(String name) {
return mReservedNames.add(name);
}
synchronized boolean isValid() {
// Only use loader for 100 injections, to facilitate class
// unloading.
return mReservedNames.size() < 100;
}
Class define(String name, byte[] b) {
Class clazz = defineClass(name, b, 0, b.length);
resolveClass(clazz);
return clazz;
}
}
}