package net.sourceforge.javautil.classloader.boot;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.logging.LogManager;
import net.sourceforge.javautil.classloader.boot.IEntryPointType.TargetType;
import net.sourceforge.javautil.classloader.impl.ClassContext;
import net.sourceforge.javautil.classloader.impl.StandardClassLoaderHeiarchy;
import net.sourceforge.javautil.classloader.impl.maven.MavenEntryPoint;
import net.sourceforge.javautil.classloader.source.ClassSource;
import net.sourceforge.javautil.classloader.source.CompositeClassSource;
import net.sourceforge.javautil.classloader.source.LibDirectoryClassSource;
import net.sourceforge.javautil.classloader.source.VirtualDirectoryClassSource;
import net.sourceforge.javautil.classloader.source.ZipClassSource;
import net.sourceforge.javautil.common.CollectionUtil;
import net.sourceforge.javautil.common.ReflectionUtil;
import net.sourceforge.javautil.common.ThrowableUtil;
import net.sourceforge.javautil.common.URLStreamHandlerFactoryComposite;
import net.sourceforge.javautil.common.classloader.ClassLoaderResource;
import net.sourceforge.javautil.common.classloader.ClassLoaderScanner;
import net.sourceforge.javautil.common.classloader.IResourceObjectFactory;
import net.sourceforge.javautil.common.exception.ThrowableManagerRegistry;
import net.sourceforge.javautil.common.io.InputStreamRegistry;
import net.sourceforge.javautil.common.io.OutputStreamRegistry;
import net.sourceforge.javautil.common.io.impl.SystemDirectory;
import net.sourceforge.javautil.common.logging.LoggingContext;
import net.sourceforge.javautil.common.logging.jdk.LoggingFrameworkJDK;
import net.sourceforge.javautil.common.reflection.cache.ClassCache;
import net.sourceforge.javautil.common.reflection.cache.ClassDescriptor;
import net.sourceforge.javautil.common.shutdown.Shutdown;
import net.sourceforge.javautil.common.shutdown.IShutdownHandler;
import net.sourceforge.javautil.common.shutdown.IShutdownHook;
/**
* The entry point (boot strapper) which will setup a class loader for applications.<br/><br/>
*
* You can specify the following java system properties:<br/>
* <b><code> -D{@link #MAIN_OUTPUT}=[true|false]</code></b><br/>
* If true (by default) it will make sure that the standard System.out
* and System.err continue to receive the output as normal, otherwise
* they will simply be redirected, allowing runtime intervention and
* enabling/disabling.<br/><br/>
*
* <b><code> -D{@link #MAIN_INPUT}=[true|false]</code></b><br/>
* If true (by default) it will make sure that the standard System.in
* continues to receive the input as normal, otherwise they will simply be
* redirected, allowing runtime intervention and enabling/disabling.<br/><br/>
*
* <b><code> -D{@link #MAIN_TYPE_PROPERTY}=[type class name|abbreviation]</code></b><br/>
* This tells the system to use a particular entry point type. For instance
* you can specify <code>net.sourceforge.javautil.classloader.impl.maven.MavenEntryPoint</code>
* which will make the startup use the pom.xml of the current directory
* to load an application. See {@link MavenEntryPoint} for details on maven
* specific properties.<br/><br/>
*
* <b><code> -D{@link #MAIN_CLASS_PROPERTY}=[main class]</code></b><br/>
* This tells the system which main class to use once the class loader has been
* configured, and will be passed to the {@link IEntryPointType} used.<br/><br/>
*
* <b>Standard Entry Point Type</b><br/>
* When not specifying the {@value #MAIN_TYPE_PROPERTY}, the standard type will be
* used. The standard entry point will attempt to load an internal jar packaged with
* the main jar.<br/><br/>
*
* The following parameter(s) can be used in connection with it:<br/><br/>
* <b><code> -D{value #MAIN_JAR_PROPERTY}=[jar path]</code></b><br/>
* A jar (when no internal jar is packaged) that should be loaded in order to load application level classes.
* This can be helpful when running the application in an IDE environment in which you have not yet
* packaged/jarred your application.<br/><br/>
*
* The following parameter can be used to have the entry point set the {@link URLStreamHandlerFactoryComposite} for runtime registration:<br/><br/>
* <b><code> -D{@link #MAIN_URLSFOVERRIDE}=[true|false]</code></b><br/>
* Since the standard {@link URL} class only allows one stream handler factory to be registered
* it must be specified early on in order to allow override of the original/default factory. If not false
* the composite will be set and it can be used in order to register more factories at runtime.<br/><br/>
*
* The following parameter can be used in connection with the previous one in order to setup a default {@link URLStreamHandlerFactory}:<br/><br/>
* <b><code> -D{@link #MAIN_URLSFCLASS}=[class name]</code></b><br/>
* The previous setting must be enabled in order for this one to work. {@link IEntryPointType}'s will be responsible for
* registering the specified class with the {@link URLStreamHandlerFactoryComposite}.<br/><br/>
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: EntryPoint.java 2709 2010-12-31 00:46:45Z ponderator $
*
* @see IEntryPointType
* @see EntryPointConfiguration
*/
public class EntryPoint extends EntryPointTypeAbstract implements IEntryPointType, UncaughtExceptionHandler, IResourceObjectFactory<IEntryPointType> {
/**
* The library directories settings for specifying library directories.<br/><br/>
*
* A semi-colon (;) separated list of optionally recursive directories containing jars/java archives
*/
public static final String LIBDIRS_PROPERTY = "net.sf.javautil.libdirs";
public static final String MAIN_JAR_PROPERTY = "net.sf.javautil.jar";
public static final String MAIN_CLASS_PROPERTY = "net.sf.javautil.main";
public static final String MAIN_TYPE_PROPERTY = "net.sf.javautil.type";
public static final String MAIN_LOG_CONFIGDIR = "net.sf.javautil.logconfigdir";
public static final String MAIN_OUTPUT = "net.sf.javautil.output.standard";
public static final String MAIN_INPUT = "net.sf.javautil.input.standard";
public static final String MAIN_URLSFOVERRIDE = "net.sf.javautil.urlsf.override";
public static final String MAIN_URLSFCLASS = "net.sf.javautil.urlsf.default";
public static final String VERSION = EntryPoint.class.getPackage().getSpecificationVersion();
/**
* @return A routed input stream if an {@link EntryPoint} was used for this thread, otherwise the standard {@link System#in}
*/
public static InputStream getStandardInputStream () {
IEntryPointType ep = EntryPoint.getEntryPointType();
if (ep != null && ep.getEntryPoint().inputRegistry != null) {
return ep.getEntryPoint().getInputRegistry();
}
return System.in;
}
/**
* @return A routed system.out stream from the registry if an {@link EntryPoint} was used for this thread, otherwise the standard {@link System#out}
*/
public static PrintStream getStandardOutputStream () {
IEntryPointType ep = EntryPoint.getEntryPointType();
if (ep != null && ep.getEntryPoint().outputRegistry != null) {
return new PrintStream( ep.getEntryPoint().getOutputRegistry().getStream("System.out", true) );
}
return System.out;
}
/**
* @return A routed system.err stream from the registry if an {@link EntryPoint} was used for this thread, otherwise the standard {@link System#err}
*/
public static PrintStream getStandardErrorStream () {
IEntryPointType ep = EntryPoint.getEntryPointType();
if (ep != null && ep.getEntryPoint().outputRegistry != null) {
return new PrintStream( ep.getEntryPoint().getOutputRegistry().getStream("System.err", true) );
}
return System.err;
}
/**
* The main entry point
*/
private static IEntryPointType main;
/**
* Thread locals of the entry points for boot strapping
*/
private static ThreadLocal<IEntryPointType> entryPoints = new ThreadLocal<IEntryPointType>();
/**
* @return The entry point used in the main thread, or null if {@link EntryPoint} was not used for the main thread bootstrapping
*/
public static IEntryPointType getMainEntryPointType () { return main; }
/**
* @return The entry point for the current thread, the {@link #main} entry point, or null if the current thread or the
* main thread did not use {@link EntryPoint} for boot strapping
*/
public static IEntryPointType getEntryPointType () { return entryPoints.get() == null ? main : entryPoints.get(); }
/**
* @param hook The hook to be called when the entry point returned by {@link #getEntryPointType()} exits
*/
public static void registerExitHook (IExitPointHook hook) { getEntryPointType().registerExitPointHook(hook); }
/**
* @param hook The hook no longer to be called when the entry point returned by {@link #getEntryPointType()} exits
*/
public static void unregisterExitHook (IExitPointHook hook) { getEntryPointType().unregisterExitPointHook(hook); }
/**
* This should only be called once during the lifecycle of a JVM. It will create
* an {@link EntryPoint} which will handle the startup of an application class loader.
*
* @param args The arguments from the command line
* @throws Exception
*/
public static void main (String[] args) throws Throwable { new EntryPoint(args).boot(); }
/**
* Print a help message to {@link System#err}, then exit the JVM.
*
* @param error The error message to display
*/
protected static void printHelp (String error) {
System.err.println(error);
System.err.println();
System.exit(-1);
}
private final String[] originalArguments;
private final PrintStream originalOut;
private final PrintStream originalErr;
private final InputStream originalIn;
private boolean loggingSetup = false;
private final OutputStreamRegistry outputRegistry;
private final InputStreamRegistry inputRegistry;
private URLStreamHandlerFactoryComposite urlsfOverride;
/**
* @return The number of arguments available via {@link #getOriginalArgument(int)}
*/
public int getOriginalArgumentCount() { return originalArguments.length; }
/**
* @param idx The index of the corresponding argument
* @return The corresponding argument value
*
* @see #getOriginalArgumentCount()
*/
public String getOriginalArgument (int idx) { return originalArguments[idx]; }
/**
* @return The standard output when this entry point was created
*/
public PrintStream getOriginalOut() { return originalOut; }
/**
* @return The standard error output when this entry point was created
*/
public PrintStream getOriginalErr() { return originalErr; }
/**
* @return The standard input when this entry point was created
*/
public InputStream getOriginalIn() { return originalIn; }
/**
* There will be two standard streams registered, System.out and System.err
*
* @return The output regisry for the entry point
*/
public OutputStreamRegistry getOutputRegistry() { return outputRegistry; }
/**
* @return The input regitry for the entry point
*/
protected InputStreamRegistry getInputRegistry() { return inputRegistry; }
/**
* @return True if the default {@link URLStreamHandlerFactory} has been overriden with {@link URLStreamHandlerFactoryComposite}, otherwise false
*/
public boolean isUrlsfOverride() { return urlsfOverride != null; }
/**
* If {@link #isUrlsfOverride()} returns true then this method can be used to register a {@link URLStreamHandlerFactory}
* with the composite override, otherwise it will throw a {@link IllegalStateException}.
*
* @param factory The factory to register
*/
public void registerURLStreamHandlerFactory (URLStreamHandlerFactory factory) {
if (this.urlsfOverride == null) throw new IllegalStateException("The default stream handler factory was not overriden, registry disabled");
this.urlsfOverride.factories.add(factory);
}
/**
* If {@link #isUrlsfOverride()} returns true then this can be used to map a protocol to a {@link URLStreamHandler}
* otherwise it will throw an {@link IllegalStateException}.
*
* @param protocol The protocol to map the handler to
* @param handler The handler to map to the protocol
* @param override True if any existing protocol handler should be overridden, otherwise false
*/
public void registerURLStreamHandler (String protocol, URLStreamHandler handler, boolean override) {
if (this.urlsfOverride == null) throw new IllegalStateException("The default stream handler factory was not overriden, registry disabled");
if (!this.urlsfOverride.handlers.containsKey(protocol) || override)
this.urlsfOverride.handlers.put(protocol, handler);
}
/**
* This can only be instantiated once per thread. You can instantiate in different threads to
* setup different applications using this same technique inside the same JVM.<br/><br/>
*
* @param args The 'command line' arguments to pass to the application.
*/
private EntryPoint (String[] args) {
super(null, new EntryPointSettings());
Thread.currentThread().setUncaughtExceptionHandler(this);
if (entryPoints.get() != null)
throw new UnsupportedOperationException("An application has already been started for this thread: " + Thread.currentThread().getName());
originalIn = System.in;
originalOut = System.out;
originalErr = System.err;
originalArguments = args;
String stdout = this.getSetting(MAIN_OUTPUT);
if (stdout != null && "false".equalsIgnoreCase(stdout)) {
outputRegistry = new OutputStreamRegistry();
System.setOut( new PrintStream( outputRegistry.getStream("System.out", true), true ) );
System.setErr( new PrintStream( outputRegistry.getStream("System.err", true), true ) );
outputRegistry.addListener("System.out", originalOut, false);
outputRegistry.addListener("System.err", originalErr, false);
} else {
outputRegistry = null;
}
String stdin = this.getSetting(MAIN_INPUT);
if (stdin != null && "false".equalsIgnoreCase(stdin)) {
inputRegistry = new InputStreamRegistry("Standard Input", System.in);
inputRegistry.start();
System.setIn(inputRegistry);
} else {
inputRegistry = null;
}
if (!"false".equalsIgnoreCase(this.getSetting(MAIN_URLSFOVERRIDE)) && main == null)
try {
URL.setURLStreamHandlerFactory(this.urlsfOverride = new URLStreamHandlerFactoryComposite());
} catch (Throwable t) {
this.urlsfOverride = null;
}
}
public EntryPoint getEntryPoint() { return this; }
/**
* This will first determine the {@link IEntryPointType} to be used for class loader
* configuration. Then determine the main class specified. After that it will call
* {@link IEntryPointType#main(EntryPointConfiguration)} on the specified type.
*
* @throws Exception
*/
private void boot () throws Throwable {
if (log.isDebug())
log.debug("BOOTING START: " + System.currentTimeMillis());
this.setupDefaultLogging(true);
if (log.isDebug())
log.debug("Using JavaUtil's EntryPoint Bootstrapping " + (VERSION == null ? "" : "[" + VERSION + "]"));
IEntryPointType type = this;
String bootType = this.getSetting(MAIN_TYPE_PROPERTY);
if ("main".equals( Thread.currentThread().getName() ))
main = type;
entryPoints.set(type);
Shutdown.setGlobalHandler(ExitPoint.class);
EntryPointConfiguration config = new EntryPointConfiguration(this);
if (bootType != null) {
ClassDescriptor<? extends IEntryPointType> typeClass = ClassCache.getFor( ReflectionUtil.getClass(bootType) );
type = typeClass.newInstance(this, settings);
} else {
EntryPointTypeComposite composite = this.detectEntryPoints(config);
if (composite != null) type = composite;
}
try {
while (true) {
try {
if (log.isDebug())
log.debug("BOOTING LEVEL 1: " + type + ": " + System.currentTimeMillis());
type.main(config);
type.cleanup(false);
} catch (Throwable t) {
EntryPointRebootException reboot = ThrowableUtil.getWrapped(t, EntryPointRebootException.class);
if (reboot != null) {
Shutdown.shutdown();
type.cleanup(true);
continue;
} else {
if (type.getExceptionHandler() != null) {
Shutdown.shutdown();
type.getExceptionHandler().handle(type, t);
}
else throw t;
}
}
break;
}
} finally {
if (type.getEntryPoint().inputRegistry != null)
type.getEntryPoint().getInputRegistry().stop();
}
}
public IEntryPointType generate(ClassLoaderResource resource) {
try {
return (IEntryPointType) ClassCache.getFor( resource.loadClass() ).newInstance(this, settings);
} catch (ClassNotFoundException e) {
throw ThrowableManagerRegistry.caught(e);
}
}
public EntryPointTypeComposite detectEntryPoints (EntryPointConfiguration config) {
ServiceLoader<IEntryPointType> located = ServiceLoader.load(IEntryPointType.class);
Iterator<IEntryPointType> types = located.iterator();
if (types.hasNext()) {
List<IEntryPointType> bootstrappers = new ArrayList<IEntryPointType>();
IEntryPointType endpoint = null;
while (types.hasNext()) {
IEntryPointType etype = types.next();
etype.setEntryPoint(this);
etype.setSettings(settings);
if (config.isAlreadyExecuted(etype.getClass()) || !etype.canBoot()) continue;
if (etype.getTargetType() == TargetType.Bootstrapper) {
bootstrappers.add(etype);
} else if (endpoint == null) {
endpoint = etype;
}
}
if (bootstrappers.size() > 0 || endpoint != null) {
return new EntryPointTypeComposite(this, settings, bootstrappers, endpoint);
}
}
return null;
}
/**
* This will allow auto detection of double boot classes, where the main class is
* actually another {@link IEntryPointType}. Also it will detect if the class is of type
* {@link Runnable} and if so, instantiate it and call {@link Runnable#run()}, otherwise it
* will assume that the class is a normal {@link #main(String[])} class.
*
* @param mainDescriptor The descriptor for the main class
* @param type The entry point type that is currently booting
* @param mainClass The next main class to be called in the boot chain (only used if mainDescriptor is another {@link IEntryPointType}
*/
public void boot (ClassDescriptor mainDescriptor, IEntryPointType type) throws Throwable {
if (IEntryPointType.class.isAssignableFrom( mainDescriptor.getDescribedClass() )) {
EntryPointConfiguration config = new EntryPointConfiguration(type, this);
if (log.isDebug())
log.debug("BOOTING LEVEL 2: " + System.currentTimeMillis());
IEntryPointType ept = (IEntryPointType) mainDescriptor.newInstance(this, this.settings);
ept.main(config);
} else if (Runnable.class.isAssignableFrom( mainDescriptor.getDescribedClass() )) {
if (log.isDebug())
log.debug("BOOTING LEVEL 3: " + System.currentTimeMillis());
Runnable runnable = (Runnable) mainDescriptor.newInstance();
runnable.run();
} else {
if (log.isDebug())
log.debug("BOOTING LEVEL 4: " + System.currentTimeMillis());
mainDescriptor.invoke("main", null, new Object[] { CollectionUtil.copy( this.originalArguments ) });
}
}
/**
* @param type The type that is booting the instance
* @param instance The instance to boot
*/
public void boot (IEntryPointType type, Object instance) throws Throwable {
if (instance instanceof IEntryPointType) {
EntryPointConfiguration config = new EntryPointConfiguration(type, this);
((IEntryPointType) instance).main(config);
} else if (instance instanceof Runnable) {
((Runnable) instance).run();
} else {
ClassCache.getFor(instance.getClass()).invoke("main", instance, new Object[] { CollectionUtil.copy( this.originalArguments ) });
}
}
/**
* If the {@link #MAIN_URLSFCLASS} is specified and the {@link #isUrlsfOverride()} returns true
* this will load the specified class using the provided class loader and register it using
* {@link #registerURLStreamHandlerFactory(URLStreamHandlerFactory)}.
*
* @param cl The class loader to use for loading the specified factory class
*/
public void setupDefaultURLStreamHandlerFactory (ClassLoader cl) {
if (this.getSetting(MAIN_URLSFCLASS) != null) {
this.registerURLStreamHandlerFactory(
(URLStreamHandlerFactory) ReflectionUtil.newInstance(
ReflectionUtil.getClass(this.getSetting(MAIN_URLSFCLASS), cl), new Class[0]
)
);
}
}
/**
* Detect a logging.properties in the local directory or in a specified directory (default "config")
* and setup parameters.
*/
public void setupDefaultLogging (boolean retry) {
if (this.loggingSetup || !retry) return;
this.loggingSetup = true;
File configDir = new File(this.getSetting(MAIN_LOG_CONFIGDIR) == null ? "conf" : this.getSetting(MAIN_LOG_CONFIGDIR)).getAbsoluteFile();
for (File config : new File[] {
new File("logging.properties").getAbsoluteFile(),
new File(configDir, "logging.properties")
}) {
if (config.exists()) {
try {
LogManager.getLogManager().readConfiguration(new FileInputStream(config));
new LoggingContext(LoggingFrameworkJDK.getInstance()).setGlobal();
} catch (Exception e) {
ThrowableManagerRegistry.caught(e);
}
}
}
}
public TargetType getTargetType() {
return TargetType.Bootstrapper;
}
public boolean canBoot() {
return true;
}
public void main (EntryPointConfiguration config) throws Throwable {
URL boot = EntryPoint.class.getClassLoader()
.getResource("META-INF/net/sf/javautil/boot.jar");
ClassContext context = null;
CompositeClassSource mainSource = new CompositeClassSource();
ClassSource additional = this.getAdditionalClassSources();
if (additional != null) mainSource.add(additional);
if (boot != null) {
mainSource.add( VirtualDirectoryClassSource.createInMemoryJar("boot", "main", boot.openStream()) );
context = new ClassContext(new StandardClassLoaderHeiarchy(Thread.currentThread().getContextClassLoader()), mainSource);
} else {
String main = this.getSetting(MAIN_JAR_PROPERTY);
if (main == null)
printHelp("No internal uber jar and no main jar specified -D" + MAIN_JAR_PROPERTY + "=''");
File mainFile = new File(main);
if (!mainFile.exists())
printHelp("Specified main jar does not exist: " + mainFile);
mainSource.add( new ZipClassSource(mainFile) );
context = new ClassContext(new StandardClassLoaderHeiarchy(Thread.currentThread().getContextClassLoader()), mainSource);
}
Thread.currentThread().setContextClassLoader(context);
this.setupDefaultURLStreamHandlerFactory(context);
String mainClassName = null;
if (mainClassName == null) {
Manifest manifest = mainSource.getManifest();
if (manifest == null)
printHelp("No main jar manifest found in " + mainSource);
mainClassName = manifest.getMainAttributes().getValue("Main-Class");
}
Thread.currentThread().setContextClassLoader(context);
if (config.getMainClass() != null)
this.boot(ClassCache.getFor(context.loadClass(mainClassName)), this);
}
/**
* This will by default detect the {@link #LIBDIRS_PROPERTY}.
*
* @return The class sources to add in addition to the pom.xml and project directory
* @throws Exception
*/
public ClassSource getAdditionalClassSources () throws Exception {
String libdirs = this.getSetting(LIBDIRS_PROPERTY);
if (libdirs == null) return null;
String[] libs = libdirs.split(";");
if (libs.length == 1) return new LibDirectoryClassSource(new SystemDirectory(libs[0]), true);
else {
CompositeClassSource ccs = new CompositeClassSource();
for (String lib : libs) ccs.add(new LibDirectoryClassSource(new SystemDirectory(lib), true));
return ccs;
}
}
public void uncaughtException(Thread t, Throwable e) {
try {
e.printStackTrace(originalOut);
ThrowableManagerRegistry.get().uncaughtException(t, e);
} catch (Throwable throwable) {
e.printStackTrace(originalOut);
}
}
/**
* This will allow for {@link IEntryPointType} exit point handlers to be called related to the use of this framework.
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: EntryPoint.java 2709 2010-12-31 00:46:45Z ponderator $
*/
public static class ExitPoint implements IShutdownHandler {
protected final IShutdownHandler parent;
public ExitPoint(IShutdownHandler parent) {
this.parent = parent;
}
public void shutdown(Thread thread) {
getEntryPointType().invokeExitPointHooks();
parent.shutdown(thread);
}
public void register(IShutdownHook hook) {
getEntryPointType().registerExitPointHook(new ExitPointShutdownHook(hook));
}
public void unregister(IShutdownHook hook) {
for (IExitPointHook ehook : getEntryPointType().getHooks()) {
if (ehook instanceof ExitPointShutdownHook) {
if (((ExitPointShutdownHook)ehook).hook == hook) {
getEntryPointType().unregisterExitPointHook(ehook);
break;
}
}
}
}
/**
* A simple wrapper for {@link IShutdownHook}'s.
*
* @author elponderador
* @author $Author: ponderator $
* @version $Id: EntryPoint.java 2709 2010-12-31 00:46:45Z ponderator $
*/
protected class ExitPointShutdownHook implements IExitPointHook {
protected final IShutdownHook hook;
public ExitPointShutdownHook(IShutdownHook hook) { this.hook = hook; }
public void exit() { this.hook.shutdown(); }
}
}
}