Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerImpl.java
blob55e436b411e9e67450c05806ccf007f550e4373e
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import static com.google.appengine.tools.development.ContainerService.EnvironmentVariableMismatchSeverity.ERROR;
6 import static com.google.appengine.tools.development.ContainerService.EnvironmentVariableMismatchSeverity.WARNING;
7 import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
9 import com.google.apphosting.api.ApiProxy;
10 import com.google.apphosting.api.ApiProxy.Environment;
11 import com.google.apphosting.utils.config.AppEngineConfigException;
12 import com.google.apphosting.utils.config.AppEngineWebXml;
13 import com.google.apphosting.utils.config.AppEngineWebXmlReader;
15 import java.io.File;
16 import java.lang.reflect.Field;
17 import java.lang.reflect.Method;
18 import java.net.BindException;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.TimeZone;
22 import java.util.logging.ConsoleHandler;
23 import java.util.logging.Handler;
24 import java.util.logging.Level;
25 import java.util.logging.Logger;
27 /**
28 * {@code DevAppServer} launches a local Jetty server (by default) with a single
29 * hosted web application. It can be invoked from the command-line by
30 * providing the path to the directory in which the application resides as the
31 * only argument.
34 class DevAppServerImpl implements DevAppServer {
36 private final LocalServerEnvironment environment;
38 private Map<String, String> serviceProperties = new HashMap<String, String>();
40 private Logger logger = Logger.getLogger(DevAppServerImpl.class.getName());
42 enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN }
44 /**
45 * The current state of the server.
47 private ServerState serverState = ServerState.INITIALIZING;
49 /**
50 * This is the main container containing the main (default) server
52 private ContainerService mainContainer = null;
54 /**
55 * Contains the backend servers configured as part of the "Servers" feature.
56 * Each backend server is started on a separate port and keep their own
57 * internal state. Memcache, datastore, and other API services are shared by
58 * all servers, including the "main" server.
60 private final BackendContainer backendContainer;
62 /**
63 * The api proxy we created when we started the web containers. Not initialized until after
64 * {@link #start()} is called.
66 private ApiProxyLocal apiProxyLocal;
68 /**
69 * Constructs a development application server that runs the single
70 * application located in the given directory. The application is configured
71 * via <webXmlLocation> and the {@link AppEngineWebXml}
72 * instance returned by the provided {@link AppEngineWebXmlReader}.
74 * @param appDir The location of the application to run.
75 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
76 * This will be searched before appDir when looking for resources.
77 * @param webXmlLocation The location of a file whose format complies with
78 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
79 * defaults to <appDir>/WEB-INF/web.xml
80 * @param appEngineWebXmlLocation The name of the app engine config file. If
81 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
82 * @param address The address on which to run
83 * @param port The port on which to run
84 * @param useCustomStreamHandler If {@code true}, install
85 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
86 * server.
87 * @param containerConfigProperties Additional properties used in the
88 * configuration of the specific container implementation.
90 public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocation,
91 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
92 Map<String, Object> containerConfigProperties) {
93 String serverInfo = ContainerUtils.getServerInfo();
94 if (useCustomStreamHandler) {
95 StreamHandlerFactory.install();
97 DevSocketImplFactory.install();
98 mainContainer = ContainerUtils.loadContainer();
99 environment = mainContainer.configure(serverInfo, appDir, externalResourceDir,
100 webXmlLocation, appEngineWebXmlLocation, address, port,
101 containerConfigProperties, this);
102 backendContainer = BackendServers.getInstance();
103 backendContainer.init(appDir, externalResourceDir, webXmlLocation, appEngineWebXmlLocation,
104 address, containerConfigProperties, this);
108 * Sets the properties that will be used by the local services to
109 * configure themselves. This method must be called before the server
110 * has been started.
112 * @param properties a, maybe {@code null}, set of properties.
114 * @throws IllegalStateException if the server has already been started.
116 @Override
117 public void setServiceProperties(Map<String,String> properties) {
118 if (serverState != ServerState.INITIALIZING) {
119 String msg = "Cannot set service properties after the server has been started.";
120 throw new IllegalStateException(msg);
122 serviceProperties = new HashMap<String, String>(properties);
123 backendContainer.setServiceProperties(properties);
127 * Starts the server.
129 * @throws IllegalStateException If the server has already been started or
130 * shutdown.
131 * @throws AppEngineConfigException If no WEB-INF directory can be found or
132 * WEB-INF/appengine-web.xml does not exist.
134 @Override
135 public void start() throws Exception {
136 if (serverState != ServerState.INITIALIZING) {
137 throw new IllegalStateException("Cannot start a server that has already been started.");
140 initializeLogging();
142 ApiProxyLocalFactory factory = new ApiProxyLocalFactory();
143 apiProxyLocal = factory.create(environment);
144 apiProxyLocal.setProperties(serviceProperties);
145 ApiProxy.setDelegate(apiProxyLocal);
147 TimeZone currentTimeZone = null;
149 try {
150 currentTimeZone = setServerTimeZone();
151 mainContainer.startup();
153 Environment env = ApiProxy.getCurrentEnvironment();
154 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + getPort());
156 this.serviceProperties.putAll(mainContainer.getServiceProperties());
157 apiProxyLocal.appendProperties(mainContainer.getServiceProperties());
159 backendContainer.startupAll(mainContainer.getBackendsXml(), apiProxyLocal);
160 } catch (BindException ex) {
161 System.err.println();
162 System.err.println("************************************************");
163 System.err.println("Could not open the requested socket: " + ex.getMessage());
164 System.err.println("Try overriding --address and/or --port.");
165 System.exit(2);
166 } finally {
167 ApiProxy.clearEnvironmentForCurrentThread();
168 restoreLocalTimeZone(currentTimeZone);
170 serverState = ServerState.RUNNING;
172 String prettyAddress = mainContainer.getAddress();
173 if (prettyAddress.equals("0.0.0.0") || prettyAddress.equals("127.0.0.1")) {
174 prettyAddress = "localhost";
177 String listeningHostAndPort = prettyAddress + ":" + mainContainer.getPort();
178 logger.info("The server is running at http://" + listeningHostAndPort + "/");
179 logger.info("The admin console is running at http://" + listeningHostAndPort + "/_ah/admin");
183 * Change the TimeZone for the current thread. By calling this method before
184 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
185 * DevAppServer and all of its related services.
187 * @return the previously installed ThreadLocal TimeZone
189 private TimeZone setServerTimeZone() {
190 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
191 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
192 return null;
195 TimeZone utc = TimeZone.getTimeZone("UTC");
196 assert utc.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
198 try {
199 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
200 f.setAccessible(true);
201 ThreadLocal tl = (ThreadLocal) f.get(null);
202 Method getZone = ThreadLocal.class.getMethod("get");
203 TimeZone previousZone = (TimeZone) getZone.invoke(tl);
204 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
205 setZone.invoke(tl, utc);
206 return previousZone;
207 } catch (Exception e) {
208 try {
209 Method getZone = TimeZone.class.getDeclaredMethod("getDefaultInAppContext");
210 getZone.setAccessible(true);
211 TimeZone previousZone = (TimeZone) getZone.invoke(null);
212 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
213 setZone.setAccessible(true);
214 setZone.invoke(null, utc);
215 return previousZone;
216 } catch (Exception ex) {
217 throw new RuntimeException("Unable to set the TimeZone to UTC", ex);
223 * Restores the ThreadLocal TimeZone to {@code timeZone}.
225 private void restoreLocalTimeZone(TimeZone timeZone) {
226 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
227 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
228 return;
231 try {
232 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
233 f.setAccessible(true);
234 ThreadLocal tl = (ThreadLocal) f.get(null);
235 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
236 setZone.invoke(tl, timeZone);
237 } catch (Exception e) {
238 try {
239 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
240 setZone.setAccessible(true);
241 setZone.invoke(null, timeZone);
242 } catch (Exception ex) {
243 throw new RuntimeException("Unable to restore the previous TimeZone", ex);
248 @Override
249 public void restart() throws Exception {
250 if (serverState != ServerState.RUNNING) {
251 throw new IllegalStateException("Cannot restart a server that is not currently running.");
253 mainContainer.shutdown();
254 backendContainer.shutdownAll();
255 mainContainer.startup();
256 backendContainer.startupAll(mainContainer.getBackendsXml(), apiProxyLocal);
260 * Shut down the server.
262 * @throws IllegalStateException If the server has not been started or has
263 * already been shutdown.
265 @Override
266 public void shutdown() throws Exception {
267 if (serverState != ServerState.RUNNING) {
268 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
270 mainContainer.shutdown();
271 backendContainer.shutdownAll();
272 ApiProxy.setDelegate(null);
273 apiProxyLocal = null;
274 serverState = ServerState.SHUTDOWN;
278 * @return the servlet container listener port number.
280 @Override
281 public int getPort() {
282 return mainContainer.getPort();
286 * Returns the web app context. Useful in embedding scenarios to allow the
287 * embedder to install servlets, etc. Any such modification should be done
288 * before calling {@link #start()}.
290 * @see ContainerService#getAppContext
292 @Override
293 public AppContext getAppContext() {
294 return mainContainer.getAppContext();
298 * Reset the container EnvironmentVariableMismatchSeverity.
300 @Override
301 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch) {
302 mainContainer.setEnvironmentVariableMismatchSeverity(throwOnMismatch ? ERROR : WARNING);
306 * We're happy with the default logging behavior, which is to
307 * install a {@link ConsoleHandler} at the root level. The only
308 * issue is that we want its level to be FINEST to be consistent
309 * with our runtime environment.
311 * <p>Note that this does not mean that any fine messages will be
312 * logged by default -- each Logger still defaults to INFO.
313 * However, it is sufficient to call {@link Logger#setLevel(Level)}
314 * to adjust the level.
316 private void initializeLogging() {
317 for (Handler handler : Logger.getLogger("").getHandlers()) {
318 if (handler instanceof ConsoleHandler) {
319 handler.setLevel(Level.FINEST);
324 ServerState getServerState() {
325 return serverState;