/*******************************************************************************
* Copyright © 2011, 2013 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*
*******************************************************************************/
package org.eclipse.edt.ide.testserver;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.edt.ide.internal.testserver.ContributionConfiguration;
import org.eclipse.edt.ide.internal.testserver.DefaultServlet;
import org.eclipse.edt.ide.internal.testserver.Logger;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext;
/**
* Jetty server that's used for testing out Java applications before they're deployed. The applications,
* such as services, are run directly in their Java project.
*/
public class TestServer {
private boolean debug;
private final Integer port;
private final Integer idePort;
private final String contextRoot;
private final List<AbstractConfigurator> configurators;
private Server jettyServer;
private WebAppContext webApp;
private boolean ready;
private String tempDirectory;
/**
* Required arguments:
* <ul>
* <li>-p <port number> (the server's port number)</li>
* <li>-i <IDE port number> (the port number over which the IDE can be reached)</li>
* <li>-c <context root> (the server's context root)</li>
* </ul>
*
* Optional arguments:
* <ul>
* <li>-d (enables debug output)</li>
* </ul>
*
* Additional arguments may be passed for registered configurators.
* @see {@link AbstractConfigurator}
*/
public static void main(String[] args) throws Exception {
List<String> remainingArgs = new ArrayList<String>(args.length);
String[] contributionClassNames = null;
Integer port = null;
Integer idePort = null;
String contextRoot = null;
String tempDir = null;
boolean debug = false;
// First-pass through args is to gather the contributions and any core arg values.
for (int i = 0; i < args.length; i++) {
if ("-p".equals(args[i])) { //$NON-NLS-1$
if (i + 1 < args.length) {
try {
port = Integer.parseInt(args[i+1]);
i++;
}
catch (NumberFormatException e) {
logWarning("Unable to parse port value \"" + args[i+1] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else {
logWarning("Missing port value for argument \"" + args[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else if ("-i".equals(args[i])) { //$NON-NLS-1$
if (i + 1 < args.length) {
try {
idePort = Integer.parseInt(args[i+1]);
i++;
}
catch (NumberFormatException e) {
TestServer.logWarning("Unable to parse IDE port value \"" + args[i+1] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else {
TestServer.logWarning("Missing IDE port value for argument \"" + args[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else if ("-c".equals(args[i])) { //$NON-NLS-1$
if (i + 1 < args.length) {
contextRoot = args[i+1].trim();
i++;
}
else {
logWarning("Missing context root value for argument \"" + args[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else if ("-d".equals(args[i])) { //$NON-NLS-1$
debug = true;
}
else if ("-contribs".equals(args[i])) { //$NON-NLS-1$
if (i + 1 < args.length) {
contributionClassNames = args[i+1].split(";"); //$NON-NLS-1$
i++;
}
else {
logWarning("Missing value for argument \"" + args[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else if ("-td".equals(args[i])) { //$NON-NLS-1$
if (i + 1 < args.length) {
tempDir = args[i+1]; // don't trim, spaces are valid in a path on some systems
i++;
}
else {
logWarning("Missing value for argument \"" + args[i] + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else {
remainingArgs.add(args[i]);
}
}
List<AbstractConfigurator> configurators = new ArrayList<AbstractConfigurator>(contributionClassNames == null ? 0 : contributionClassNames.length);
if (contributionClassNames != null && contributionClassNames.length > 0) {
for (String contrib : contributionClassNames) {
try {
Class c = Class.forName(contrib);
Object o = c.newInstance();
if (o instanceof AbstractConfigurator) {
configurators.add((AbstractConfigurator)o);
}
else {
logWarning("Contribution class \"" + contrib + "\" does not extend \"" + AbstractConfigurator.class.getCanonicalName() + "\". Some functionality may be missing."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
catch (Exception e) {
logWarning("Could not load contribution class \"" + contrib + "\". Some functionality may be missing. Error: " + e.getMessage()); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
// Initialize the test server. This must be done before we invoke any methods in the contributions.
TestServer server = new TestServer(port, idePort, contextRoot, tempDir, configurators, debug);
// Let the contributions process the remaining arguments.
int size = remainingArgs.size();
boolean processed;
for (int i = 0; i < size;) {
processed = false;
for (AbstractConfigurator contrib : configurators) {
int newIndex = contrib.processNextArgument(remainingArgs, i);
if (newIndex != i) {
// Arg was processed; move on to next arg.
processed = true;
i = newIndex;
break;
}
}
if (!processed) {
if (remainingArgs.get(i).startsWith("-")) { //$NON-NLS-1$
logWarning("Unrecognized argument \"" + remainingArgs.get(i) + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
else {
logWarning("Skipping argument value \"" + remainingArgs.get(i) + "\""); //$NON-NLS-1$ //$NON-NLS-2$
}
i++;
}
}
server.start();
}
public TestServer(Integer port, Integer idePort, String contextRoot, String tempDir, List<AbstractConfigurator> configurators, boolean debug) {
this.port = port;
this.idePort = idePort;
this.configurators = configurators;
this.tempDirectory = tempDir;
// Set up logging.
Log.setLog(new Logger());
setDebug(debug, true);
if (contextRoot != null) {
try {
// Encode the context but not a leading '/'
contextRoot = '/' + URLEncoder.encode(contextRoot.charAt(0) == '/' ? contextRoot.substring(1) : contextRoot, "UTF-8"); //$NON-NLS-1$
}
catch (UnsupportedEncodingException e) {
// Shouldn't happen.
if (contextRoot.charAt(0) != '/') {
contextRoot = "/" + contextRoot; //$NON-NLS-1$
}
}
}
this.contextRoot = contextRoot;
// Initialize the configurators.
for (AbstractConfigurator config : configurators) {
config.setTestServer(this);
}
}
private void start() throws Exception {
if (contextRoot == null || contextRoot.length() == 0) {
throw new Exception("Context root argument not specified, cannot start the server"); //$NON-NLS-1$
}
if (port == null) {
throw new Exception("Port argument not specified, cannot start the server"); //$NON-NLS-1$
}
if (port < 0) {
throw new Exception("Port argument \"" + port + "\" is invalid, cannot start the server"); //$NON-NLS-1$ //$NON-NLS-2$
}
if (idePort == null) {
throw new Exception("IDE port argument not specified, cannot start the server"); //$NON-NLS-1$
}
if (idePort < 0) {
throw new Exception("IDE port argument \"" + idePort + "\" is invalid, cannot start the server"); //$NON-NLS-1$ //$NON-NLS-2$
}
webApp = new WebAppContext(null, contextRoot);
webApp.setDefaultsDescriptor(null);
webApp.setThrowUnavailableOnStartupException(true);
// Override the default error handler which breaks sending error messages (e.g. uncaught exception) back to the client.
webApp.setErrorHandler(new ErrorHandler() {
@Override
public void handle(String arg0, Request arg1, HttpServletRequest arg2, HttpServletResponse arg3) throws IOException {
}
});
if (tempDirectory != null && tempDirectory.length() > 0) {
File temp = new File(tempDirectory);
if (!temp.exists()) {
temp.mkdirs();
}
webApp.setTempDirectory(temp);
}
jettyServer = new Server(port);
jettyServer.setHandler(webApp);
// We don't have a real WAR so we must set the base to the project's directory.
if (webApp.getResourceBase() == null) {
webApp.setResourceBase(new File(".").getAbsolutePath()); //$NON-NLS-1$
}
// Register the ping servlet.
webApp.addServlet(new ServletHolder(new DefaultServlet(this)), "/" + DefaultServlet.SERVLET_PATH); //$NON-NLS-1$
// Add our ContributionConfiguration so that the contributions can be invoked in the middle of the jetty startup too.
ContributionConfiguration contributionConfig = new ContributionConfiguration();
contributionConfig.setContributions(configurators);
appendConfiguration(contributionConfig);
// Invoke preStartup() on all contributions.
for (AbstractConfigurator config : configurators) {
config.preStartup();
}
jettyServer.start();
// Invoke postStartup() on all contributions.
for (AbstractConfigurator config : configurators) {
config.postStartup();
}
ready = true;
jettyServer.join();
}
/**
* Adds the configuration to the webapp, taking care of loading classes as needed.
*/
public void appendConfiguration(Configuration config) throws Exception {
if (config == null) {
log("Attempted to add a null configuration.", LogLevel.WARN); //$NON-NLS-1$
return;
}
if (webApp == null) {
log("Attempted to add configuration class " + config.getClass().getCanonicalName() + " before the webapp was created.", LogLevel.WARN); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
// If the webapp has already loaded the other configs, just append this one. Otherwise we
// need to load them first.
Configuration[] configs = webApp.getConfigurations();
if (configs == null) {
String[] configNames = webApp.getConfigurationClasses();
if (configNames != null) {
configs = new Configuration[configNames.length];
for (int i = 0; i < configNames.length; i++) {
configs[i] = (Configuration)Loader.loadClass(this.getClass(), configNames[i]).newInstance();
}
}
else {
// Shouldn't happen.
configs = new Configuration[0];
}
webApp.setConfigurations(configs);
}
// Check if it's already in the list.
Class configClass = config.getClass();
for (Configuration next : configs) {
if (configClass.equals(next.getClass())) {
log("Configuration class " + configClass.getCanonicalName() + " was already added to the webapp - skipping.", LogLevel.WARN); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
}
Configuration[] newConfigs = new Configuration[configs.length + 1];
System.arraycopy(configs, 0, newConfigs, 0, configs.length);
newConfigs[configs.length] = config;
webApp.setConfigurations(newConfigs);
log("Configuration " + configClass.getCanonicalName() + " successfully added to the webapp.", LogLevel.INFO); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* @return the port on which Jetty is running.
*/
public Integer getPort() {
return port;
}
/**
* @return the port on which the IDE can be reached.
*/
public Integer getIDEPort() {
return idePort;
}
/**
* @return the context root of Jetty.
*/
public String getContextRoot() {
return contextRoot;
}
/**
* @return the Jetty server.
*/
public Server getJettyServer() {
return jettyServer;
}
/**
* @return the web app context of the Jetty server.
*/
public WebAppContext getWebApp() {
return webApp;
}
/**
* Sets the debug mode for logging messages.
*
* @param debug The new debug setting.
* @param quiet True if we should print a message to stdout about the setting change.
*/
public void setDebug(boolean debug, boolean quiet) {
this.debug = debug;
Log.getRootLogger().setDebugEnabled(debug);
if (!quiet) {
// Don't use logInfo(), otherwise the user only sees when messages are enabled and not when disabled.
System.out.println("Tracing messages " + (debug ? "enabled" : "disabled")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
/**
* @return the debug mode for logging messages.
*/
public boolean isDebug() {
return debug;
}
/**
* @return true if the server is ready for requests.
*/
public boolean isReady() {
return ready;
}
/**
* Logs a message to standard err if debug mode is enabled.
*/
public void log(String msg, LogLevel level) {
if (debug) {
switch (level) {
case INFO:
System.out.println("INFO: " + msg); //$NON-NLS-1$
break;
case WARN:
System.err.println("WARN: " + msg); //$NON-NLS-1$
break;
case ERROR:
System.err.println("ERROR: " + msg); //$NON-NLS-1$
break;
default:
System.err.println(msg);
break;
}
}
}
/**
* Logs an exception to the console; the debug mode setting is ignored and the message always displayed.
*/
public void log(Exception e) {
e.printStackTrace();
}
/**
* Logs a warning to the console; the debug mode setting is ignored and the message always displayed.
*/
public static void logWarning(String msg) {
System.err.println("WARN: " + msg); //$NON-NLS-1$
}
/**
* Logs a message to standard out if debug mode is enabled.
*/
public static void logInfo(String msg) {
System.out.println("INFO: " + msg); //$NON-NLS-1$
}
}