package net.i2p.router.web;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext;
import org.apache.tomcat.SimpleInstanceManager;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppClassLoader;
import org.eclipse.jetty.webapp.WebAppContext;
/**
* Add to the webapp classpath as specified in webapps.config.
* This allows us to reference classes that are not in the classpath
* specified in wrapper.config, since old installations have
* individual jars and not lib/*.jar specified in wrapper.config.
*
* A sample line in webapps.config is:
* webapps.appname.classpath=foo.jar,$I2P/lib/bar.jar
* Unless $I2P is specified the path will be relative to $I2P/lib for
* webapps in the installation and appDir/plugins/appname/lib for plugins.
*
* Sadly, setting Class-Path in MANIFEST.MF doesn't work for jetty wars.
* We could look there ourselves, or look for another properties file in the war,
* but let's just do it in webapps.config.
*
* No, wac.addClassPath() does not work. For more info see:
*
* http://servlets.com/archive/servlet/ReadMsg?msgId=511113&listName=jetty-support
*
* @since 0.7.12
* @author zzz
*/
public class WebAppConfiguration implements Configuration {
private static final String CLASSPATH = ".classpath";
/**
* This was the interface in Jetty 5, in Jetty 6 was configureClassLoader(),
* now it's configure()
*/
private void configureClassPath(WebAppContext wac) throws Exception {
String ctxPath = wac.getContextPath();
//System.err.println("Configure Class Path " + ctxPath);
if (ctxPath.equals("/"))
return;
String appName = ctxPath.substring(1);
/****
if (ctxPath.equals("/susimail")) {
// allow certain Jetty classes, restricted as of Jetty 7
// See http://wiki.eclipse.org/Jetty/Reference/Jetty_Classloading
//System.err.println("Allowing Jetty utils in classpath for " + appName);
//System.err.println("System classes before: " + Arrays.toString(wac.getSystemClasses()));
//System.err.println("Server classes before: " + Arrays.toString(wac.getServerClasses()));
wac.addSystemClass("org.eclipse.jetty.http.");
wac.addSystemClass("org.eclipse.jetty.io.");
wac.addSystemClass("org.eclipse.jetty.util.");
// org.eclipse.jetty.webapp.ClasspathPattern looks in-order, and
// WebAppContext doesn't provide a remove method, so we must
// convert to a list, remove the wildcard entry, add ours, then
// add the wildcard back, then reset.
List<String> classes = new ArrayList<String>(16);
classes.addAll(Arrays.asList(wac.getServerClasses()));
classes.remove("org.eclipse.jetty.");
classes.add("-org.eclipse.jetty.http.");
classes.add("-org.eclipse.jetty.io.");
classes.add("-org.eclipse.jetty.util.");
classes.add("org.eclipse.jetty.");
wac.setServerClasses(classes.toArray(new String[classes.size()]));
//System.err.println("System classes after: " + Arrays.toString(wac.getSystemClasses()));
//System.err.println("Server classes after: " + Arrays.toString(wac.getServerClasses()));
}
****/
I2PAppContext i2pContext = I2PAppContext.getGlobalContext();
File libDir = new File(i2pContext.getBaseDir(), "lib");
// FIXME this only works if war is the same name as the plugin
File pluginDir = new File(i2pContext.getConfigDir(),
PluginStarter.PLUGIN_DIR + ctxPath);
File dir = libDir;
String cp;
/****
if (ctxPath.equals("/susimail")) {
// Ticket #957... don't know why...
// Only really required if started manually, but we don't know that from here
cp = "jetty-util.jar";
****/
if (ctxPath.equals("/susidns")) {
// Old installs don't have this in their wrapper.config classpath
cp = "addressbook.jar";
} else if (pluginDir.exists()) {
File consoleDir = new File(pluginDir, "console");
Properties props = RouterConsoleRunner.webAppProperties(consoleDir.getAbsolutePath());
cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH);
dir = pluginDir;
} else {
Properties props = RouterConsoleRunner.webAppProperties(i2pContext);
cp = props.getProperty(RouterConsoleRunner.PREFIX + appName + CLASSPATH);
}
if (cp == null)
return;
StringTokenizer tok = new StringTokenizer(cp, " ,");
StringBuilder buf = new StringBuilder();
Set<URI> systemCP = getSystemClassPath(i2pContext);
while (tok.hasMoreTokens()) {
if (buf.length() > 0)
buf.append(',');
String elem = tok.nextToken().trim();
String path;
if (elem.startsWith("$I2P"))
path = i2pContext.getBaseDir().getAbsolutePath() + elem.substring(4);
else if (elem.startsWith("$PLUGIN"))
path = dir.getAbsolutePath() + elem.substring(7);
else
path = dir.getAbsolutePath() + '/' + elem;
// As of Jetty 6, we can't add dups to the class path, or
// else it screws up statics
// This is not a complete solution because the Windows no-wrapper classpath is set
// by the launchi2p.jar (i2p.exe) manifest and is not detected below.
// TODO: Add a classpath to the command line in i2pstandalone.xml?
File jfile = new File(path);
File jdir = jfile.getParentFile();
if (systemCP.contains(jfile.toURI()) ||
(jdir != null && systemCP.contains(jdir.toURI()))) {
//System.err.println("Not adding " + path + " to classpath for " + appName + ", already in system classpath");
// Ticket #957... don't know why...
if (!ctxPath.equals("/susimail"))
continue;
}
//System.err.println("Adding " + path + " to classpath for " + appName);
buf.append(path);
}
if (buf.length() <= 0)
return;
ClassLoader cl = wac.getClassLoader();
if (cl != null && cl instanceof WebAppClassLoader) {
WebAppClassLoader wacl = (WebAppClassLoader) cl;
wacl.addClassPath(buf.toString());
} else {
// This was not working because the WebAppClassLoader already exists
// and it calls getExtraClasspath in its constructor
// Not sure why WACL already exists...
wac.setExtraClasspath(buf.toString());
}
}
/**
* Convert URL to URI so there's no blocking equals(),
* not that there's really any hostnames in here,
* but keep findbugs happy.
* @since 0.9
*/
private static Set<URI> getSystemClassPath(I2PAppContext ctx) {
Set<URI> rv = new HashSet<URI>(32);
ClassLoader loader = ClassLoader.getSystemClassLoader();
if (loader instanceof URLClassLoader) {
// through Java 8, not available in Java 9
URLClassLoader urlClassLoader = (URLClassLoader) loader;
URL urls[] = urlClassLoader.getURLs();
for (int i = 0; i < urls.length; i++) {
try {
rv.add(urls[i].toURI());
} catch (URISyntaxException use) {}
}
} else {
// Java 9 - assume everything in lib/ is in the classpath
// except addressbook.jar
File libDir = new File(ctx.getBaseDir(), "lib");
File[] files = libDir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
String name = files[i].getName();
if (name.endsWith(".jar") && !name.equals("addressbook.jar"))
rv.add(files[i].toURI());
}
}
}
return rv;
}
/** @since Jetty 7 */
public void deconfigure(WebAppContext context) {}
/** @since Jetty 7 */
public void configure(WebAppContext context) throws Exception {
configureClassPath(context);
// do we just need one, in the ContextHandlerCollection, or one for each?
// http://stackoverflow.com/questions/17529936/issues-while-using-jetty-embedded-to-handle-jsp-jasperexception-unable-to-com
// https://github.com/jetty-project/embedded-jetty-jsp/blob/master/src/main/java/org/eclipse/jetty/demo/Main.java
context.getServletContext().setAttribute("org.apache.tomcat.InstanceManager", new SimpleInstanceManager());
}
/** @since Jetty 7 */
public void cloneConfigure(WebAppContext template, WebAppContext context) {
// no state, nothing to be done
}
/** @since Jetty 7 */
public void destroy(WebAppContext context) {}
/** @since Jetty 7 */
public void preConfigure(WebAppContext context) {}
/** @since Jetty 7 */
public void postConfigure(WebAppContext context) {}
}