Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / tools / development / DevAppServerImpl.java
blobf2501886d559b88553b1bd9261f7515785274155
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.tools.development.EnvironmentVariableChecker.MismatchReportingPolicy;
8 import com.google.appengine.tools.info.SdkInfo;
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;
14 import com.google.apphosting.utils.config.EarHelper;
15 import com.google.common.base.Joiner;
16 import com.google.common.collect.ImmutableList;
17 import com.google.common.collect.ImmutableMap;
18 import com.google.common.collect.ImmutableSet;
20 import java.io.File;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Method;
23 import java.net.BindException;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.TimeZone;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.logging.ConsoleHandler;
30 import java.util.logging.Handler;
31 import java.util.logging.Level;
32 import java.util.logging.Logger;
34 /**
35 * {@code DevAppServer} launches a local Jetty server (by default) with a single
36 * hosted web application. It can be invoked from the command-line by
37 * providing the path to the directory in which the application resides as the
38 * only argument.
41 class DevAppServerImpl implements DevAppServer {
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>();
48 enum ServerState { INITIALIZING, RUNNING, STOPPING, SHUTDOWN }
50 /**
51 * The current state of the server.
53 private ServerState serverState = ServerState.INITIALIZING;
55 /**
56 * Contains the backend servers configured as part of the "Servers" feature.
57 * Each backend server is started on a separate port and keep their own
58 * internal state. Memcache, datastore, and other API services are shared by
59 * all servers, including the "main" server.
61 private final BackendContainer backendContainer;
63 /**
64 * The api proxy we created when we started the web containers. Not initialized until after
65 * {@link #start()} is called.
67 private ApiProxyLocal apiProxyLocal;
69 /**
70 * We defer reporting construction time configuration exceptions until
71 * {@link #start()} for compatibility.
73 private final AppEngineConfigException configurationException;
75 /**
76 * Constructs a development application server that runs the single
77 * application located in the given directory. The application is configured
78 * via <webXmlLocation> and the {@link AppEngineWebXml}
79 * instance returned by the provided {@link AppEngineWebXmlReader}.
81 * @param appDir The location of the application to run.
82 * @param externalResourceDir If not {@code null}, a resource directory external to the appDir.
83 * This will be searched before appDir when looking for resources.
84 * @param webXmlLocation The location of a file whose format complies with
85 * http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd. If {@code null},
86 * defaults to <appDir>/WEB-INF/web.xml
87 * @param appEngineWebXmlLocation The name of the app engine config file. If
88 * {@code null}, defaults to <appDir>/WEB-INF/appengine-web.xml
89 * @param address The address on which to run
90 * @param port The port on which to run
91 * @param useCustomStreamHandler If {@code true}, install
92 * {@link StreamHandlerFactory}. This is "normal" behavior for the dev app
93 * server.
94 * @param containerConfigProperties Additional properties used in the
95 * configuration of the specific container implementation.
97 public DevAppServerImpl(File appDir, File externalResourceDir, File webXmlLocation,
98 File appEngineWebXmlLocation, String address, int port, boolean useCustomStreamHandler,
99 Map<String, Object> containerConfigProperties) {
100 String serverInfo = ContainerUtils.getServerInfo();
101 if (useCustomStreamHandler) {
102 StreamHandlerFactory.install();
104 DevSocketImplFactory.install();
106 ApplicationConfigurationManager tempManager = null;
107 try {
108 if (EarHelper.isEar(appDir.getAbsolutePath())) {
109 tempManager = ApplicationConfigurationManager.newEarConfigurationManager(appDir,
110 SdkInfo.getLocalVersion().getRelease());
111 } else {
112 tempManager = ApplicationConfigurationManager.newWarConfigurationManager(
113 appDir, appEngineWebXmlLocation, webXmlLocation, externalResourceDir,
114 SdkInfo.getLocalVersion().getRelease());
116 } catch (AppEngineConfigException configurationException) {
117 servers = null;
118 applicationConfigurationManager = null;
119 backendContainer = null;
120 this.configurationException = configurationException;
121 return;
123 this.applicationConfigurationManager = tempManager;
124 this.servers = Servers.createServers(applicationConfigurationManager, serverInfo,
125 externalResourceDir, address, port, containerConfigProperties, this);
126 backendContainer = BackendServers.getInstance();
127 backendContainer.init(address,
128 applicationConfigurationManager.getPrimaryServerConfigurationHandle(),
129 externalResourceDir, containerConfigProperties, this);
130 configurationException = null;
134 * Sets the properties that will be used by the local services to
135 * configure themselves. This method must be called before the server
136 * has been started.
138 * @param properties a, maybe {@code null}, set of properties.
140 * @throws IllegalStateException if the server has already been started.
142 @Override
143 public void setServiceProperties(Map<String,String> properties) {
144 if (serverState != ServerState.INITIALIZING) {
145 String msg = "Cannot set service properties after the server has been started.";
146 throw new IllegalStateException(msg);
149 if (configurationException == null) {
150 serviceProperties = new ConcurrentHashMap<String, String>(properties);
151 backendContainer.setServiceProperties(properties);
155 Map<String, String> getServiceProperties() {
156 return serviceProperties;
160 * Starts the server.
162 * @throws IllegalStateException If the server has already been started or
163 * shutdown.
164 * @throws AppEngineConfigException If no WEB-INF directory can be found or
165 * WEB-INF/appengine-web.xml does not exist.
167 @Override
168 public void start() throws Exception {
169 if (serverState != ServerState.INITIALIZING) {
170 throw new IllegalStateException("Cannot start a server that has already been started.");
173 reportDeferredConfigurationException();
175 initializeLogging();
176 servers.configure();
177 servers.createConnections();
179 ApiProxyLocalFactory factory = new ApiProxyLocalFactory();
180 apiProxyLocal = factory.create(servers.getLocalServerEnvironment());
181 setInboundServicesProperty();
182 apiProxyLocal.setProperties(serviceProperties);
183 ApiProxy.setDelegate(apiProxyLocal);
185 TimeZone currentTimeZone = null;
187 try {
188 currentTimeZone = setServerTimeZone();
189 servers.startup();
191 Environment env = ApiProxy.getCurrentEnvironment();
192 env.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:" + getPort());
194 backendContainer.startupAll(apiProxyLocal);
195 } catch (BindException ex) {
196 System.err.println();
197 System.err.println("************************************************");
198 System.err.println("Could not open the requested socket: " + ex.getMessage());
199 System.err.println("Try overriding --address and/or --port.");
200 System.exit(2);
201 } finally {
202 ApiProxy.clearEnvironmentForCurrentThread();
203 restoreLocalTimeZone(currentTimeZone);
205 serverState = ServerState.RUNNING;
206 logger.info("Dev App Server is now running");
209 public void setInboundServicesProperty() {
210 ImmutableSet.Builder<String> setBuilder = ImmutableSet.builder();
211 for (ApplicationConfigurationManager.ServerConfigurationHandle serverConfigurationHandle :
212 applicationConfigurationManager.getServerConfigurationHandles()) {
213 setBuilder.addAll(
214 serverConfigurationHandle.getModule().getAppEngineWebXml().getInboundServices());
217 serviceProperties.put("appengine.dev.inbound-services",
218 Joiner.on(",").useForNull("null").join(setBuilder.build()));
222 * Change the TimeZone for the current thread. By calling this method before
223 * {@link ContainerService#startup()} start}, we set the default TimeZone for the
224 * DevAppServer and all of its related services.
226 * @return the previously installed ThreadLocal TimeZone
228 private TimeZone setServerTimeZone() {
229 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
230 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
231 return null;
234 TimeZone utc = TimeZone.getTimeZone("UTC");
235 assert utc.getID().equals("UTC") : "Unable to retrieve the UTC TimeZone";
237 try {
238 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
239 f.setAccessible(true);
240 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
241 Method getZone = ThreadLocal.class.getMethod("get");
242 TimeZone previousZone = (TimeZone) getZone.invoke(tl);
243 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
244 setZone.invoke(tl, utc);
245 return previousZone;
246 } catch (Exception e) {
247 try {
248 Method getZone = TimeZone.class.getDeclaredMethod("getDefaultInAppContext");
249 getZone.setAccessible(true);
250 TimeZone previousZone = (TimeZone) getZone.invoke(null);
251 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
252 setZone.setAccessible(true);
253 setZone.invoke(null, utc);
254 return previousZone;
255 } catch (Exception ex) {
256 throw new RuntimeException("Unable to set the TimeZone to UTC", ex);
262 * Restores the ThreadLocal TimeZone to {@code timeZone}.
264 private void restoreLocalTimeZone(TimeZone timeZone) {
265 String sysTimeZone = serviceProperties.get("appengine.user.timezone.impl");
266 if (sysTimeZone != null && sysTimeZone.trim().length() > 0) {
267 return;
270 try {
271 Field f = TimeZone.class.getDeclaredField("defaultZoneTL");
272 f.setAccessible(true);
273 ThreadLocal<?> tl = (ThreadLocal<?>) f.get(null);
274 Method setZone = ThreadLocal.class.getMethod("set", Object.class);
275 setZone.invoke(tl, timeZone);
276 } catch (Exception e) {
277 try {
278 Method setZone = TimeZone.class.getDeclaredMethod("setDefaultInAppContext", TimeZone.class);
279 setZone.setAccessible(true);
280 setZone.invoke(null, timeZone);
281 } catch (Exception ex) {
282 throw new RuntimeException("Unable to restore the previous TimeZone", ex);
287 @Override
288 public void restart() throws Exception {
289 if (serverState != ServerState.RUNNING) {
290 throw new IllegalStateException("Cannot restart a server that is not currently running.");
292 servers.shutdown();
293 backendContainer.shutdownAll();
294 servers.createConnections();
295 servers.startup();
296 backendContainer.startupAll(apiProxyLocal);
299 @Override
300 public void shutdown() throws Exception {
301 if (serverState != ServerState.RUNNING) {
302 throw new IllegalStateException("Cannot shutdown a server that is not currently running.");
304 servers.shutdown();
305 backendContainer.shutdownAll();
306 ApiProxy.setDelegate(null);
307 apiProxyLocal = null;
308 serverState = ServerState.SHUTDOWN;
311 @Override
312 public int getPort() {
313 reportDeferredConfigurationException();
314 return servers.getMainContainer().getPort();
317 protected void reportDeferredConfigurationException() {
318 if (configurationException != null) {
319 throw new AppEngineConfigException("Invalid configuration", configurationException);
323 @Override
324 public AppContext getAppContext() {
325 reportDeferredConfigurationException();
326 return servers.getMainContainer().getAppContext();
329 @Override
330 public AppContext getCurrentAppContext() {
331 AppContext result = null;
332 Environment env = ApiProxy.getCurrentEnvironment();
333 if (env != null && env.getVersionId() != null) {
334 String serverName = LocalEnvironment.getServerName(env.getVersionId());
335 result = servers.getServer(serverName).getMainContainer().getAppContext();
337 return result;
340 @Override
341 public void setThrowOnEnvironmentVariableMismatch(boolean throwOnMismatch) {
342 if (configurationException == null) {
343 applicationConfigurationManager.setEnvironmentVariableMismatchReportingPolicy(
344 throwOnMismatch ? MismatchReportingPolicy.EXCEPTION : MismatchReportingPolicy.LOG);
349 * We're happy with the default logging behavior, which is to
350 * install a {@link ConsoleHandler} at the root level. The only
351 * issue is that we want its level to be FINEST to be consistent
352 * with our runtime environment.
354 * <p>Note that this does not mean that any fine messages will be
355 * logged by default -- each Logger still defaults to INFO.
356 * However, it is sufficient to call {@link Logger#setLevel(Level)}
357 * to adjust the level.
359 private void initializeLogging() {
360 for (Handler handler : Logger.getLogger("").getHandlers()) {
361 if (handler instanceof ConsoleHandler) {
362 handler.setLevel(Level.FINEST);
367 ServerState getServerState() {
368 return serverState;
371 static class Servers {
372 private final List<Server> servers;
373 private final Map<String, Server> serverNameToServerMap;
375 static Servers createServers(ApplicationConfigurationManager applicationConfigurationManager,
376 String serverInfo, File externalResourceDir, String address, int port, Map<String,
377 Object> containerConfigProperties, DevAppServerImpl devAppServer) {
379 ImmutableList.Builder<Server> builder = ImmutableList.builder();
380 LocalServerEnvironment mainEnvironment = null;
381 for (ApplicationConfigurationManager.ServerConfigurationHandle serverConfigurationHandle :
382 applicationConfigurationManager.getServerConfigurationHandles()) {
383 AppEngineWebXml appEngineWebXml =
384 serverConfigurationHandle.getModule().getAppEngineWebXml();
385 Server server = null;
386 if (!appEngineWebXml.getBasicScaling().isEmpty()) {
387 throw new AppEngineConfigException("Basic scaling servers are currently not supported");
388 } else if (!appEngineWebXml.getManualScaling().isEmpty()) {
389 server = new ManualServer(serverConfigurationHandle, serverInfo, address,
390 containerConfigProperties, devAppServer, appEngineWebXml);
391 } else {
392 server = new AutomaticServer(serverConfigurationHandle, serverInfo, externalResourceDir,
393 address, port, containerConfigProperties, devAppServer);
395 builder.add(server);
397 port = 0;
398 externalResourceDir = null;
400 return new Servers(builder.build());
403 void shutdown() throws Exception {
404 for (Server server : servers) {
405 server.shutdown();
409 void configure() throws Exception {
410 for (Server server : servers) {
411 server.configure();
415 void createConnections() throws Exception {
416 for (Server server : servers) {
417 server.createConnection();
421 void startup() throws Exception {
422 for (Server server : servers) {
423 server.startup();
427 ContainerService getMainContainer() {
428 return servers.get(0).getMainContainer();
431 private Servers(List<Server> servers) {
432 if (servers.size() < 1) {
433 throw new IllegalArgumentException("servers must not be empty.");
435 this.servers = servers;
437 ImmutableMap.Builder<String, Server> mapBuilder = ImmutableMap.builder();
438 for (Server server : this.servers) {
439 mapBuilder.put(server.getServerName(), server);
441 serverNameToServerMap = mapBuilder.build();
444 LocalServerEnvironment getLocalServerEnvironment() {
445 return servers.get(0).getLocalServerEnvironment();
448 Server getServer(String serverName) {
449 return serverNameToServerMap.get(serverName);