/*
* Copyright 2007 Google Inc.
*
* 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 com.google.gwt.dev;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.shell.BrowserWidget;
import com.google.gwt.dev.shell.BrowserWidgetHost;
import com.google.gwt.dev.shell.BrowserWidgetHostChecker;
import com.google.gwt.dev.shell.LowLevel;
import com.google.gwt.dev.shell.ModuleSpaceHost;
import com.google.gwt.dev.shell.PlatformSpecific;
import com.google.gwt.dev.shell.ShellMainWindow;
import com.google.gwt.dev.shell.ShellModuleSpaceHost;
import com.google.gwt.dev.shell.tomcat.EmbeddedTomcatServer;
import com.google.gwt.dev.util.arg.ArgHandlerGenDir;
import com.google.gwt.dev.util.arg.ArgHandlerLogLevel;
import com.google.gwt.dev.util.arg.ArgHandlerScriptStyle;
import com.google.gwt.dev.util.log.AbstractTreeLogger;
import com.google.gwt.util.tools.ArgHandlerExtra;
import com.google.gwt.util.tools.ArgHandlerFlag;
import com.google.gwt.util.tools.ArgHandlerOutDir;
import com.google.gwt.util.tools.ArgHandlerString;
import com.google.gwt.util.tools.ToolBase;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.internal.Library;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* The main executable class for the hosted mode shell.
*/
public class GWTShell extends ToolBase {
/**
* Handles the -blacklist command line argument.
*/
protected class ArgHandlerBlacklist extends ArgHandlerString {
public String[] getDefaultArgs() {
return new String[] {"-blacklist", ""};
}
public String getPurpose() {
return "Prevents the user browsing URLs that match the specified regexes (comma or space separated)";
}
public String getTag() {
return "-blacklist";
}
public String[] getTagArgs() {
return new String[] {"blacklist-string"};
}
public boolean setString(String blacklistStr) {
return BrowserWidgetHostChecker.blacklistRegexes(blacklistStr);
}
}
/**
* handles the -noserver command line flag.
*/
protected class ArgHandlerNoServerFlag extends ArgHandlerFlag {
public String getPurpose() {
return "Prevents the embedded Tomcat server from running, even if a port is specified";
}
public String getTag() {
return "-noserver";
}
public boolean setFlag() {
runTomcat = false;
return true;
}
}
/**
* Handles the -port command line flag.
*/
protected class ArgHandlerPort extends ArgHandlerString {
public String[] getDefaultArgs() {
return new String[] {"-port", "8888"};
}
public String getPurpose() {
return "Runs an embedded Tomcat instance on the specified port (defaults to 8888)";
}
public String getTag() {
return "-port";
}
public String[] getTagArgs() {
return new String[] {"port-number | \"auto\""};
}
public boolean setString(String value) {
if (value.equals("auto")) {
port = 0;
} else {
try {
port = Integer.parseInt(value);
} catch (NumberFormatException e) {
String msg = "A port must be an integer or \"auto\"";
getTopLogger().log(TreeLogger.ERROR, msg, null);
return false;
}
}
return true;
}
}
/**
* Handles the list of startup urls that can be passed on the command line.
*/
protected class ArgHandlerStartupURLs extends ArgHandlerExtra {
public boolean addExtraArg(String arg) {
addStartupURL(arg);
return true;
}
public String getPurpose() {
return "Automatically launches the specified URL";
}
public String[] getTagArgs() {
return new String[] {"url"};
}
}
/**
* Handles the -whitelist command line flag.
*/
protected class ArgHandlerWhitelist extends ArgHandlerString {
public String[] getDefaultArgs() {
return new String[] {"-whitelist", ""};
}
public String getPurpose() {
return "Allows the user to browse URLs that match the specified regexes (comma or space separated)";
}
public String getTag() {
return "-whitelist";
}
public String[] getTagArgs() {
return new String[] {"whitelist-string"};
}
public boolean setString(String whitelistStr) {
return BrowserWidgetHostChecker.whitelistRegexes(whitelistStr);
}
}
private class BrowserWidgetHostImpl implements BrowserWidgetHost {
public BrowserWidgetHostImpl() {
}
public void compile(String[] moduleNames) throws UnableToCompleteException {
for (int i = 0; i < moduleNames.length; i++) {
String moduleName = moduleNames[i];
ModuleDef moduleDef = loadModule(moduleName, getLogger());
GWTShell.this.compile(getLogger(), moduleDef);
}
}
public ModuleSpaceHost createModuleSpaceHost(BrowserWidget widget,
final String moduleName) throws UnableToCompleteException {
TreeLogger logger = getLogger();
// Switch to a wait cursor.
//
Shell widgetShell = widget.getShell();
try {
Cursor waitCursor = display.getSystemCursor(SWT.CURSOR_WAIT);
widgetShell.setCursor(waitCursor);
// Try to find an existing loaded version of the module def.
//
ModuleDef moduleDef = loadModule(moduleName, logger);
assert (moduleDef != null);
// Create a sandbox for the module.
//
TypeOracle typeOracle = moduleDef.getTypeOracle(logger);
ShellModuleSpaceHost host = doCreateShellModuleSpaceHost(logger,
typeOracle, moduleDef, genDir, outDir);
return host;
} finally {
Cursor normalCursor = display.getSystemCursor(SWT.CURSOR_ARROW);
widgetShell.setCursor(normalCursor);
}
}
public TreeLogger getLogger() {
return getTopLogger();
}
public String normalizeURL(String whatTheUserTyped) {
return GWTShell.this.normalizeURL(whatTheUserTyped);
}
public BrowserWidget openNewBrowserWindow()
throws UnableToCompleteException {
return GWTShell.this.openNewBrowserWindow();
}
/**
* Load a module.
*
* @param moduleName name of the module to load
* @param logger TreeLogger to use
* @return the loaded module
* @throws UnableToCompleteException
*/
private ModuleDef loadModule(final String moduleName, TreeLogger logger)
throws UnableToCompleteException {
ModuleDef moduleDef = doLoadModule(logger, moduleName);
assert (moduleDef != null) : "Required module state is absent";
return moduleDef;
}
}
private static Image[] icons;
static {
// Correct menu on Mac OS X
Display.setAppName("GWT");
}
public static String checkHost(String hostUnderConsideration, Set hosts) {
hostUnderConsideration = hostUnderConsideration.toLowerCase();
for (Iterator i = hosts.iterator(); i.hasNext();) {
String rule = i.next().toString().toLowerCase();
// match on lowercased regex
if (hostUnderConsideration.matches(".*" + rule + ".*")) {
return rule;
}
}
return null;
}
public static String computeHostRegex(String url) {
// the enture URL up to the first slash not prefixed by a slash or colon.
String raw = url.split("(?<![:/])/")[0];
// escape the dots and put a begin line specifier on the result
return "^" + raw.replaceAll("[.]", "[.]");
}
public static String formatRules(Set invalidHttpHosts) {
StringBuffer out = new StringBuffer();
for (Iterator i = invalidHttpHosts.iterator(); i.hasNext();) {
String rule = (String) i.next();
out.append(rule);
out.append(" ");
}
return out.toString();
}
/**
* Well-known place to get the GWT icons.
*/
public static Image[] getIcons() {
// Make sure icon images are loaded.
//
if (icons == null) {
icons = new Image[] {
LowLevel.loadImage("icon16.png"), LowLevel.loadImage("icon24.png"),
LowLevel.loadImage("icon32.png"), LowLevel.loadImage("icon48.png"),
LowLevel.loadImage("icon128.png")};
}
return icons;
}
public static void main(String[] args) {
/*
* NOTE: main always exits with a call to System.exit to terminate any
* non-daemon threads that were started in Generators. Typically, this is to
* shutdown AWT related threads, since the contract for their termination is
* still implementation-dependent.
*/
BootStrapPlatform.go();
GWTShell shellMain = new GWTShell();
if (shellMain.processArgs(args)) {
shellMain.run();
}
System.exit(0);
}
/**
* Use the default display; constructing a new one would make instantiating
* multiple GWTShells fail with a mysterious exception.
*/
protected final Display display = Display.getDefault();
protected File outDir;
private BrowserWidgetHostImpl browserHost = new BrowserWidgetHostImpl();
private ShellMainWindow mainWnd;
private boolean runTomcat = true;
private final List browserShells = new ArrayList();
private final List startupUrls = new ArrayList();
private File genDir;
private boolean headlessMode = false;
private TreeLogger.Type logLevel;
private boolean obfuscate;
private int port;
private boolean prettyNames;
private boolean started;
public GWTShell() {
this(false, false);
}
protected GWTShell(boolean forceServer, boolean noURLs) {
registerHandler(getArgHandlerPort());
if (!forceServer) {
registerHandler(new ArgHandlerNoServerFlag());
}
registerHandler(new ArgHandlerWhitelist());
registerHandler(new ArgHandlerBlacklist());
registerHandler(new ArgHandlerLogLevel() {
public String[] getDefaultArgs() {
return new String[] {getTag(), doGetDefaultLogLevel()};
}
public void setLogLevel(Type level) {
logLevel = level;
}
});
registerHandler(new ArgHandlerGenDir() {
public void setDir(File dir) {
genDir = dir;
}
});
if (!noURLs) {
registerHandler(new ArgHandlerStartupURLs());
}
registerHandler(new ArgHandlerOutDir() {
public void setDir(File dir) {
outDir = dir;
}
});
registerHandler(new ArgHandlerScriptStyle() {
public void setStyleDetailed() {
obfuscate = false;
prettyNames = false;
}
public void setStyleObfuscated() {
obfuscate = true;
}
public void setStylePretty() {
obfuscate = false;
prettyNames = true;
}
});
}
public void addStartupURL(String url) {
startupUrls.add(url);
}
public void closeAllBrowserWindows() {
while (!browserShells.isEmpty()) {
((Shell) browserShells.get(0)).dispose();
}
}
public File getGenDir() {
return genDir;
}
public Type getLogLevel() {
return logLevel;
}
public File getOutDir() {
return outDir;
}
public int getPort() {
return port;
}
public TreeLogger getTopLogger() {
return mainWnd.getLogger();
}
public boolean hasBrowserWindowsOpen() {
if (browserShells.isEmpty()) {
return false;
} else {
return true;
}
}
/**
* Launch the arguments as Urls in separate windows.
*/
public void launchStartupUrls(final TreeLogger logger) {
if (startupUrls != null) {
// Launch a browser window for each startup url.
//
String startupURL = "";
try {
for (Iterator iter = startupUrls.iterator(); iter.hasNext();) {
startupURL = normalizeURL((String) iter.next());
logger.log(TreeLogger.TRACE, "Starting URL: " + startupURL, null);
BrowserWidget bw = openNewBrowserWindow();
bw.go(startupURL);
}
} catch (UnableToCompleteException e) {
logger.log(TreeLogger.ERROR,
"Unable to open new window for startup URL: " + startupURL, null);
}
}
}
public String normalizeURL(String unknownUrlText) {
if (unknownUrlText.indexOf(":") != -1) {
// Assume it's a full url.
return unknownUrlText;
}
// Assume it's a trailing url path.
//
if (unknownUrlText.length() > 0 && unknownUrlText.charAt(0) == '/') {
unknownUrlText = unknownUrlText.substring(1);
}
int prt = getPort();
if (prt != 80 && prt != 0) {
// CHECKSTYLE_OFF: Not really an assembled error message, so no space after ':'.
return "http://localhost:" + prt + "/" + unknownUrlText;
// CHECKSTYLE_ON
} else {
return "http://localhost/" + unknownUrlText;
}
}
/**
* Called directly by ShellMainWindow and indirectly via BrowserWidgetHost.
*/
public BrowserWidget openNewBrowserWindow() throws UnableToCompleteException {
boolean succeeded = false;
Shell s = createTrackedBrowserShell();
try {
BrowserWidget bw = PlatformSpecific.createBrowserWidget(getTopLogger(),
s, browserHost);
if (mainWnd != null) {
Rectangle r = mainWnd.getShell().getBounds();
int n = browserShells.size() + 1;
s.setBounds(r.x + n * 50, r.y + n * 50, 800, 600);
} else {
s.setSize(800, 600);
}
if (!isHeadless()) {
s.open();
}
bw.onFirstShown();
succeeded = true;
return bw;
} finally {
if (!succeeded) {
s.dispose();
}
}
}
/**
* Sets up all the major aspects of running the shell graphically, including
* creating the main window and optionally starting the embedded Tomcat
* server.
*/
public void run() {
try {
// Set any platform specific system properties.
BootStrapPlatform.setSystemProperties();
if (!startUp()) {
// Failed to initalize.
return;
}
// Eager AWT initialization for OS X to ensure safe coexistence with SWT.
BootStrapPlatform.maybeInitializeAWT();
// Tomcat's running now, so launch browsers for startup urls now.
launchStartupUrls(getTopLogger());
pumpEventLoop();
shutDown();
} catch (Exception e) {
e.printStackTrace();
}
}
public void setGenDir(File genDir) {
this.genDir = genDir;
}
public void setLogLevel(Type level) {
this.logLevel = level;
}
public void setOutDir(File outDir) {
this.outDir = outDir;
}
public void setPort(int port) {
this.port = port;
}
public void setRunTomcat(boolean run) {
runTomcat = run;
}
/**
* Compiles a logical module def. The caller can modify the specified module
* def programmatically in some cases (this is needed for JUnit support, for
* example).
*/
protected void compile(TreeLogger logger, ModuleDef moduleDef)
throws UnableToCompleteException {
GWTCompiler compiler = new GWTCompiler(moduleDef.getCacheManager());
compiler.setGenDir(genDir);
compiler.setOutDir(outDir);
compiler.setModuleName(moduleDef.getName());
compiler.setLogLevel(logLevel);
if (obfuscate) {
compiler.setStyleObfuscated();
} else if (prettyNames) {
compiler.setStylePretty();
} else {
compiler.setStyleDetailed();
}
compiler.distill(logger, moduleDef);
}
/**
* Creates an instance of ShellModuleSpaceHost (or a derived class) using the
* specified constituent parts. This method is made to be overridden for
* subclasses that need to change the behavior of ShellModuleSpaceHost.
*
* @param logger TreeLogger to use
* @param typeOracle
* @param moduleDef
* @param genDir
* @return ShellModuleSpaceHost instance
*/
protected ShellModuleSpaceHost doCreateShellModuleSpaceHost(
TreeLogger logger, TypeOracle typeOracle, ModuleDef moduleDef,
File genDir, File outDir) {
return new ShellModuleSpaceHost(logger, typeOracle, moduleDef, genDir,
outDir);
}
/**
* Can be override to change the default log level in subclasses. JUnit does
* this for example.
*/
protected String doGetDefaultLogLevel() {
return "INFO";
}
/**
* Loads a named module. This method can be overridden if the module def needs
* to be tweaked (or even created) programmatically -- JUnit integration does
* this, for example.
*/
protected ModuleDef doLoadModule(TreeLogger logger, final String moduleName)
throws UnableToCompleteException {
return ModuleDefLoader.loadFromClassPath(logger, moduleName);
}
/**
* Derived classes can override to prevent automatic update checking.
*/
protected boolean doShouldCheckForUpdates() {
return true;
}
/**
* Derived classes can override to set a default port.
*/
protected ArgHandlerPort getArgHandlerPort() {
return new ArgHandlerPort();
}
protected BrowserWidgetHost getBrowserHost() {
return browserHost;
}
protected void initializeLogger() {
final AbstractTreeLogger logger = mainWnd.getLogger();
logger.setMaxDetail(logLevel);
}
/**
* By default we will open the application window.
*
* @return true if we are running in headless mode
*/
protected boolean isHeadless() {
return headlessMode;
}
protected boolean notDone() {
if (!mainWnd.isDisposed()) {
return true;
}
if (!browserShells.isEmpty()) {
return true;
}
return false;
}
/**
*
*/
protected void pumpEventLoop() {
TreeLogger logger = getTopLogger();
// Run the event loop. When there are no open shells, quit.
//
while (notDone()) {
try {
if (!display.readAndDispatch()) {
display.sleep();
}
} catch (Throwable e) {
String msg = e.getMessage();
msg = (msg != null ? msg : e.getClass().getName());
logger.log(TreeLogger.ERROR, msg, e);
}
}
}
protected void setHeadless(boolean headlessMode) {
this.headlessMode = headlessMode;
}
/**
*
*/
protected void shutDown() {
if (!runTomcat) {
return;
}
// Stop the HTTP server.
//
EmbeddedTomcatServer.stop();
}
protected boolean startUp() {
if (started) {
throw new IllegalStateException("Startup code has already been run");
}
started = true;
loadRequiredNativeLibs();
// Create the main app window.
// When it is up and running, it will start the Tomcat server if desired.
//
openAppWindow();
// Initialize the logger.
//
initializeLogger();
if (runTomcat) {
// Start the HTTP server.
// Use a new thread so that logging that occurs during startup is
// displayed immediately.
//
final int serverPort = getPort();
String whyFailed = EmbeddedTomcatServer.start(getTopLogger(), serverPort,
outDir);
if (whyFailed != null) {
System.err.println(whyFailed);
return false;
}
// Record what port Tomcat is actually running on.
port = EmbeddedTomcatServer.getPort();
}
return true;
}
private Shell createTrackedBrowserShell() {
final Shell shell = new Shell(display);
FillLayout fillLayout = new FillLayout();
fillLayout.marginWidth = 0;
fillLayout.marginHeight = 0;
shell.setLayout(fillLayout);
browserShells.add(shell);
shell.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
if (e.widget == shell) {
browserShells.remove(shell);
}
}
});
shell.setImages(getIcons());
return shell;
}
private void loadRequiredNativeLibs() {
String libName = null;
try {
libName = "swt";
Library.loadLibrary(libName);
} catch (UnsatisfiedLinkError e) {
StringBuffer sb = new StringBuffer();
sb.append("Unable to load required native library '" + libName + "'");
sb.append("\n\tPlease specify the JVM startup argument ");
sb.append("\"-Djava.library.path\"");
throw new RuntimeException(sb.toString(), e);
}
}
private void openAppWindow() {
final Shell shell = new Shell(display);
FillLayout fillLayout = new FillLayout();
fillLayout.marginWidth = 0;
fillLayout.marginHeight = 0;
shell.setLayout(fillLayout);
shell.setImages(getIcons());
boolean checkForUpdates = doShouldCheckForUpdates();
mainWnd = new ShellMainWindow(this, shell, runTomcat ? getPort() : 0,
checkForUpdates);
shell.setSize(700, 600);
if (!isHeadless()) {
shell.open();
}
}
}