/* * Copyright 2009-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.classfile; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.security.Permission; import java.security.PermissionCollection; import java.security.Principal; import java.security.ProtectionDomain; import java.security.cert.Certificate; import org.cojen.util.KeyFactory; import org.cojen.util.WeakValuedHashMap; /** * Allows classes to be defined and loaded at runtime. A random number is * appended to class names to prevent name collisions and to discourage * referencing them persistently outside the runtime environment. This behavior * can be disabled by constructing with {@code explicit} set to true. * * <p>Debugging can be enabled via the java command-line option * "-Dorg.cojen.classfile.RuntimeClassFile.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 */ public class RuntimeClassFile extends ClassFile { 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(); private static Map<Object, Loader> cLoaders = new WeakValuedHashMap<Object, Loader>(); private final Loader mLoader; public RuntimeClassFile() { this(null, null, null, null, false, null); } /** * @param className fully qualified class name; pass null to use default */ public RuntimeClassFile(String className) { this(className, null, null, null, false, null); } /** * @param className fully qualified class name; pass null to use default * @param superClassName fully qualified super class name; pass null to use Object. */ public RuntimeClassFile(String className, String superClassName) { this(className, superClassName, null, null, false, null); } /** * @param className fully qualified class name; pass null to use default * @param superClassName fully qualified super class name; pass null to use Object. * @param parentLoader parent class loader; pass null to use default */ public RuntimeClassFile(String className, String superClassName, ClassLoader parentLoader) { this(className, superClassName, parentLoader, null, false, null); } /** * @param className fully qualified class name; pass null to use default * @param superClassName fully qualified super class name; pass null to use Object. * @param parentLoader parent class loader; pass null to use default * @param domain to define class in; pass null to use default */ public RuntimeClassFile(String className, String superClassName, ClassLoader parentLoader, ProtectionDomain domain) { this(className, superClassName, parentLoader, domain, false, null); } /** * @param className fully qualified class name; pass null to use default * @param superClassName fully qualified super class name; pass null to use Object. * @param parentLoader parent class loader; pass null to use default * @param domain to define class in; pass null to use default * @param explicit pass true to prevent name mangling */ public RuntimeClassFile(String className, String superClassName, ClassLoader parentLoader, ProtectionDomain domain, boolean explicit) { this(className, superClassName, parentLoader, domain, explicit, null); } // Magic constructor to select name and loader before calling super class constructor. private RuntimeClassFile(String className, String superClassName, ClassLoader parentLoader, ProtectionDomain domain, boolean explicit, LoaderAndName loaderAndName) { super((loaderAndName = loaderAndName (className, parentLoader, domain, explicit)).mClassName, superClassName); mLoader = loaderAndName.mLoader; } /** * Finishes the class definition. */ public Class defineClass() { byte[] bytes; try { ByteArrayOutputStream bout = new ByteArrayOutputStream(); writeTo(bout); bytes = bout.toByteArray(); } catch (IOException e) { InternalError ie = new InternalError(e.toString()); ie.initCause(e); throw ie; } if (DEBUG) { File file = new File(getClassName().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("RuntimeClassFile writing to " + file); OutputStream out = new FileOutputStream(file); out.write(bytes); out.close(); } catch (Exception e) { e.printStackTrace(); } } return mLoader.define(getClassName(), bytes); } private static LoaderAndName loaderAndName(String className, ClassLoader parentLoader, ProtectionDomain domain, boolean explicit) { if (className == null) { if (explicit) { throw new IllegalArgumentException("Explicit class name not provided"); } className = RuntimeClassFile.class.getName(); } if (parentLoader == null) { parentLoader = RuntimeClassFile.class.getClassLoader(); if (parentLoader == null) { parentLoader = ClassLoader.getSystemClassLoader(); } } final Object loaderKey = createLoaderKey(className, parentLoader, domain); Loader loader = cLoaders.get(loaderKey); if (loader == null) { loader = parentLoader == null ? new Loader(domain) : new Loader(parentLoader, domain); cLoaders.put(loaderKey, loader); } if (explicit) { if (!loader.reserveName(className, true)) { throw new IllegalArgumentException("Class already defined: " + className); } return new LoaderAndName(loader, className); } for (int tryCount = 0; tryCount < 1000; tryCount++) { 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: case 2: case 3: case 4: id &= 0xffffL; break; default: id &= 0xffffffffL; break; } String mangled = className + '$' + id; if (loader.reserveName(mangled, false)) { return new LoaderAndName(loader, mangled); } } throw new InternalError("Unable to create unique class name"); } private static Object createLoaderKey(String className, ClassLoader parentLoader, ProtectionDomain domain) { String packageName; { int index = className.lastIndexOf('.'); if (index < 0) { packageName = ""; } else { packageName = className.substring(0, index); } } // ProtectionDomain doesn't have an equals method, so break it apart // and add the elements to the composite key. Object domainKey = null; Object csKey = null; Object permsKey = null; Object principalsKey = null; if (domain != null) { domainKey = ""; csKey = domain.getCodeSource(); PermissionCollection pc = domain.getPermissions(); if (pc != null) { List<Permission> permList = Collections.list(pc.elements()); if (permList.size() == 1) { permsKey = permList.get(0); } else if (permList.size() > 1) { permsKey = new HashSet<Permission>(permList); } } Principal[] principals = domain.getPrincipals(); if (principals != null && principals.length > 0) { if (principals.length == 1) { principalsKey = principals[0]; } else { Set<Principal> principalSet = new HashSet<Principal>(principals.length); for (Principal principal : principals) { principalSet.add(principal); } principalsKey = principalSet; } } } return KeyFactory.createKey(new Object[] { parentLoader, packageName, domainKey, csKey, permsKey, principalsKey }); } private static final class Loader extends ClassLoader { private final Map<String, Boolean> mReservedNames = new WeakHashMap<String, Boolean>(); private final ProtectionDomain mDomain; Loader(ClassLoader parent, ProtectionDomain domain) { super(parent); mDomain = prepareDomain(domain, this); } Loader(ProtectionDomain domain) { super(); mDomain = prepareDomain(domain, this); } private static ProtectionDomain prepareDomain(ProtectionDomain domain, ClassLoader loader) { if (domain == null) { return null; } return new ProtectionDomain(domain.getCodeSource(), domain.getPermissions(), loader, domain.getPrincipals()); } // Prevent name collisions while multiple threads are defining classes // by reserving the name. boolean reserveName(String name, boolean explicit) { synchronized (mReservedNames) { if (mReservedNames.put(name, Boolean.TRUE) != null && !explicit) { return false; } } // If explicit and name has already been reserved, don't // immediately return false. This allows the class to be defined if // an earlier RuntimeClassFile instance was abandoned. A duplicate // class definition can still be attempted later, which is // converted to an IllegalStateException by the define method. try { loadClass(name); } catch (ClassNotFoundException e) { return true; } catch (LinkageError e) { // Class by same name exists, but it is broken. } return false; } Class define(String name, byte[] b) { try { Class clazz; if (mDomain == null) { clazz = defineClass(name, b, 0, b.length); } else { clazz = defineClass(name, b, 0, b.length, mDomain); } resolveClass(clazz); return clazz; } catch (LinkageError e) { // Replace duplicate name definition with a better exception. try { loadClass(name); throw new IllegalStateException("Class already defined: " + name); } catch (ClassNotFoundException e2) { } throw e; } finally { mReservedNames.remove(name); } } } private static final class LoaderAndName { final Loader mLoader; final String mClassName; LoaderAndName(Loader loader, String className) { mLoader = loader; mClassName = className; } } }