Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerImpl.java
blobaf39a86e177ce216f30a092693b94764344fb13c
1 // Copyright 2008 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
7 import com.google.appengine.api.labs.servers.dev.LocalServersService;
8 import com.google.appengine.tools.development.EnvironmentVariableChecker.MismatchReportingPolicy;
9 import com.google.appengine.tools.info.SdkInfo;
10 import com.google.apphosting.api.ApiProxy;
11 import com.google.apphosting.api.ApiProxy.Environment;
12 import com.google.apphosting.utils.config.AppEngineConfigException;
13 import com.google.apphosting.utils.config.AppEngineWebXmlReader;
14 import com.google.apphosting.utils.config.EarHelper;
15 import com.google.common.base.Joiner;
16 import com.google.common.collect.ImmutableMap;
17 import com.google.common.collect.ImmutableSet;
19 import java.io.File;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.Method;
22 import java.net.BindException;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.TimeZone;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.logging.ConsoleHandler;
28 import java.util.logging.Handler;
29 import java.util.logging.Level;
30 import java.util.logging.Logger;
32 /**
33 * {@code DevAppServer} launches a local Jetty server (by default) with a single
34 * hosted web application. It can be invoked from the command-line by
35 * providing the path to the directory in which the application resides as the
36 * only argument.
39 class DevAppServerImpl implements DevAppServer {
40 public static final String SERVERS_FILTER_HELPER_PROPERTY =
41 "com.google.appengine.tools.development.servers_filter_helper";
42 private static final Logger logger = Logger.getLogger(DevAppServerImpl.class.getName());
44 private final ApplicationConfigurationManager applicationConfigurationManager;
45 private final Servers servers;
46 private Map<String, String> serviceProperties = new HashMap<String, String>();
47 private final Map<String, Object> containerConfigProperties;
49 enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN }
51 /**
52 * The current state of the server.
54 private ServerState serverState = ServerState.INITIALIZING;
56 /**
57 * Contains the backend servers configured as part of the "Servers" feature.
58 * Each backend server is started on a separate port and keep their own
59 * internal state. Memcache, datastore, and other API services are shared by
60 * all servers, including the "main" server.
62 private final BackendServers backendContainer;
64 /**
65 * The api proxy we created when we started the web containers. Not initialized until after
66 * {@link #start()} is called.
68 private ApiProxyLocal apiProxyLocal;
70 /**
71 * We defer reporting construction time configuration exceptions until
72 * {@link #start()} for compatibility.
74 private final AppEngineConfigException configurationException;
76 /**
77 * Constructs a development application server that runs the single
78 * application located in the given directory. The application is configured
79 * via <webXmlLocation> and the {@link com.google.apphosting.utils.config.AppEngineWebXml}
80 * instance returned by the provided {@link AppEngineWebXmlReader}.
82 * @param appDir The location of the application to run.
83 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
84 * This will be searched before appDir when looking for resources.
85 * @param webXmlLocation The location of a file whose format complies with
86 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
87 * defaults to <appDir>/WEB-INF/web.xml
88 * @param appEngineWebXmlLocation The name of the app engine config file. If
89 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
90 * @param address The address on which to run
91 * @param port The port on which to run
92 * @param useCustomStreamHandler If {@code true}, install
93 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
94 * server.
95 * @param containerConfigProperties Additional properties used in the
96 * configuration of the specific container implementation.
98 public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocation,
99 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
100 Map<String, Object> containerConfigProperties) {
101 String serverInfo = ContainerUtils.getServerInfo();
102 if (useCustomStreamHandler) {
103 StreamHandlerFactory.install();
105 DevSocketImplFactory.install();
107 ApplicationConfigurationManager tempManager = null;
108 try {
109 if (EarHelper.isEar(appDir.getAbsolutePath())) {
110 tempManager = ApplicationConfigurationManager.newEarConfigurationManager(appDir,
111 SdkInfo.getLocalVersion().getRelease());
112 } else {
113 tempManager = ApplicationConfigurationManager.newWarConfigurationManager(
114 appDir, appEngineWebXmlLocation, webXmlLocation, externalResourceDir,
115 SdkInfo.getLocalVersion().getRelease());
117 } catch (AppEngineConfigException configurationException) {
118 servers = null;
119 applicationConfigurationManager = null;
120 backendContainer = null;
121 this.containerConfigProperties = null;
122 this.configurationException = configurationException;
123 return;
125 this.applicationConfigurationManager = tempManager;
126 this.servers = Servers.createServers(applicationConfigurationManager, serverInfo,
127 externalResourceDir, address, port, this);
128 backendContainer = BackendServers.getInstance();
129 DelegatingServersFilterHelper serversFilterHelper =
130 new DelegatingServersFilterHelper(backendContainer, servers);
131 this.containerConfigProperties =
132 ImmutableMap.<String, Object>builder()
133 .putAll(containerConfigProperties)
134 .put(SERVERS_FILTER_HELPER_PROPERTY, serversFilterHelper)
135 .build();
136 backendContainer.init(address,
137 applicationConfigurationManager.getPrimaryServerConfigurationHandle(),
138 externalResourceDir, this.containerConfigProperties, this);
139 configurationException = null;
143 * Sets the properties that will be used by the local services to
144 * configure themselves. This method must be called before the server
145 * has been started.
147 * @param properties a, maybe {@code null}, set of properties.
149 * @throws IllegalStateException if the server has already been started.
151 @Override
152 public void setServiceProperties(Map<String,String> properties) {
153 if (serverState != ServerState.INITIALIZING) {
154 String msg = "Cannot set service properties after the server has been started.";
155 throw new IllegalStateException(msg);
158 if (configurationException == null) {
159 serviceProperties = new ConcurrentHashMap<String, String>(properties);
160 backendContainer.setServiceProperties(properties);
164 Map<String, String> getServiceProperties() {
165 return serviceProperties;
169 * Starts the server.
171 * @throws IllegalStateException If the server has already been started or
172 * shutdown.
173 * @throws AppEngineConfigException If no WEB-INF directory can be found or
174 * WEB-INF/appengine-web.xml does not exist.
176 @Override
177 public void start() throws Exception {
178 if (serverState != ServerState.INITIALIZING) {
179 throw new IllegalStateException("Cannot start a server that has already been started.");
182 reportDeferredConfigurationException();
184 initializeLogging();
185 servers.configure(containerConfigProperties);
186 servers.createConnections();
188 ApiProxyLocalFactory factory = new ApiProxyLocalFactory();
189 apiProxyLocal = factory.create(servers.getLocalServerEnvironment());
190 setInboundServicesProperty();
191 apiProxyLocal.setProperties(serviceProperties);
192 ApiProxy.setDelegate(apiProxyLocal);
193 LocalServersService localServersService =
194 (LocalServersService) apiProxyLocal.getService(LocalServersService.PACKAGE);
195 localServersService.setServersControler(servers);
196 TimeZone currentTimeZone = null;
198 try {
199 currentTimeZone = setServerTimeZone();
200 servers.startup();
202 Environment env = ApiProxy.getCurrentEnvironment();
203 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + getPort());
205 backendContainer.startupAll(apiProxyLocal);
206 } catch (BindException ex) {
207 System.err.println();
208 System.err.println("************************************************");
209 System.err.println("Could not open the requested socket: " + ex.getMessage());
210 System.err.println("Try overriding --address and/or --port.");
211 System.exit(2);
212 } finally {
213 ApiProxy.clearEnvironmentForCurrentThread();
214 restoreLocalTimeZone(currentTimeZone);
216 serverState = ServerState.RUNNING;
217 logger.info("Dev App Server is now running");
220 public void setInboundServicesProperty() {
221 ImmutableSet.Builder<String> setBuilder = ImmutableSet.builder();
222 for (ApplicationConfigurationManager.ServerConfigurationHandle serverConfigurationHandle :
223 applicationConfigurationManager.getServerConfigurationHandles()) {
224 setBuilder.addAll(
225 serverConfigurationHandle.getModule().getAppEngineWebXml().getInboundServices());
228 serviceProperties.put("appengine.dev.inbound-services",
229 Joiner.on(",").useForNull("null").join(setBuilder.build()));
233 * Change the TimeZone for the current thread. By calling this method before
234 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
235 * DevAppServer and all of its related services.
237 * @return the previously installed ThreadLocal TimeZone
239 private TimeZone setServerTimeZone() {
240 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
241 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
242 return null;
245 TimeZone utc = TimeZone.getTimeZone("UTC");
246 assert utc.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
248 try {
249 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
250 f.setAccessible(true);
251 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
252 Method getZone = ThreadLocal.class.getMethod("get");
253 TimeZone previousZone = (TimeZone) getZone.invoke(tl);
254 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
255 setZone.invoke(tl, utc);
256 return previousZone;
257 } catch (Exception e) {
258 try {
259 Method getZone = TimeZone.class.getDeclaredMethod("getDefaultInAppContext");
260 getZone.setAccessible(true);
261 TimeZone previousZone = (TimeZone) getZone.invoke(null);
262 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
263 setZone.setAccessible(true);
264 setZone.invoke(null, utc);
265 return previousZone;
266 } catch (Exception ex) {
267 throw new RuntimeException("Unable to set the TimeZone to UTC", ex);
273 * Restores the ThreadLocal TimeZone to {@code timeZone}.
275 private void restoreLocalTimeZone(TimeZone timeZone) {
276 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
277 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
278 return;
281 try {
282 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
283 f.setAccessible(true);
284 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
285 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
286 setZone.invoke(tl, timeZone);
287 } catch (Exception e) {
288 try {
289 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
290 setZone.setAccessible(true);
291 setZone.invoke(null, timeZone);
292 } catch (Exception ex) {
293 throw new RuntimeException("Unable to restore the previous TimeZone", ex);
298 @Override
299 public void restart() throws Exception {
300 if (serverState != ServerState.RUNNING) {
301 throw new IllegalStateException("Cannot restart a server that is not currently running.");
303 servers.shutdown();
304 backendContainer.shutdownAll();
305 servers.createConnections();
306 servers.startup();
307 backendContainer.startupAll(apiProxyLocal);
310 @Override
311 public void shutdown() throws Exception {
312 if (serverState != ServerState.RUNNING) {
313 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
315 servers.shutdown();
316 backendContainer.shutdownAll();
317 ApiProxy.setDelegate(null);
318 apiProxyLocal = null;
319 serverState = ServerState.SHUTDOWN;
322 @Override
323 public int getPort() {
324 reportDeferredConfigurationException();
325 return servers.getMainContainer().getPort();
328 protected void reportDeferredConfigurationException() {
329 if (configurationException != null) {
330 throw new AppEngineConfigException("Invalid configuration", configurationException);
334 @Override
335 public AppContext getAppContext() {
336 reportDeferredConfigurationException();
337 return servers.getMainContainer().getAppContext();
340 @Override
341 public AppContext getCurrentAppContext() {
342 AppContext result = null;
343 Environment env = ApiProxy.getCurrentEnvironment();
344 if (env != null && env.getVersionId() != null) {
345 String serverName = LocalEnvironment.getServerName(env.getVersionId());
346 result = servers.getServer(serverName).getMainContainer().getAppContext();
348 return result;
351 @Override
352 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch) {
353 if (configurationException == null) {
354 applicationConfigurationManager.setEnvironmentVariableMismatchReportingPolicy(
355 throwOnMismatch ? MismatchReportingPolicy.EXCEPTION : MismatchReportingPolicy.LOG);
360 * We're happy with the default logging behavior, which is to
361 * install a {@link ConsoleHandler} at the root level. The only
362 * issue is that we want its level to be FINEST to be consistent
363 * with our runtime environment.
365 * <p>Note that this does not mean that any fine messages will be
366 * logged by default -- each Logger still defaults to INFO.
367 * However, it is sufficient to call {@link Logger#setLevel(Level)}
368 * to adjust the level.
370 private void initializeLogging() {
371 for (Handler handler : Logger.getLogger("").getHandlers()) {
372 if (handler instanceof ConsoleHandler) {
373 handler.setLevel(Level.FINEST);
378 ServerState getServerState() {
379 return serverState;