You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by al...@apache.org on 2015/08/12 17:55:40 UTC

[22/35] incubator-brooklyn git commit: [BROOKLYN-162] package rename to org.apache.brooklyn: software/webapp

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/nodejs/NodeJsWebAppSshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/nodejs/NodeJsWebAppSshDriver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/nodejs/NodeJsWebAppSshDriver.java
deleted file mode 100644
index 3beee70..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/nodejs/NodeJsWebAppSshDriver.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.nodejs;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.basic.AbstractSoftwareProcessSshDriver;
-import brooklyn.entity.basic.Attributes;
-import brooklyn.entity.basic.SoftwareProcess;
-import brooklyn.entity.webapp.WebAppService;
-import brooklyn.location.basic.SshMachineLocation;
-import brooklyn.util.collections.MutableList;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.file.ArchiveUtils;
-import brooklyn.util.net.Networking;
-import brooklyn.util.os.Os;
-import brooklyn.util.ssh.BashCommands;
-import brooklyn.util.text.Strings;
-
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Lists;
-
-public class NodeJsWebAppSshDriver extends AbstractSoftwareProcessSshDriver implements NodeJsWebAppDriver {
-
-    private static final Logger LOG = LoggerFactory.getLogger(NodeJsWebAppService.class);
-
-    public NodeJsWebAppSshDriver(NodeJsWebAppServiceImpl entity, SshMachineLocation machine) {
-        super(entity, machine);
-    }
-
-    public NodeJsWebAppServiceImpl getEntity() {
-        return (NodeJsWebAppServiceImpl) super.getEntity();
-    }
-
-    @Override
-    public Integer getHttpPort() {
-        return getEntity().getAttribute(Attributes.HTTP_PORT);
-    }
-
-    @Override
-    public String getAppDir() {
-        return Os.mergePaths(getRunDir(), getEntity().getConfig(NodeJsWebAppService.APP_NAME));
-    }
-
-    @Override
-    public void postLaunch() {
-        String rootUrl = String.format("http://%s:%d/", getHostname(), getHttpPort());
-        entity.setAttribute(Attributes.MAIN_URI, URI.create(rootUrl));
-        entity.setAttribute(WebAppService.ROOT_URL, rootUrl);
-    }
-
-    protected Map<String, Integer> getPortMap() {
-        return MutableMap.of("http", getHttpPort());
-    }
-
-    @Override
-    public Set<Integer> getPortsUsed() {
-        return ImmutableSet.<Integer>builder()
-                .addAll(super.getPortsUsed())
-                .addAll(getPortMap().values())
-                .build();
-    }
-
-    // TODO Suggest that other entities follow this pattern as well: check for port availability early
-    // to report failures early, and in case getShellEnvironment() tries to convert any null port numbers
-    // to int.
-    @Override
-    public void preInstall() {
-        super.preInstall();
-        Networking.checkPortsValid(getPortMap());
-    }
-    
-    @Override
-    public void install() {
-        LOG.info("Installing Node.JS {}", getEntity().getConfig(SoftwareProcess.SUGGESTED_VERSION));
-
-        List<String> commands = MutableList.<String>builder()
-                .add(BashCommands.INSTALL_CURL)
-                .add(BashCommands.ifExecutableElse0("apt-get", BashCommands.chain(
-                        BashCommands.installPackage("software-properties-common python-software-properties python g++ make"),
-                        BashCommands.sudo("add-apt-repository ppa:chris-lea/node.js"))))
-                .add(BashCommands.installPackage(MutableMap.of("yum", "git nodejs npm", "apt", "git-core nodejs"), null))
-                .add("mkdir \"$HOME/.npm\"")
-                .add(BashCommands.sudo("npm install -g n"))
-                .add(BashCommands.sudo("n " + getEntity().getConfig(SoftwareProcess.SUGGESTED_VERSION)))
-                .build();
-
-        newScript(INSTALLING)
-                .body.append(commands)
-                .execute();
-    }
-
-    @Override
-    public void customize() {
-        List<String> commands = Lists.newLinkedList();
-
-        String gitRepoUrl = getEntity().getConfig(NodeJsWebAppService.APP_GIT_REPOSITORY_URL);
-        String archiveUrl = getEntity().getConfig(NodeJsWebAppService.APP_ARCHIVE_URL);
-        String appName = getEntity().getConfig(NodeJsWebAppService.APP_NAME);
-        if (Strings.isNonBlank(gitRepoUrl) && Strings.isNonBlank(archiveUrl)) {
-            throw new IllegalStateException("Only one of Git or archive URL must be set for " + getEntity());
-        } else if (Strings.isNonBlank(gitRepoUrl)) {
-            commands.add(String.format("git clone %s %s", gitRepoUrl, appName));
-            commands.add(String.format("cd %s", appName));
-        } else if (Strings.isNonBlank(archiveUrl)) {
-            ArchiveUtils.deploy(archiveUrl, getMachine(), getRunDir());
-        } else {
-            throw new IllegalStateException("At least one of Git or archive URL must be set for " + getEntity());
-        }
-
-        commands.add(BashCommands.ifFileExistsElse1("package.json", "npm install"));
-        List<String> packages = getEntity().getConfig(NodeJsWebAppService.NODE_PACKAGE_LIST);
-        if (packages != null && packages.size() > 0) {
-            commands.add(BashCommands.sudo("npm install -g " + Joiner.on(' ').join(packages)));
-        }
-
-        newScript(CUSTOMIZING)
-                .body.append(commands)
-                .execute();
-    }
-
-    @Override
-    public void launch() {
-        List<String> commands = Lists.newLinkedList();
-
-        String appName = getEntity().getConfig(NodeJsWebAppService.APP_NAME);
-        String appFile = getEntity().getConfig(NodeJsWebAppService.APP_FILE);
-        String appCommand = getEntity().getConfig(NodeJsWebAppService.APP_COMMAND);
-        String appCommandLine = getEntity().getConfig(NodeJsWebAppService.APP_COMMAND_LINE);
-
-        if (Strings.isBlank(appCommandLine)) {
-            appCommandLine = appCommand + " " + appFile;
-        }
-
-        // Ensure global NPM modules are on Node's path.
-        commands.add("export NODE_PATH=\"$NODE_PATH:$(npm root -g)\"");
-        commands.add(String.format("cd %s", Os.mergePathsUnix(getRunDir(), appName)));
-        commands.add("nohup " + appCommandLine + " > console.out 2>&1 &");
-
-        newScript(MutableMap.of(USE_PID_FILE, true), LAUNCHING)
-                .body.append(commands)
-                .execute();
-    }
-
-    @Override
-    public boolean isRunning() {
-        return newScript(MutableMap.of(USE_PID_FILE, true), CHECK_RUNNING).execute() == 0;
-    }
-
-    @Override
-    public void stop() {
-        newScript(MutableMap.of(USE_PID_FILE, true), STOPPING).execute();
-    }
-
-    @Override
-    public Map<String, String> getShellEnvironment() {
-        return MutableMap.<String, String>builder().putAll(super.getShellEnvironment())
-                .put("PORT", Integer.toString(getHttpPort()))
-                .build();
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7Driver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7Driver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7Driver.java
deleted file mode 100644
index d5a98ac..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7Driver.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-@Deprecated
-public interface Tomcat7Driver extends TomcatDriver {
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7SshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7SshDriver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7SshDriver.java
deleted file mode 100644
index 7fc6150..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat7SshDriver.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import brooklyn.location.basic.SshMachineLocation;
-
-@Deprecated
-public class Tomcat7SshDriver extends TomcatSshDriver implements Tomcat7Driver {
-
-   public Tomcat7SshDriver(TomcatServerImpl entity, SshMachineLocation machine) {
-       super(entity, machine);
-   }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8Server.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8Server.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8Server.java
deleted file mode 100644
index 6262ac7..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8Server.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import org.apache.brooklyn.catalog.Catalog;
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.SoftwareProcess;
-import brooklyn.entity.proxying.ImplementedBy;
-import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
-import brooklyn.util.flags.SetFromFlag;
-import brooklyn.util.javalang.JavaClassNames;
-
-/**
- * An {@link brooklyn.entity.Entity} that represents a single Tomcat instance.
- */
-@Catalog(name="Tomcat Server",
-        description="Apache Tomcat is an open source software implementation of the Java Servlet and JavaServer Pages technologies",
-        iconUrl="classpath:///tomcat-logo.png")
-@ImplementedBy(Tomcat8ServerImpl.class)
-public interface Tomcat8Server extends TomcatServer {
-
-    @SetFromFlag("version")
-    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "8.0.22");
-
-    @SetFromFlag("downloadUrl")
-    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new BasicAttributeSensorAndConfigKey<String>(
-            SoftwareProcess.DOWNLOAD_URL, "http://download.nextag.com/apache/tomcat/tomcat-8/v${version}/bin/apache-tomcat-${version}.tar.gz");
-
-    @SetFromFlag("server.xml")
-    ConfigKey<String> SERVER_XML_RESOURCE = ConfigKeys.newStringConfigKey(
-            "tomcat.serverxml", "The file to template and use as the Tomcat process' server.xml",
-            JavaClassNames.resolveClasspathUrl(Tomcat8Server.class, "tomcat8-server.xml"));
-
-    @SetFromFlag("web.xml")
-    ConfigKey<String> WEB_XML_RESOURCE = ConfigKeys.newStringConfigKey(
-            "tomcat.webxml", "The file to template and use as the Tomcat process' web.xml",
-            JavaClassNames.resolveClasspathUrl(Tomcat8Server.class, "tomcat8-web.xml"));
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8ServerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8ServerImpl.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8ServerImpl.java
deleted file mode 100644
index 6858f51..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/Tomcat8ServerImpl.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-/**
- * An {@link brooklyn.entity.Entity} that represents a single Tomcat instance.
- */
-public class Tomcat8ServerImpl extends TomcatServerImpl implements Tomcat8Server {
-}
-

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatDriver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatDriver.java
deleted file mode 100644
index 00b4628..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatDriver.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import brooklyn.entity.webapp.JavaWebAppDriver;
-
-public interface TomcatDriver extends JavaWebAppDriver {
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServer.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServer.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServer.java
deleted file mode 100644
index 24cd729..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServer.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import org.apache.brooklyn.catalog.Catalog;
-import brooklyn.config.ConfigKey;
-import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.SoftwareProcess;
-import brooklyn.entity.java.UsesJmx;
-import brooklyn.entity.proxying.ImplementedBy;
-import brooklyn.entity.trait.HasShortName;
-import brooklyn.entity.webapp.JavaWebAppSoftwareProcess;
-import brooklyn.event.AttributeSensor;
-import brooklyn.event.basic.BasicAttributeSensor;
-import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
-import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
-import brooklyn.location.basic.PortRanges;
-import brooklyn.util.flags.SetFromFlag;
-import brooklyn.util.javalang.JavaClassNames;
-import brooklyn.util.time.Duration;
-
-/**
- * An {@link brooklyn.entity.Entity} that represents a single Tomcat instance.
- */
-@Catalog(name="Tomcat Server",
-        description="Apache Tomcat is an open source software implementation of the Java Servlet and JavaServer Pages technologies",
-        iconUrl="classpath:///tomcat-logo.png")
-@ImplementedBy(TomcatServerImpl.class)
-public interface TomcatServer extends JavaWebAppSoftwareProcess, UsesJmx, HasShortName {
-
-    @SetFromFlag("version")
-    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "7.0.56");
-
-    @SetFromFlag("downloadUrl")
-    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new BasicAttributeSensorAndConfigKey<String>(
-            SoftwareProcess.DOWNLOAD_URL, "http://download.nextag.com/apache/tomcat/tomcat-7/v${version}/bin/apache-tomcat-${version}.tar.gz");
-
-    /**
-     * Tomcat insists on having a port you can connect to for the sole purpose of shutting it down.
-     * Don't see an easy way to disable it; causes collisions in its out-of-the-box location of 8005,
-     * so override default here to a high-numbered port.
-     */
-    @SetFromFlag("shutdownPort")
-    PortAttributeSensorAndConfigKey SHUTDOWN_PORT =
-            ConfigKeys.newPortSensorAndConfigKey("tomcat.shutdownport", "Suggested shutdown port", PortRanges.fromString("31880+"));
-
-    @SetFromFlag("server.xml")
-    ConfigKey<String> SERVER_XML_RESOURCE = ConfigKeys.newStringConfigKey(
-            "tomcat.serverxml", "The file to template and use as the Tomcat process' server.xml",
-            JavaClassNames.resolveClasspathUrl(TomcatServer.class, "server.xml"));
-
-    @SetFromFlag("web.xml")
-    ConfigKey<String> WEB_XML_RESOURCE = ConfigKeys.newStringConfigKey(
-            "tomcat.webxml", "The file to template and use as the Tomcat process' web.xml",
-            JavaClassNames.resolveClasspathUrl(TomcatServer.class, "web.xml"));
-
-    ConfigKey<Duration> START_TIMEOUT = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.START_TIMEOUT, Duration.FIVE_MINUTES);
-
-    AttributeSensor<String> CONNECTOR_STATUS =
-            new BasicAttributeSensor<String>(String.class, "webapp.tomcat.connectorStatus", "Catalina connector state name");
-
-    AttributeSensor<String> JMX_SERVICE_URL = UsesJmx.JMX_URL;
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java
deleted file mode 100644
index e5b5ac0..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatServerImpl.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import static java.lang.String.format;
-
-import java.util.concurrent.TimeUnit;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.java.JavaAppUtils;
-import brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl;
-import brooklyn.event.feed.jmx.JmxAttributePollConfig;
-import brooklyn.event.feed.jmx.JmxFeed;
-
-import com.google.common.base.Functions;
-import com.google.common.base.Predicates;
-
-/**
- * An {@link brooklyn.entity.Entity} that represents a single Tomcat instance.
- */
-public class TomcatServerImpl extends JavaWebAppSoftwareProcessImpl implements TomcatServer {
-
-    private static final Logger LOG = LoggerFactory.getLogger(TomcatServerImpl.class);
-
-    public TomcatServerImpl() {
-        super();
-    }
-
-    private volatile JmxFeed jmxWebFeed;
-    private volatile JmxFeed jmxAppFeed;
-
-    @Override
-    public void connectSensors() {
-        super.connectSensors();
-
-        if (getDriver().isJmxEnabled()) {
-            String requestProcessorMbeanName = "Catalina:type=GlobalRequestProcessor,name=\"http-*\"";
-
-            Integer port = isHttpsEnabled() ? getAttribute(HTTPS_PORT) : getAttribute(HTTP_PORT);
-            String connectorMbeanName = format("Catalina:type=Connector,port=%s", port);
-            boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS);
-
-            jmxWebFeed = JmxFeed.builder()
-                    .entity(this)
-                    .period(3000, TimeUnit.MILLISECONDS)
-                    .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_PROCESS_IS_RUNNING)
-                            // TODO Want to use something different from SERVICE_PROCESS_IS_RUNNING,
-                            // to indicate this is jmx MBean's reported state (or failure to connect)
-                            .objectName(connectorMbeanName)
-                            .attributeName("stateName")
-                            .onSuccess(Functions.forPredicate(Predicates.<Object>equalTo("STARTED")))
-                            .setOnFailureOrException(false)
-                            .suppressDuplicates(true))
-                    .pollAttribute(new JmxAttributePollConfig<String>(CONNECTOR_STATUS)
-                            .objectName(connectorMbeanName)
-                            .attributeName("stateName")
-                            .suppressDuplicates(true))
-                    .pollAttribute(new JmxAttributePollConfig<Integer>(ERROR_COUNT)
-                            .objectName(requestProcessorMbeanName)
-                            .attributeName("errorCount")
-                            .enabled(retrieveUsageMetrics))
-                    .pollAttribute(new JmxAttributePollConfig<Integer>(REQUEST_COUNT)
-                            .objectName(requestProcessorMbeanName)
-                            .attributeName("requestCount")
-                            .enabled(retrieveUsageMetrics))
-                    .pollAttribute(new JmxAttributePollConfig<Integer>(TOTAL_PROCESSING_TIME)
-                            .objectName(requestProcessorMbeanName)
-                            .attributeName("processingTime")
-                            .enabled(retrieveUsageMetrics))
-                    .build();
-
-            jmxAppFeed = JavaAppUtils.connectMXBeanSensors(this);
-        } else {
-            // if not using JMX
-            LOG.warn("Tomcat running without JMX monitoring; limited visibility of service available");
-            connectServiceUpIsRunning();
-        }
-    }
-
-    @Override
-    public void disconnectSensors() {
-        super.disconnectSensors();
-        if (getDriver() != null && getDriver().isJmxEnabled()) {
-           if (jmxWebFeed != null) jmxWebFeed.stop();
-           if (jmxAppFeed != null) jmxAppFeed.stop();
-        } else {
-            disconnectServiceUpIsRunning();
-        }
-    }
-
-    @SuppressWarnings("rawtypes")
-    @Override
-    public Class getDriverInterface() {
-        return TomcatDriver.class;
-    }
-    
-    @Override
-    public String getShortName() {
-        return "Tomcat";
-    }
-}
-

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatSshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatSshDriver.java b/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatSshDriver.java
deleted file mode 100644
index 76c820d..0000000
--- a/software/webapp/src/main/java/brooklyn/entity/webapp/tomcat/TomcatSshDriver.java
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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 brooklyn.entity.webapp.tomcat;
-
-import static java.lang.String.format;
-
-import java.io.InputStream;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-import brooklyn.entity.basic.Entities;
-import brooklyn.entity.webapp.JavaWebAppSshDriver;
-import brooklyn.location.basic.SshMachineLocation;
-import brooklyn.util.collections.MutableList;
-import brooklyn.util.collections.MutableMap;
-import brooklyn.util.net.Networking;
-import brooklyn.util.os.Os;
-import brooklyn.util.ssh.BashCommands;
-import brooklyn.util.text.StringEscapes.BashStringEscapes;
-
-import com.google.common.base.Preconditions;
-
-public class TomcatSshDriver extends JavaWebAppSshDriver implements TomcatDriver {
-
-    private static final String KEYSTORE_FILE = "keystore";
-
-    public TomcatSshDriver(TomcatServerImpl entity, SshMachineLocation machine) {
-        super(entity, machine);
-    }
-
-    @Override
-    public void preInstall() {
-        resolver = Entities.newDownloader(this);
-        setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName("apache-tomcat-"+getVersion())));
-    }
-
-    @Override
-    public void install() {
-        List<String> urls = resolver.getTargets();
-        String saveAs = resolver.getFilename();
-
-        List<String> commands = new LinkedList<String>();
-        commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, saveAs));
-        commands.add(BashCommands.INSTALL_TAR);
-        commands.add(format("tar xvzf %s", saveAs));
-
-        newScript(INSTALLING)
-                .environmentVariablesReset()
-                .body.append(commands)
-                .execute();
-    }
-
-    @Override
-    public void customize() {
-        newScript(CUSTOMIZING)
-                .body.append("mkdir -p conf logs webapps temp")
-                .failOnNonZeroResultCode()
-                .execute();
-
-        copyTemplate(entity.getConfig(TomcatServer.SERVER_XML_RESOURCE), Os.mergePaths(getRunDir(), "conf", "server.xml"));
-        copyTemplate(entity.getConfig(TomcatServer.WEB_XML_RESOURCE), Os.mergePaths(getRunDir(), "conf", "web.xml"));
-
-        // Deduplicate same code in JBoss
-        if (isProtocolEnabled("HTTPS")) {
-            String keystoreUrl = Preconditions.checkNotNull(getSslKeystoreUrl(), "keystore URL must be specified if using HTTPS for " + entity);
-            String destinationSslKeystoreFile = getHttpsSslKeystoreFile();
-            InputStream keystoreStream = resource.getResourceFromUrl(keystoreUrl);
-            getMachine().copyTo(keystoreStream, destinationSslKeystoreFile);
-        }
-
-        getEntity().deployInitialWars();
-    }
-
-    @Override
-    public void launch() {
-        Map<String, Integer> ports = MutableMap.of("httpPort", getHttpPort(), "shutdownPort", getShutdownPort());
-        Networking.checkPortsValid(ports);
-
-        // We wait for evidence of tomcat running because, using 
-        // brooklyn.ssh.config.tool.class=brooklyn.util.internal.ssh.cli.SshCliTool,
-        // we saw the ssh session return before the tomcat process was fully running 
-        // so the process failed to start.
-        newScript(MutableMap.of(USE_PID_FILE, false), LAUNCHING)
-                .body.append(
-                        format("%s/bin/startup.sh >>$RUN/console 2>&1 </dev/null",getExpandedInstallDir()),
-                        "for i in {1..10}\n" +
-                        "do\n" +
-                        "    if [ -s "+getLogFileLocation()+" ]; then exit; fi\n" +
-                        "    sleep 1\n" +
-                        "done\n" +
-                        "echo \"Couldn't determine if tomcat-server is running (logs/catalina.out is still empty); continuing but may subsequently fail\""
-                    )
-                .execute();
-    }
-
-    @Override
-    public boolean isRunning() {
-        return newScript(MutableMap.of(USE_PID_FILE, "pid.txt"), CHECK_RUNNING).execute() == 0;
-    }
-
-    @Override
-    public void stop() {
-        newScript(MutableMap.of(USE_PID_FILE, "pid.txt"), STOPPING).execute();
-    }
-
-    @Override
-    public void kill() {
-        newScript(MutableMap.of(USE_PID_FILE, "pid.txt"), KILLING).execute();
-    }
-
-    @Override
-    protected List<String> getCustomJavaConfigOptions() {
-        return MutableList.<String>builder()
-                .addAll(super.getCustomJavaConfigOptions())
-                .add("-Xms200m")
-                .add("-Xmx800m")
-                .add("-XX:MaxPermSize=400m")
-                .build();
-    }
-
-    @Override
-    public Map<String, String> getShellEnvironment() {
-        Map<String, String> shellEnv =  MutableMap.<String, String>builder()
-                .putAll(super.getShellEnvironment())
-                .remove("JAVA_OPTS")
-                .put("CATALINA_PID", "pid.txt")
-                .put("CATALINA_BASE", getRunDir())
-                .put("RUN", getRunDir())
-                .build();
-
-        // Double quoting of individual JAVA_OPTS entries required due to eval in catalina.sh
-        List<String> javaOpts = getJavaOpts();
-        String sJavaOpts = BashStringEscapes.doubleQuoteLiteralsForBash(javaOpts.toArray(new String[0]));
-        shellEnv.put("CATALINA_OPTS", sJavaOpts);
-
-        return shellEnv;
-    }
-
-    @Override
-    protected String getLogFileLocation() {
-        return Os.mergePathsUnix(getRunDir(), "logs/catalina.out");
-    }
-
-    @Override
-    protected String getDeploySubdir() {
-       return "webapps";
-    }
-
-    public Integer getShutdownPort() {
-        return entity.getAttribute(TomcatServerImpl.SHUTDOWN_PORT);
-    }
-
-    public String getHttpsSslKeystoreFile() {
-        return Os.mergePathsUnix(getRunDir(), "conf", KEYSTORE_FILE);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsService.java
new file mode 100644
index 0000000..670a6aa
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsService.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.entity.dns;
+
+import java.util.Map;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.trait.Startable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.location.geo.HostGeoInfo;
+
+import com.google.common.reflect.TypeToken;
+
+public interface AbstractGeoDnsService extends Entity {
+    
+    public static final ConfigKey<Boolean> INCLUDE_HOMELESS_ENTITIES = ConfigKeys.newBooleanConfigKey("geodns.includeHomeless", "Whether to include entities whose geo-coordinates cannot be inferred", false);
+    public static final ConfigKey<Boolean> USE_HOSTNAMES = ConfigKeys.newBooleanConfigKey("geodns.useHostnames", "Whether to use the hostname for the returned value for routing, rather than IP address (defaults to true)", true);
+    
+    public static final AttributeSensor<Lifecycle> SERVICE_STATE_ACTUAL = Attributes.SERVICE_STATE_ACTUAL;
+    public static final AttributeSensor<Boolean> SERVICE_UP = Startable.SERVICE_UP;
+    public static final AttributeSensor<String> HOSTNAME = Attributes.HOSTNAME;
+    public static final AttributeSensor<String> ADDRESS = Attributes.ADDRESS;
+    @SuppressWarnings("serial")
+    public static final AttributeSensor<Map<String,String>> TARGETS = new BasicAttributeSensor<Map<String,String>>(
+            new TypeToken<Map<String,String>>() {}, "geodns.targets", "Map of targets currently being managed (entity ID to URL)");
+
+    public void setServiceState(Lifecycle state);
+    
+    /** sets target to be a group whose *members* will be searched (non-Group items not supported) */
+    // prior to 0.7.0 the API accepted non-group items, but did not handle them correctly
+    public void setTargetEntityProvider(final Group entityProvider);
+    
+    /** should return the hostname which this DNS service is configuring */
+    public String getHostname();
+    
+    public Map<Entity, HostGeoInfo> getTargetHosts();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java
new file mode 100644
index 0000000..a6b2b5a
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/AbstractGeoDnsServiceImpl.java
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.entity.dns;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.webapp.WebAppService;
+import org.apache.brooklyn.policy.PolicySpec;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.DynamicGroup;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.basic.ServiceStateLogic.ServiceNotUpLogic;
+import brooklyn.entity.group.AbstractMembershipTrackingPolicy;
+import brooklyn.location.geo.HostGeoInfo;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.net.Networking;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+public abstract class AbstractGeoDnsServiceImpl extends AbstractEntity implements AbstractGeoDnsService {
+    private static final Logger log = LoggerFactory.getLogger(AbstractGeoDnsService.class);
+
+    @SetFromFlag
+    protected Group targetEntityProvider;
+    protected AbstractMembershipTrackingPolicy tracker;
+    
+    protected Map<Entity, HostGeoInfo> targetHosts = Collections.synchronizedMap(new LinkedHashMap<Entity, HostGeoInfo>());
+    
+    // We complain (at debug) when we encounter a target entity for whom we can't derive hostname/ip information; 
+    // this is the commonest case for the transient condition between the time the entity is created and the time 
+    // it is started (at which point the location is specified). This set contains those entities we've complained 
+    // about already, to avoid repetitive logging.
+    transient protected Set<Entity> entitiesWithoutHostname = new HashSet<Entity>();
+
+    // We complain (at info/warn) when we encounter a target entity for whom we can't derive geo information, even 
+    // when hostname/ip is known. This set contains those entities we've complained about already, to avoid repetitive 
+    // logging.
+    transient protected Set<Entity> entitiesWithoutGeoInfo = new HashSet<Entity>();
+
+    public AbstractGeoDnsServiceImpl() {
+        super();
+    }
+    
+    @Override
+    public Map<Entity, HostGeoInfo> getTargetHosts() {
+        return targetHosts;
+    }
+    
+    @Override
+    public void onManagementBecomingMaster() {
+        super.onManagementBecomingMaster();
+        startTracker();
+    }
+    @Override
+    public void onManagementNoLongerMaster() {
+        endTracker();
+        super.onManagementNoLongerMaster();
+    }
+
+    @Override
+    public void destroy() {
+        setServiceState(Lifecycle.DESTROYED);
+        super.destroy();
+    }
+        
+    @Override
+    public void setServiceState(Lifecycle state) {
+        setAttribute(HOSTNAME, getHostname());
+        ServiceStateLogic.setExpectedState(this, state);
+        if (state==Lifecycle.RUNNING)
+            ServiceNotUpLogic.clearNotUpIndicator(this, SERVICE_STATE_ACTUAL);
+        else
+            ServiceNotUpLogic.updateNotUpIndicator(this, SERVICE_STATE_ACTUAL, "Not in RUNNING state");
+    }
+    
+    @Override
+    public void setTargetEntityProvider(final Group entityProvider) {
+        this.targetEntityProvider = checkNotNull(entityProvider, "targetEntityProvider");
+        startTracker();
+    }
+    
+    /** should set up so these hosts are targeted, and setServiceState appropriately */
+    protected abstract void reconfigureService(Collection<HostGeoInfo> targetHosts);
+    
+    protected synchronized void startTracker() {
+        if (targetEntityProvider==null || !getManagementSupport().isDeployed()) {
+            log.debug("Tracker for "+this+" not yet active: "+targetEntityProvider+" / "+getManagementContext());
+            return;
+        }
+        endTracker();
+        log.debug("Initializing tracker for "+this+", following "+targetEntityProvider);
+        tracker = addPolicy(PolicySpec.create(MemberTrackingPolicy.class)
+                .displayName("GeoDNS targets tracker")
+                .configure("sensorsToTrack", ImmutableSet.of(HOSTNAME, ADDRESS, Attributes.MAIN_URI, WebAppService.ROOT_URL))
+                .configure("group", targetEntityProvider));
+        refreshGroupMembership();
+    }
+    
+    protected synchronized void endTracker() {
+        if (tracker == null || targetEntityProvider==null) return;
+        removePolicy(tracker);
+        tracker = null;
+    }
+    
+    public static class MemberTrackingPolicy extends AbstractMembershipTrackingPolicy {
+        @Override
+        protected void onEntityEvent(EventType type, Entity entity) {
+            ((AbstractGeoDnsServiceImpl)super.entity).refreshGroupMembership();
+        }
+    }
+
+    @Override
+    public abstract String getHostname();
+    
+    long lastUpdate = -1;
+    
+    // TODO: remove group member polling once locations can be determined via subscriptions
+    protected void refreshGroupMembership() {
+        try {
+            if (log.isDebugEnabled()) log.debug("GeoDns {} refreshing targets", this);
+            if (targetEntityProvider == null)
+                return;
+            if (targetEntityProvider instanceof DynamicGroup)
+                ((DynamicGroup) targetEntityProvider).rescanEntities();
+            Set<Entity> pool = MutableSet.copyOf(targetEntityProvider instanceof Group ? ((Group)targetEntityProvider).getMembers(): targetEntityProvider.getChildren());
+            if (log.isDebugEnabled()) log.debug("GeoDns {} refreshing targets, pool now {}", this, pool);
+            
+            boolean changed = false;
+            Set<Entity> previousOnes = MutableSet.copyOf(targetHosts.keySet());
+            for (Entity e: pool) {
+                previousOnes.remove(e);
+                changed |= addTargetHost(e);
+            }
+            // anything left in previousOnes is no longer applicable
+            for (Entity e: previousOnes) {
+                changed = true;
+                removeTargetHost(e, false);
+            }
+            
+            // do a periodic full update hourly once we are active (the latter is probably not needed)
+            if (changed || (lastUpdate>0 && Time.hasElapsedSince(lastUpdate, Duration.ONE_HOUR)))
+                update();
+            
+        } catch (Exception e) {
+            log.error("Problem refreshing group membership: "+e, e);
+        }
+    }
+    
+    /**
+     * Adds this host, if it is absent or if its hostname has changed.
+     * <p>
+     * For whether to use hostname or ip, see config and attributes {@link AbstractGeoDnsService#USE_HOSTNAMES}, 
+     * {@link Attributes#HOSTNAME} and {@link Attributes#ADDRESS} (via {@link #inferHostname(Entity)} and {@link #inferIp(Entity)}.
+     * Note that the "hostname" could in fact be an IP address, if {@link #inferHostname(Entity)} returns an IP!
+     * <p>
+     * TODO in a future release, we may change this to explicitly set the sensor(s) to look at on the entity, and 
+     * be stricter about using them in order.
+     * 
+     * @return true if host is added or changed
+     */
+    protected boolean addTargetHost(Entity entity) {
+        try {
+            HostGeoInfo oldGeo = targetHosts.get(entity);
+            String hostname = inferHostname(entity);
+            String ip = inferIp(entity);
+            String addr = (getConfig(USE_HOSTNAMES) || ip == null) ? hostname : ip;
+            
+            if (addr==null) addr = ip;
+            if (addr == null) {
+                if (entitiesWithoutHostname.add(entity)) {
+                    log.debug("GeoDns ignoring {} (no hostname/ip/URL info yet available)", entity);
+                }
+                return false;
+            }
+            
+            // prefer the geo from the entity (or location parent), but fall back to inferring
+            // e.g. if it supplies a URL
+            HostGeoInfo geo = HostGeoInfo.fromEntity(entity);
+            if (geo==null) geo = inferHostGeoInfo(hostname, ip);
+            
+            if (Networking.isPrivateSubnet(addr) && ip!=null && !Networking.isPrivateSubnet(ip)) {
+                // fix for #1216
+                log.debug("GeoDns using IP "+ip+" for "+entity+" as addr "+addr+" resolves to private subnet");
+                addr = ip;
+            }
+            if (Networking.isPrivateSubnet(addr)) {
+                if (getConfig(INCLUDE_HOMELESS_ENTITIES)) {
+                    if (entitiesWithoutGeoInfo.add(entity)) {
+                        log.info("GeoDns including {}, even though {} is a private subnet (homeless entities included)", entity, addr);
+                    }
+                } else {
+                    if (entitiesWithoutGeoInfo.add(entity)) {
+                        log.warn("GeoDns ignoring {} (private subnet detected for {})", entity, addr);
+                    }
+                    return false;
+                }
+            }
+
+            if (geo == null) {
+                if (getConfig(INCLUDE_HOMELESS_ENTITIES)) {
+                    if (entitiesWithoutGeoInfo.add(entity)) {
+                        log.info("GeoDns including {}, even though no geography info available for {})", entity, addr);
+                    }
+                    geo = HostGeoInfo.create(addr, "unknownLocation("+addr+")", 0, 0);
+                } else {
+                    if (entitiesWithoutGeoInfo.add(entity)) {
+                        log.warn("GeoDns ignoring {} (no geography info available for {})", entity, addr);
+                    }
+                    return false;
+                }
+            }
+
+            if (!addr.equals(geo.getAddress())) {
+                // if the location provider did not have an address, create a new one with it
+                geo = HostGeoInfo.create(addr, geo.displayName, geo.latitude, geo.longitude);
+            }
+            
+            // If we already knew about it, and it hasn't changed, then nothing to do
+            if (oldGeo != null && geo.getAddress().equals(oldGeo.getAddress())) {
+                return false;
+            }
+            
+            entitiesWithoutHostname.remove(entity);
+            entitiesWithoutGeoInfo.remove(entity);
+            log.info("GeoDns adding "+entity+" at "+geo+(oldGeo != null ? " (previously "+oldGeo+")" : ""));
+            targetHosts.put(entity, geo);
+            return true;
+
+        } catch (Exception ee) {
+            log.warn("GeoDns ignoring "+entity+" (error analysing location): "+ee, ee);
+            return false;
+        }
+    }
+
+    /** remove if host removed */
+    protected boolean removeTargetHost(Entity e, boolean doUpdate) {
+        if (targetHosts.remove(e) != null) {
+            log.info("GeoDns removing reference to {}", e);
+            if (doUpdate) update();
+            return true;
+        }
+        return false;
+    }
+    
+    protected void update() {
+        lastUpdate = System.currentTimeMillis();
+        
+        Map<Entity, HostGeoInfo> m;
+        synchronized(targetHosts) { m = ImmutableMap.copyOf(targetHosts); }
+        if (log.isDebugEnabled()) log.debug("Full update of "+this+" ("+m.size()+" target hosts)");
+        
+        Map<String,String> entityIdToAddress = Maps.newLinkedHashMap();
+        for (Map.Entry<Entity, HostGeoInfo> entry : m.entrySet()) {
+            entityIdToAddress.put(entry.getKey().getId(), entry.getValue().address);
+        }
+        
+        reconfigureService(new LinkedHashSet<HostGeoInfo>(m.values()));
+        
+        if (log.isDebugEnabled()) log.debug("Targets being set as "+entityIdToAddress);
+        setAttribute(TARGETS, entityIdToAddress);
+    }
+    
+    protected String inferHostname(Entity entity) {
+        String hostname = entity.getAttribute(Attributes.HOSTNAME);
+        URI url = entity.getAttribute(Attributes.MAIN_URI);
+        if (url!=null) {
+            try {
+                URL u = url.toURL();
+                
+                String hostname2 = u.getHost(); 
+                if (hostname==null) {
+                    if (!entitiesWithoutGeoInfo.contains(entity))  //don't log repeatedly
+                        log.warn("GeoDns "+this+" using URL {} to redirect to {} (HOSTNAME attribute is preferred, but not available)", url, entity);
+                    hostname = hostname2;
+                } else if (!hostname.equals(hostname2)) {
+                    if (!entitiesWithoutGeoInfo.contains(entity))  //don't log repeatedly
+                        log.warn("GeoDns "+this+" URL {} of "+entity+" does not match advertised HOSTNAME {}; using hostname, not URL", url, hostname);
+                }
+                
+                if (u.getPort() > 0 && u.getPort() != 80 && u.getPort() != 443) {
+                    if (!entitiesWithoutGeoInfo.contains(entity))  //don't log repeatedly
+                        log.warn("GeoDns "+this+" detected non-standard port in URL {} for {}; forwarding may not work", url, entity);
+                }
+                
+            } catch (MalformedURLException e) {
+                log.warn("Invalid URL {} for entity {} in {}", new Object[] {url, entity, this});
+            }
+        }
+        return hostname;
+    }
+    
+    protected String inferIp(Entity entity) {
+        return entity.getAttribute(Attributes.ADDRESS);
+    }
+    
+    protected HostGeoInfo inferHostGeoInfo(String hostname, String ip) throws UnknownHostException {
+        HostGeoInfo geoH = null;
+        if (hostname != null) {
+            try {
+                // For some entities, the hostname can actually be an IP! Therefore use Networking.getInetAddressWithFixedName
+                InetAddress addr = Networking.getInetAddressWithFixedName(hostname);
+                geoH = HostGeoInfo.fromIpAddress(addr);
+            } catch (RuntimeException e) {
+                // Most likely caused by (a wrapped) UnknownHostException
+                Exceptions.propagateIfFatal(e);
+                if (ip == null) {
+                    if (log.isTraceEnabled()) log.trace("inferHostGeoInfo failing ("+Exceptions.getFirstInteresting(e)+"): hostname="+hostname+"; ip="+ip);
+                    throw e;
+                } else {
+                    if (log.isTraceEnabled()) log.trace("GeoDns failed to infer GeoInfo from hostname {}; will try with IP {} ({})", new Object[] {hostname, ip, e});
+                }
+            }
+        }
+
+        // Try IP address (prior to Mar 2014 we did not do this if USE_HOSTNAME was set but don't think that is desirable due to #1216)
+        if (ip != null) {
+            if (geoH == null) {
+                InetAddress addr = Networking.getInetAddressWithFixedName(ip);
+                geoH = HostGeoInfo.fromIpAddress(addr);
+                if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from ip {} (could not infer from hostname {})", new Object[] {geoH, ip, hostname});
+            } else {
+                geoH = HostGeoInfo.create(ip, geoH.displayName, geoH.latitude, geoH.longitude);
+                if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from hostname {}; switching it to ip {}", new Object[] {geoH, hostname, ip});
+            }
+        } else {
+            if (log.isTraceEnabled()) log.trace("GeoDns inferred GeoInfo {} from hostname {}", geoH, hostname);
+        }
+        
+        return geoH;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java
new file mode 100644
index 0000000..31c8831
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsService.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.entity.dns.geoscaling;
+
+import java.net.URI;
+
+import org.apache.brooklyn.entity.dns.AbstractGeoDnsService;
+import org.apache.brooklyn.entity.webapp.WebAppServiceConstants;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+@ImplementedBy(GeoscalingDnsServiceImpl.class)
+public interface GeoscalingDnsService extends AbstractGeoDnsService {
+    
+    @SetFromFlag("sslTrustAll")
+    public static final ConfigKey<Boolean> SSL_TRUST_ALL = ConfigKeys.newBooleanConfigKey(
+            "ssl.trustAll",
+            "Whether to trust all certificates, or to fail with 'peer not authenticated' if untrusted (default false)",
+            false);
+    @SetFromFlag("randomizeSubdomainName")
+    public static final ConfigKey<Boolean> RANDOMIZE_SUBDOMAIN_NAME = new BasicConfigKey<Boolean>(
+            Boolean.class, "randomize.subdomain.name");
+    @SetFromFlag("username")
+    public static final ConfigKey<String> GEOSCALING_USERNAME = new BasicConfigKey<String>(
+            String.class, "geoscaling.username");
+    @SetFromFlag("password")
+    public static final ConfigKey<String> GEOSCALING_PASSWORD = new BasicConfigKey<String>(
+            String.class, "geoscaling.password");
+    @SetFromFlag("primaryDomainName")
+    public static final ConfigKey<String> GEOSCALING_PRIMARY_DOMAIN_NAME = new BasicConfigKey<String>(
+            String.class, "geoscaling.primary.domain.name");
+    @SetFromFlag("smartSubdomainName")
+    public static final ConfigKey<String> GEOSCALING_SMART_SUBDOMAIN_NAME = new BasicConfigKey<String>(
+            String.class, "geoscaling.smart.subdomain.name");
+    
+    public static final AttributeSensor<String> GEOSCALING_ACCOUNT = new BasicAttributeSensor<String>(
+            String.class, "geoscaling.account", "Active user account for the GeoScaling.com service");
+    public static final AttributeSensor<URI> MAIN_URI = Attributes.MAIN_URI;
+    public static final AttributeSensor<String> ROOT_URL = WebAppServiceConstants.ROOT_URL;
+    public static final AttributeSensor<String> MANAGED_DOMAIN = new BasicAttributeSensor<String>(
+            String.class, "geoscaling.managed.domain", "Fully qualified domain name that will be geo-redirected; " +
+                    "this will be the same as "+ROOT_URL.getName()+" but the latter will only be set when the domain has active targets");
+    
+    public void applyConfig();
+    
+    /** minimum/default TTL here is 300s = 5m */
+    public long getTimeToLiveSeconds();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java
new file mode 100644
index 0000000..5779ec2
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingDnsServiceImpl.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.entity.dns.geoscaling;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.PROVIDE_CITY_INFO;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Set;
+
+import org.apache.brooklyn.entity.dns.AbstractGeoDnsServiceImpl;
+import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.Domain;
+import org.apache.brooklyn.entity.dns.geoscaling.GeoscalingWebClient.SmartSubdomain;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.location.geo.HostGeoInfo;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.http.HttpTool;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.Strings;
+
+public class GeoscalingDnsServiceImpl extends AbstractGeoDnsServiceImpl implements GeoscalingDnsService {
+
+    private static final Logger log = LoggerFactory.getLogger(GeoscalingDnsServiceImpl.class);
+
+    // Must remember any desired redirection targets if they're specified before configure() has been called.
+    private Set<HostGeoInfo> rememberedTargetHosts;
+    private GeoscalingWebClient webClient;
+    
+    // These are available only after the configure() method has been invoked.
+    private boolean randomizeSmartSubdomainName;
+    private String username;
+    private String password;
+    private String primaryDomainName;
+    private String smartSubdomainName;
+
+    public GeoscalingDnsServiceImpl() {
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        
+        // defaulting to randomized subdomains makes deploying multiple applications easier
+        if (getConfig(RANDOMIZE_SUBDOMAIN_NAME)==null) setConfig(RANDOMIZE_SUBDOMAIN_NAME, true); 
+        
+        Boolean trustAll = getConfig(SSL_TRUST_ALL);
+        if (Boolean.TRUE.equals(trustAll)) {
+            webClient = new GeoscalingWebClient(HttpTool.httpClientBuilder().trustAll().build());
+        } else {
+            webClient = new GeoscalingWebClient();
+        }
+    }
+    
+    // Ensure our configure() method gets called; may be able to remove this if/when the framework detects this
+    // and invokes the configure() method automatically?
+    @Override
+    public void onManagementBecomingMaster() {
+        try {
+            applyConfig();
+        } catch (Exception e) {
+            // don't prevent management coming up, but do mark it as on fire
+            log.error("Geoscaling did not come up correctly: "+e, e);
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
+        }
+        super.onManagementBecomingMaster();
+    }
+
+    boolean isConfigured = false;
+    
+    public synchronized void applyConfig() {        
+        randomizeSmartSubdomainName = getConfig(RANDOMIZE_SUBDOMAIN_NAME);
+        username = getConfig(GEOSCALING_USERNAME);
+        password = getConfig(GEOSCALING_PASSWORD);
+        primaryDomainName = getConfig(GEOSCALING_PRIMARY_DOMAIN_NAME);
+        smartSubdomainName = getConfig(GEOSCALING_SMART_SUBDOMAIN_NAME);
+
+        // Ensure all mandatory configuration is provided.
+        checkNotNull(username, "The GeoScaling username is not specified");
+        checkNotNull(password, "The GeoScaling password is not specified");
+        checkNotNull(primaryDomainName, "The GeoScaling primary domain name is not specified");
+        
+        if (randomizeSmartSubdomainName) {
+            // if no smart subdomain specified, but random is, use something random
+            if (smartSubdomainName != null) smartSubdomainName += "-";
+            else smartSubdomainName = "";
+            smartSubdomainName += Identifiers.makeRandomId(8);
+        }
+        checkNotNull(smartSubdomainName, "The GeoScaling smart subdomain name is not specified or randomized");
+        
+        String fullDomain = smartSubdomainName+"."+primaryDomainName;
+        log.info("GeoScaling service will configure redirection for '"+fullDomain+"' domain");
+        setAttribute(GEOSCALING_ACCOUNT, username);
+        setAttribute(MANAGED_DOMAIN, fullDomain);
+        setAttribute(HOSTNAME, getHostname());
+        
+        isConfigured = true;
+        
+        if (rememberedTargetHosts != null) {
+            reconfigureService(rememberedTargetHosts);
+            rememberedTargetHosts = null;
+        }
+    }
+    
+    @Override
+    public String getHostname() {
+        String result = getAttribute(MANAGED_DOMAIN);
+        return (Strings.isBlank(result)) ? null : result;
+    }
+    
+    /** minimum/default TTL here is 300s = 5m */
+    public long getTimeToLiveSeconds() { return 5*60; }
+    
+    @Override
+    public void destroy() {
+        setServiceState(Lifecycle.STOPPING);
+        if (!isConfigured) return;
+        
+        // Don't leave randomized subdomains configured on our GeoScaling account.
+        if (randomizeSmartSubdomainName) {
+            webClient.login(username, password);
+            Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName);
+            SmartSubdomain smartSubdomain = (primaryDomain != null) ? primaryDomain.getSmartSubdomain(smartSubdomainName) : null;
+            if (smartSubdomain != null) {
+                log.info("Deleting randomized GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+"'");
+                smartSubdomain.delete();
+            }
+            webClient.logout();
+        }
+        
+        super.destroy();
+        
+        isConfigured = false;
+    }
+    
+    protected void reconfigureService(Collection<HostGeoInfo> targetHosts) {
+        if (!isConfigured) {
+            this.rememberedTargetHosts = MutableSet.copyOf(targetHosts);
+            return;
+        }
+        
+        webClient.login(username, password);
+        Domain primaryDomain = webClient.getPrimaryDomain(primaryDomainName);
+        if (primaryDomain==null) 
+            throw new NullPointerException(this+" got null from web client for primary domain "+primaryDomainName);
+        SmartSubdomain smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName);
+        
+        if (smartSubdomain == null) {
+            log.info("GeoScaling {} smart subdomain '{}.{}' does not exist, creating it now", new Object[] {this, smartSubdomainName, primaryDomainName});
+            // TODO use WithMutexes to ensure this is single-entrant
+            primaryDomain.createSmartSubdomain(smartSubdomainName);
+            smartSubdomain = primaryDomain.getSmartSubdomain(smartSubdomainName);
+        }
+        
+        if (smartSubdomain != null) {
+            log.debug("GeoScaling {} being reconfigured to use {}", this, targetHosts);
+            String script = GeoscalingScriptGenerator.generateScriptString(targetHosts);
+            smartSubdomain.configure(PROVIDE_CITY_INFO, script);
+            if (targetHosts.isEmpty()) {
+                setServiceState(Lifecycle.CREATED);
+                setAttribute(ROOT_URL, null);
+                setAttribute(MAIN_URI, null);
+            } else {
+                setServiceState(Lifecycle.RUNNING);
+                String domain = getAttribute(MANAGED_DOMAIN);
+                if (!Strings.isEmpty(domain)) {
+                    setAttribute(ROOT_URL, "http://"+domain+"/");
+                    setAttribute(MAIN_URI, URI.create("http://"+domain+"/"));
+                }
+            }
+        } else {
+            log.warn("Failed to retrieve or create GeoScaling smart subdomain '"+smartSubdomainName+"."+primaryDomainName+
+                    "', aborting attempt to configure service");
+            setServiceState(Lifecycle.ON_FIRE);
+        }
+        
+        webClient.logout();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingScriptGenerator.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingScriptGenerator.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingScriptGenerator.java
new file mode 100644
index 0000000..ae3883f
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/dns/geoscaling/GeoscalingScriptGenerator.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.brooklyn.entity.dns.geoscaling;
+
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.TimeZone;
+
+import brooklyn.location.geo.HostGeoInfo;
+import brooklyn.util.ResourceUtils;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.os.Os;
+import brooklyn.util.text.Strings;
+
+public class GeoscalingScriptGenerator {
+    
+    private static final String PHP_SCRIPT_TEMPLATE_RESOURCE = JavaClassNames.resolveClasspathUrl(GeoscalingScriptGenerator.class, "template.php");
+    private static final String HOSTS_DECLARATIONS_MARKER = "/* HOST DECLARATIONS TO BE SUBSTITUTED HERE */";
+    private static final String DATESTAMP_MARKER = "DATESTAMP";
+
+    
+    public static String generateScriptString(Collection<HostGeoInfo> hosts) {
+        return generateScriptString(new Date(), hosts);
+    }
+    
+    public static String generateScriptString(Date generationTime, Collection<HostGeoInfo> hosts) {
+        String template = ResourceUtils.create(GeoscalingScriptGenerator.class).getResourceAsString(PHP_SCRIPT_TEMPLATE_RESOURCE);
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss 'UTC'");
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+        String datestamp = sdf.format(generationTime);
+        String declarations = getHostsDeclaration(hosts);
+        return template
+            .replaceAll(DATESTAMP_MARKER, datestamp)
+            .replace(HOSTS_DECLARATIONS_MARKER, declarations);
+    }
+    
+    private static String getHostsDeclaration(Collection<HostGeoInfo> hosts) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("$hosts = array(").append(Os.LINE_SEPARATOR);
+        Iterator<HostGeoInfo> iServer = hosts.iterator();
+        while (iServer.hasNext()) {
+            HostGeoInfo server = iServer.next();
+            sb.append("    array('name'      => '").append(escape(server.displayName)).append("',").append(Os.LINE_SEPARATOR);
+            sb.append("          'latitude'  => ").append(server.latitude).append(",").append(Os.LINE_SEPARATOR);
+            sb.append("          'longitude' => ").append(server.longitude).append(",").append(Os.LINE_SEPARATOR);
+            sb.append("          'ip'        => '").append(escape(server.address)).append("')");
+            if (iServer.hasNext()) sb.append(",").append(Os.LINE_SEPARATOR);
+            sb.append(Os.LINE_SEPARATOR);
+        }
+        sb.append(");").append(Os.LINE_SEPARATOR);
+        return sb.toString();
+    }
+    
+    private static String escape(String txt) {
+        txt = Strings.replaceAllNonRegex(txt, "\\", "\\\\");
+        txt = Strings.replaceAllNonRegex(txt, "'", "\\'");
+        txt = Strings.replaceAllNonRegex(txt, "\"", "\\\"'");
+        return txt;
+    }
+    
+}