// Copyright 2008 Google Inc. All Rights Reserved.
package com.google.appengine.tools.development;
import static com.google.appengine.tools.development.LocalEnvironment.DEFAULT_VERSION_HOSTNAME;
import com.google.appengine.api.backends.BackendService;
import com.google.appengine.tools.development.ApplicationConfigurationManager.ModuleConfigurationHandle;
import com.google.appengine.tools.info.SdkImplInfo;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.ClassPathBuilder;
import com.google.apphosting.utils.config.WebModule;
import com.google.apphosting.utils.config.WebXml;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.Permissions;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.GuardedBy;
/**
* Common implementation for the {@link ContainerService} interface.
*
* There should be no reference to any third-party servlet container from here.
*
*/
abstract class AbstractContainerService implements ContainerService {
private static final Logger log = Logger.getLogger(AbstractContainerService.class.getName());
protected static final String _AH_URL_RELOAD = "/_ah/reloadwebapp";
private static final String USER_CODE_CLASSPATH_MANAGER_PROP =
"devappserver.userCodeClasspathManager";
private static final String USER_CODE_CLASSPATH = USER_CODE_CLASSPATH_MANAGER_PROP + ".classpath";
private static final String USER_CODE_REQUIRES_WEB_INF =
USER_CODE_CLASSPATH_MANAGER_PROP + ".requiresWebInf";
public static final String PORT_MAPPING_PROVIDER_PROP = "devappserver.portMappingProvider";
protected ModuleConfigurationHandle moduleConfigurationHandle;
protected String devAppServerVersion;
protected File appDir;
protected File externalResourceDir;
/**
* The location of web.xml. If not provided, defaults to
* <appDir>/WEB-INF/web.xml
*/
protected File webXmlLocation;
/**
* The hostname on which the module instance is listening for http requests.
*/
protected String hostName;
/**
* The network address on which the module instance is listening for http requests.
*/
protected String address;
/**
* The port on which the module instance is listening for http requests.
*/
protected int port;
/**
* The 0 based index for this instance or {@link LocalEnvironment#MAIN_INSTANCE}.
*/
protected int instance;
/**
* A reference to the parent DevAppServer that configured this container.
*/
protected DevAppServer devAppServer;
protected AppEngineWebXml appEngineWebXml;
protected WebXml webXml;
private String backendName;
private int backendInstance;
private PortMappingProvider portMappingProvider = new PortMappingProvider() {
@Override
public Map<String, String> getPortMapping() {
return Collections.emptyMap();
}
};
@GuardedBy("AbstractContainerService.class")
private static SystemPropertiesManager systemPropertiesManager;
/**
* Latch that will open once the module instance is fully initialized.
* TODO(user): This is used by some services but only for the
* default instance of the default module. Investigate. Does module
* start/stop cause issues? There is some issue with tasks during
* Servlet initialization.
*/
private CountDownLatch moduleInitLatch;
/**
* Not initialized until {@link #startup()} has been called.
*/
protected ApiProxy.Delegate<?> apiProxyDelegate;
protected UserCodeClasspathManager userCodeClasspathManager;
protected ModulesFilterHelper modulesFilterHelper;
@Override
public final LocalServerEnvironment configure(String devAppServerVersion, final String address,
int port, final ModuleConfigurationHandle moduleConfigurationHandle, File externalResourceDir,
Map<String, Object> containerConfigProperties, int instance, DevAppServer devAppServer) {
this.devAppServerVersion = devAppServerVersion;
this.moduleConfigurationHandle = moduleConfigurationHandle;
extractFieldsFromWebModule(moduleConfigurationHandle.getModule());
this.externalResourceDir = externalResourceDir;
this.address = address;
this.port = port;
this.moduleInitLatch = new CountDownLatch(1);
this.hostName = "localhost";
this.devAppServer = devAppServer;
if ("0.0.0.0".equals(address)) {
try {
InetAddress localhost = InetAddress.getLocalHost();
this.hostName = localhost.getHostName();
} catch (UnknownHostException ex) {
log.log(Level.WARNING,
"Unable to determine hostname - defaulting to localhost.");
}
}
this.userCodeClasspathManager = newUserCodeClasspathProvider(containerConfigProperties);
this.modulesFilterHelper = (ModulesFilterHelper)
containerConfigProperties.get(DevAppServerImpl.MODULES_FILTER_HELPER_PROPERTY);
this.backendName =
(String) containerConfigProperties.get(BackendService.BACKEND_ID_ENV_ATTRIBUTE);
Object rawBackendInstance =
containerConfigProperties.get(BackendService.INSTANCE_ID_ENV_ATTRIBUTE);
this.backendInstance =
rawBackendInstance == null ? -1 : ((Integer) rawBackendInstance).intValue();
PortMappingProvider callersPortMappingProvider =
(PortMappingProvider) containerConfigProperties.get(PORT_MAPPING_PROVIDER_PROP);
if (callersPortMappingProvider == null) {
log.warning("Null value for containerConfigProperties.get("
+ PORT_MAPPING_PROVIDER_PROP + ")");
} else {
this.portMappingProvider = callersPortMappingProvider;
}
this.instance = instance;
return new LocalServerEnvironment() {
@Override
public File getAppDir() {
return moduleConfigurationHandle.getModule().getApplicationDirectory();
}
@Override
public String getAddress() {
return address;
}
@Override
public String getHostName() {
return hostName;
}
@Override
public int getPort() {
return AbstractContainerService.this.port;
}
@Override
public void waitForServerToStart() throws InterruptedException {
moduleInitLatch.await();
}
@Override
public boolean simulateProductionLatencies() {
return false;
}
@Override
public boolean enforceApiDeadlines() {
return !Boolean.getBoolean("com.google.appengine.disable_api_deadlines");
}
};
}
@Override
public void setApiProxyDelegate(ApiProxy.Delegate<?> apiProxyDelegate) {
this.apiProxyDelegate = apiProxyDelegate;
}
/**
* @param webModule
*/
protected void extractFieldsFromWebModule(WebModule webModule) {
this.appDir = webModule.getApplicationDirectory();
this.webXml = webModule.getWebXml();
this.webXmlLocation = webModule.getWebXmlFile();
this.appEngineWebXml = webModule.getAppEngineWebXml();
}
/**
* Constructs a {@link UserCodeClasspathManager} from the given properties.
*/
private static UserCodeClasspathManager newUserCodeClasspathProvider(
Map<String, Object> containerConfigProperties) {
if (containerConfigProperties.containsKey(USER_CODE_CLASSPATH_MANAGER_PROP)) {
@SuppressWarnings("unchecked")
final Map<String, Object> userCodeClasspathManagerProps =
(Map<String, Object>) containerConfigProperties.get(USER_CODE_CLASSPATH_MANAGER_PROP);
return new UserCodeClasspathManager() {
@SuppressWarnings("unchecked")
@Override
public Collection<URL> getUserCodeClasspath(File root) {
return (Collection<URL>) userCodeClasspathManagerProps.get(USER_CODE_CLASSPATH);
}
@Override
public boolean requiresWebInf() {
return (Boolean) userCodeClasspathManagerProps.get(USER_CODE_REQUIRES_WEB_INF);
}
};
}
return new WebAppUserCodeClasspathManager();
}
@Override
public final void createConnection() throws Exception {
connectContainer();
}
@Override
public final void startup() throws Exception {
Environment prevEnvironment = ApiProxy.getCurrentEnvironment();
try {
initContext();
if (appEngineWebXml == null) {
throw new IllegalStateException("initContext failed to initialize appEngineWebXml.");
}
startContainer();
startHotDeployScanner();
moduleInitLatch.countDown();
} finally {
ApiProxy.setEnvironmentForCurrentThread(prevEnvironment);
}
}
@Override
public final void shutdown() throws Exception {
stopHotDeployScanner();
stopContainer();
moduleConfigurationHandle.restoreSystemProperties();
}
@Override
public Map<String, String> getServiceProperties() {
return ImmutableMap.of("appengine.dev.inbound-services",
Joiner.on(",").useForNull("null").join(appEngineWebXml.getInboundServices()));
}
/**
* Set up the webapp context in a container specific way.
* <p>Note that {@link #initContext()} is required to call
* {@link #installLocalInitializationEnvironment()} for the service to be correctly
* initialized.
*
* @return the effective webapp directory.
*/
protected abstract File initContext() throws IOException;
/**
* Creates the servlet container's network connections.
*/
protected abstract void connectContainer() throws Exception;
/**
* Start up the servlet container runtime.
*/
protected abstract void startContainer() throws Exception;
/**
* Stop the servlet container runtime.
*/
protected abstract void stopContainer() throws Exception;
/**
* Start up the hot-deployment scanner.
*/
protected abstract void startHotDeployScanner() throws Exception;
/**
* Stop the hot-deployment scanner.
*/
protected abstract void stopHotDeployScanner() throws Exception;
/**
* Re-deploy the current webapp context in a container specific way,
* while taking into account possible appengine-web.xml change too,
* without restarting the module instance.
*/
protected abstract void reloadWebApp() throws Exception;
@Override
public String getAddress() {
return address;
}
@Override
public AppEngineWebXml getAppEngineWebXmlConfig(){
return appEngineWebXml;
}
@Override
public int getPort() {
return port;
}
@Override
public String getHostName() {
return hostName;
}
protected Permissions getUserPermissions() {
return appEngineWebXml.getUserPermissions();
}
/**
* Installs a {@link LocalInitializationEnvironment} with
* {@link ApiProxy#setEnvironmentForCurrentThread}.
* <p>
* Filters and servlets get initialized when we call server.start(). If any of
* those filters and servlets need access to the current execution environment
* they'll call ApiProxy.getCurrentEnvironment(). We set a special initialization
* environment so that there is an environment available when this happens.
* <p>
* This depends on port which may not be set to its final value until {@link #connectContainer()}
* is called.
*/
protected void installLocalInitializationEnvironment() {
installLocalInitializationEnvironment(appEngineWebXml, instance, port, devAppServer.getPort(),
backendName, backendInstance, portMappingProvider.getPortMapping());
}
/** Returns {@code true} if appengine-web.xml <sessions-enabled> is true. */
protected boolean isSessionsEnabled() {
return appEngineWebXml.getSessionsEnabled();
}
/**
* Gets all of the URLs that should be added to the classpath for an
* application located at {@code root}.
*/
protected URL[] getClassPathForApp(File root) {
ClassPathBuilder classPathBuilder =
new ClassPathBuilder(appEngineWebXml.getClassLoaderConfig());
classPathBuilder.addUrls(SdkImplInfo.getAgentRuntimeLibs());
classPathBuilder.addUrls(userCodeClasspathManager.getUserCodeClasspath(root));
classPathBuilder.addUrls(SdkImplInfo.getUserJspLibs());
classPathBuilder.addUrls(SdkImplInfo.getWebApiToolLibs());
return getUrls(classPathBuilder);
}
private static URL[] getUrls(ClassPathBuilder classPathBuilder) {
URL[] urls = classPathBuilder.getUrls();
String message = classPathBuilder.getLogMessage();
if (!message.isEmpty()) {
log.warning(message);
}
return urls;
}
/**
* Sets up an {@link com.google.apphosting.api.ApiProxy.Environment} for container
* initialization.
*/
public static void installLocalInitializationEnvironment(AppEngineWebXml appEngineWebXml,
int instance, int port,
int defaultModuleMainPort,
String backendName,
int backendInstance,
Map<String, String> portMapping) {
Environment environment = new LocalInitializationEnvironment(
appEngineWebXml.getAppId(), WebModule.getModuleName(appEngineWebXml),
appEngineWebXml.getMajorVersionId(), instance, port);
environment.getAttributes().put(DEFAULT_VERSION_HOSTNAME, "localhost:"
+ defaultModuleMainPort);
ApiProxy.setEnvironmentForCurrentThread(environment);
DevAppServerModulesFilter.injectBackendServiceCurrentApiInfo(backendName, backendInstance,
portMapping);
}
/**
* A fake {@link LocalEnvironment} implementation that is used during the
* initialization of the Development AppServer.
*/
public static class LocalInitializationEnvironment extends LocalEnvironment {
public LocalInitializationEnvironment(String appId, String moduleName, String majorVersionId,
int instance, int port) {
super(appId, moduleName, majorVersionId, instance, port, null);
}
@Override
public String getEmail() {
return null;
}
@Override
public boolean isLoggedIn() {
return false;
}
@Override
public boolean isAdmin() {
return false;
}
}
/**
* Provider for the 'portMapping'.
* <p>
* The provided map contains an entry for every backend instance.
* For the main instance the key is the backend name and the value is
* the hostname:port for sending http requests to the instance (i.e.
* bob->127.0.0.1:1234). For other instances the key is
* instance-id.hostname and the value is again the hostname:port for
* sending http requests to the instance (i.e. 2.bob->127.0.0.1:1234).
*/
interface PortMappingProvider {
Map<String, String> getPortMapping();
}
}