001: /*
002: * Copyright 2004 Brian S O'Neill
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016:
017: package org.cojen.util;
018:
019: import java.io.ByteArrayOutputStream;
020: import java.io.File;
021: import java.io.FileOutputStream;
022: import java.io.IOException;
023: import java.io.OutputStream;
024: import java.lang.ref.SoftReference;
025: import java.util.HashSet;
026: import java.util.Map;
027: import java.util.Random;
028: import java.util.Set;
029:
030: import org.cojen.classfile.ClassFile;
031:
032: /**
033: * ClassInjector allows transient classes to be loaded, where a transient class
034: * is defined as being dynamically created at runtime. Unless explicit, the
035: * name given to transient classes is randomly assigned to prevent name
036: * collisions and to discourage referencing the classname persistently outside
037: * the runtime environment.
038: * <p>
039: * Classes defined by ClassInjector may be unloaded, if no references to it
040: * exist. Once unloaded, they cannot be loaded again by name since the
041: * original bytecode was never preserved.
042: * <p>
043: * Debugging can be enabled via the java command-line option
044: * "-Dcojen.util.ClassInjector.DEBUG=true". This causes all generated classes
045: * to be written to the temp directory, and a message is written to System.out
046: * indicating exactly where.
047: *
048: * @author Brian S O'Neill
049: */
050: public class ClassInjector {
051: private static final boolean DEBUG;
052:
053: static {
054: DEBUG = Boolean
055: .getBoolean("org.cojen.util.ClassInjector.DEBUG");
056: }
057:
058: private static final Random cRandom = new Random();
059:
060: // Weakly maps ClassLoaders to softly referenced internal ClassLoaders.
061: private static Map cLoaders = new WeakIdentityMap();
062:
063: /**
064: * Create a ClassInjector for defining one class. The parent ClassLoader
065: * used is the one which loaded the ClassInjector class.
066: */
067: public static ClassInjector create() {
068: return create(null, null);
069: }
070:
071: /**
072: * Create a ClassInjector for defining one class. The prefix is optional,
073: * which is used as the start of the auto-generated class name. If the
074: * parent ClassLoader is not specified, it will default to the ClassLoader of
075: * the ClassInjector class.
076: * <p>
077: * If the parent loader was used for loading injected classes, the new
078: * class will be loaded by it. This allows auto-generated classes access to
079: * package accessible members, as long as they are defined in the same
080: * package.
081: *
082: * @param prefix optional class name prefix
083: * @param parent optional parent ClassLoader
084: */
085: public static ClassInjector create(String prefix, ClassLoader parent) {
086: return create(prefix, parent, false);
087: }
088:
089: /**
090: * Create a ClassInjector for defining one class with an explicit name. If
091: * the parent ClassLoader is not specified, it will default to the
092: * ClassLoader of the ClassInjector class.
093: * <p>
094: * If the parent loader was used for loading injected classes, the new
095: * class will be loaded by it. This allows auto-generated classes access to
096: * package accessible members, as long as they are defined in the same
097: * package.
098: *
099: * @param name required class name
100: * @param parent optional parent ClassLoader
101: * @throws IllegalArgumentException if name is null
102: */
103: public static ClassInjector createExplicit(String name,
104: ClassLoader parent) {
105: if (name == null) {
106: throw new IllegalArgumentException(
107: "Explicit class name not provided");
108: }
109: return create(name, parent, true);
110: }
111:
112: private static ClassInjector create(String prefix,
113: ClassLoader parent, boolean explicit) {
114: if (prefix == null) {
115: prefix = ClassInjector.class.getName();
116: }
117: if (parent == null) {
118: parent = ClassInjector.class.getClassLoader();
119: if (parent == null) {
120: parent = ClassLoader.getSystemClassLoader();
121: }
122: }
123:
124: String name = explicit ? prefix : null;
125: Loader loader;
126:
127: synchronized (cRandom) {
128: getLoader: {
129: if (parent instanceof Loader) {
130: // Use the same loader, allowing the new class access to
131: // same package protected members.
132: loader = (Loader) parent;
133: break getLoader;
134: }
135: SoftReference ref = (SoftReference) cLoaders
136: .get(parent);
137: if (ref != null) {
138: loader = (Loader) ref.get();
139: if (loader != null && loader.isValid()) {
140: break getLoader;
141: }
142: ref.clear();
143: }
144: loader = parent == null ? new Loader() : new Loader(
145: parent);
146: cLoaders.put(parent, new SoftReference(loader));
147: }
148:
149: if (explicit) {
150: reserveCheck: {
151: for (int i = 0; i < 2; i++) {
152: if (loader.reserveName(name)) {
153: try {
154: loader.loadClass(name);
155: } catch (ClassNotFoundException e) {
156: break reserveCheck;
157: }
158: }
159: if (i > 0) {
160: throw new IllegalStateException(
161: "Class name already reserved: "
162: + name);
163: }
164: // Make a new loader and try again.
165: loader = parent == null ? new Loader()
166: : new Loader(parent);
167: }
168:
169: // Save new loader.
170: cLoaders.put(parent, new SoftReference(loader));
171: }
172: } else {
173: for (int tryCount = 0; tryCount < 1000; tryCount++) {
174: name = null;
175:
176: long ID = cRandom.nextInt();
177:
178: // Use a small identifier if possible, making it easier to read
179: // stack traces and decompiled classes.
180: switch (tryCount) {
181: case 0:
182: ID &= 0xffL;
183: break;
184: case 1:
185: ID &= 0xffffL;
186: break;
187: default:
188: ID &= 0xffffffffL;
189: break;
190: }
191:
192: name = prefix + '$' + ID;
193:
194: if (!loader.reserveName(name)) {
195: continue;
196: }
197:
198: try {
199: loader.loadClass(name);
200: } catch (ClassNotFoundException e) {
201: break;
202: } catch (LinkageError e) {
203: }
204: }
205: }
206: }
207:
208: if (name == null) {
209: throw new InternalError(
210: "Unable to create unique class name");
211: }
212:
213: return new ClassInjector(name, loader);
214: }
215:
216: private final String mName;
217: private final Loader mLoader;
218:
219: private ByteArrayOutputStream mData;
220: private Class mClass;
221:
222: private ClassInjector(String name, Loader loader) {
223: mName = name;
224: mLoader = loader;
225: }
226:
227: /**
228: * Returns the name that must be given to the new class.
229: */
230: public String getClassName() {
231: return mName;
232: }
233:
234: /**
235: * Open a stream to define the new class into.
236: *
237: * @throws IllegalStateException if new class has already been defined
238: * or if a stream has already been opened
239: */
240: public OutputStream openStream() throws IllegalStateException {
241: if (mClass != null) {
242: throw new IllegalStateException(
243: "New class has already been defined");
244: }
245: ByteArrayOutputStream data = mData;
246: if (data != null) {
247: throw new IllegalStateException("Stream already opened");
248: }
249: mData = data = new ByteArrayOutputStream();
250: return data;
251: }
252:
253: /**
254: * Define the new class from a ClassFile object.
255: *
256: * @return the newly created class
257: * @throws IllegalStateException if new class has already been defined
258: * or if a stream has already been opened
259: */
260: public Class defineClass(ClassFile cf) {
261: try {
262: cf.writeTo(openStream());
263: } catch (IOException e) {
264: throw new InternalError(e.toString());
265: }
266: return getNewClass();
267: }
268:
269: /**
270: * Returns the newly defined class.
271: *
272: * @throws IllegalStateException if class was never defined
273: */
274: public Class getNewClass() throws IllegalStateException,
275: ClassFormatError {
276: if (mClass != null) {
277: return mClass;
278: }
279: ByteArrayOutputStream data = mData;
280: if (data == null) {
281: throw new IllegalStateException("Class not defined yet");
282: }
283:
284: byte[] bytes = data.toByteArray();
285:
286: if (DEBUG) {
287: File file = new File(mName.replace('.', '/') + ".class");
288: try {
289: File tempDir = new File(System
290: .getProperty("java.io.tmpdir"));
291: file = new File(tempDir, file.getPath());
292: } catch (SecurityException e) {
293: }
294: try {
295: file.getParentFile().mkdirs();
296: System.out.println("ClassInjector writing to " + file);
297: OutputStream out = new FileOutputStream(file);
298: out.write(bytes);
299: out.close();
300: } catch (Exception e) {
301: e.printStackTrace();
302: }
303: }
304:
305: mClass = mLoader.define(mName, bytes);
306: mData = null;
307: return mClass;
308: }
309:
310: private static final class Loader extends ClassLoader {
311: private Set mReservedNames = new HashSet();
312:
313: Loader(ClassLoader parent) {
314: super (parent);
315: }
316:
317: Loader() {
318: super ();
319: }
320:
321: // Prevent name collisions while multiple threads are injecting classes
322: // by reserving the name.
323: synchronized boolean reserveName(String name) {
324: return mReservedNames.add(name);
325: }
326:
327: synchronized boolean isValid() {
328: // Only use loader for 100 injections, to facilitate class
329: // unloading.
330: return mReservedNames.size() < 100;
331: }
332:
333: Class define(String name, byte[] b) {
334: Class clazz = defineClass(name, b, 0, b.length);
335: resolveClass(clazz);
336: return clazz;
337: }
338: }
339: }
|