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:36 UTC

[18/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/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
new file mode 100644
index 0000000..7152c78
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMetrics.java
@@ -0,0 +1,77 @@
+/*
+ * 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.webapp;
+
+import brooklyn.config.render.RendererHints;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.math.MathFunctions;
+import brooklyn.util.text.ByteSizeStrings;
+import brooklyn.util.time.Duration;
+
+public interface WebAppServiceMetrics {
+    
+    public static final brooklyn.event.basic.BasicAttributeSensor<Integer> ERROR_COUNT =
+            new brooklyn.event.basic.BasicAttributeSensor<Integer>(Integer.class, "webapp.reqs.errors", "Request errors");
+    public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME = Sensors.newIntegerSensor(
+            "webapp.reqs.processingTime.total", "Total processing time, reported by webserver (millis)");
+    public static final AttributeSensor<Integer> MAX_PROCESSING_TIME =
+            Sensors.newIntegerSensor("webapp.reqs.processingTime.max", "Max processing time for any single request, reported by webserver (millis)");
+
+    /** the fraction of time represented by the most recent delta to TOTAL_PROCESSING_TIME, ie 0.4 if 800 millis were accumulated in last 2s;
+     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(brooklyn.entity.basic.EntityLocal, brooklyn.util.time.Duration)} */
+    public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_LAST =
+            Sensors.newDoubleSensor("webapp.reqs.processingTime.fraction.last", "Fraction of time spent processing, reported by webserver (percentage, last datapoint)");
+    public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_IN_WINDOW =
+            Sensors.newDoubleSensor("webapp.reqs.processingTime.fraction.windowed", "Fraction of time spent processing, reported by webserver (percentage, over time window)");
+
+    public static final AttributeSensor<Long> BYTES_RECEIVED =
+            new BasicAttributeSensor<Long>(Long.class, "webapp.reqs.bytes.received", "Total bytes received by the webserver");
+    public static final AttributeSensor<Long> BYTES_SENT =
+            new BasicAttributeSensor<Long>(Long.class, "webapp.reqs.bytes.sent", "Total bytes sent by the webserver");
+
+    /** req/second computed from the delta of the last request count and an associated timestamp */
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_LAST =
+            Sensors.newDoubleSensor("webapp.reqs.perSec.last", "Reqs/sec (last datapoint)");
+
+    /** rolled-up req/second for a window, 
+     * easily configured with {@link WebAppServiceMethods#connectWebAppServerPolicies(brooklyn.entity.basic.EntityLocal, brooklyn.util.time.Duration)} */
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW =
+            Sensors.newDoubleSensor("webapp.reqs.perSec.windowed", "Reqs/sec (over time window)");
+
+    public static final AttributeSensor<Integer> REQUEST_COUNT = Initializer.REQUEST_COUNT;
+
+    // this class is added because the above need static initialization which unfortunately can't be added to an interface.
+    // (but should only be referenced after the other fields have been set)
+    static class Initializer {
+        public static final AttributeSensor<Integer> REQUEST_COUNT =
+            Sensors.newIntegerSensor("webapp.reqs.total", "Request count");
+
+        static {
+            RendererHints.register(WebAppServiceConstants.TOTAL_PROCESSING_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
+            RendererHints.register(WebAppServiceConstants.MAX_PROCESSING_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
+            RendererHints.register(WebAppServiceConstants.BYTES_RECEIVED, RendererHints.displayValue(ByteSizeStrings.metric()));
+            RendererHints.register(WebAppServiceConstants.BYTES_SENT, RendererHints.displayValue(ByteSizeStrings.metric()));
+            RendererHints.register(WebAppServiceConstants.PROCESSING_TIME_FRACTION_LAST, RendererHints.displayValue(MathFunctions.percent(2)));
+            RendererHints.register(WebAppServiceConstants.PROCESSING_TIME_FRACTION_IN_WINDOW, RendererHints.displayValue(MathFunctions.percent(2)));
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Driver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Driver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Driver.java
new file mode 100644
index 0000000..d8ba9d7
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Driver.java
@@ -0,0 +1,24 @@
+/*
+ * 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.webapp.jboss;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppDriver;
+
+public interface JBoss6Driver extends JavaWebAppDriver {
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Server.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Server.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Server.java
new file mode 100644
index 0000000..5ce6ded
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6Server.java
@@ -0,0 +1,63 @@
+/*
+ * 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.webapp.jboss;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcess;
+
+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.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+@Catalog(name="JBoss Application Server 6", description="AS6: an open source Java application server from JBoss", iconUrl="classpath:///jboss-logo.png")
+@ImplementedBy(JBoss6ServerImpl.class)
+public interface JBoss6Server extends JavaWebAppSoftwareProcess, UsesJmx {
+
+    // TODO Instead of using portIncrement, would prefer to use http_port as "8080+" etc.
+    // On localhost, if an existing jboss6 is running and consuming the required port(s), 
+    // then we don't spot that and don't claim a different port.
+    // Things then fail silently!
+    
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION =
+            ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "6.0.0.Final");
+
+    @SetFromFlag("downloadUrl")
+    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new BasicAttributeSensorAndConfigKey<String>(
+            SoftwareProcess.DOWNLOAD_URL, "http://downloads.sourceforge.net/project/jboss/JBoss/JBoss-${version}/jboss-as-distribution-${version}.zip?" +
+            "r=http%3A%2F%2Fsourceforge.net%2Fprojects%2Fjboss%2Ffiles%2FJBoss%2F${version}%2F&ts=1307104229&use_mirror=kent");
+
+    @SetFromFlag("bindAddress")
+    BasicAttributeSensorAndConfigKey<String> BIND_ADDRESS =
+            new BasicAttributeSensorAndConfigKey<String>(String.class, "jboss6.bind.address", 
+                "Address of interface JBoss should listen on, defaulting 0.0.0.0 (but could set e.g. to attributeWhenReady(HOSTNAME)", 
+                "0.0.0.0");
+
+    @SetFromFlag("portIncrement")
+    BasicAttributeSensorAndConfigKey<Integer> PORT_INCREMENT =
+            new BasicAttributeSensorAndConfigKey<Integer>(Integer.class, "jboss6.portincrement", "Increment to be used for all jboss ports", 0);
+
+    @SetFromFlag("clusterName")
+    BasicAttributeSensorAndConfigKey<String> CLUSTER_NAME =
+            new BasicAttributeSensorAndConfigKey<String>(String.class, "jboss6.clusterName", "Identifier used to group JBoss instances", "");
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java
new file mode 100644
index 0000000..59d35dc
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6ServerImpl.java
@@ -0,0 +1,103 @@
+/*
+ * 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.webapp.jboss;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.event.feed.jmx.JmxAttributePollConfig;
+import brooklyn.event.feed.jmx.JmxFeed;
+
+import com.google.common.base.Functions;
+
+public class JBoss6ServerImpl extends JavaWebAppSoftwareProcessImpl implements JBoss6Server {
+
+    public static final Logger log = LoggerFactory.getLogger(JBoss6ServerImpl.class);
+
+    private volatile JmxFeed jmxFeed;
+    
+    public JBoss6ServerImpl() {
+        this(new LinkedHashMap(), null);
+    }
+
+    public JBoss6ServerImpl(Entity parent) {
+        this(new LinkedHashMap(), parent);
+    }
+
+    public JBoss6ServerImpl(Map flags){
+        this(flags, null);
+    }
+
+    public JBoss6ServerImpl(Map flags, Entity parent) {
+        super(flags, parent);
+    }
+
+    @Override
+    public void connectSensors() {
+        super.connectSensors();
+
+        String requestProcessorMbeanName = "jboss.web:type=GlobalRequestProcessor,name=http-*";
+        String serverMbeanName = "jboss.system:type=Server";
+        boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS);
+
+        jmxFeed = JmxFeed.builder()
+                .entity(this)
+                .period(500, TimeUnit.MILLISECONDS)
+                .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_UP)
+                        // TODO instead of setting SERVICE_UP directly, want to use equivalent of 
+                        // addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS).key("serverMBean")...
+                        // but not supported in feed?
+                        .objectName(serverMbeanName)
+                        .attributeName("Started")
+                        .onException(Functions.constant(false))
+                        .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();
+    }
+
+    @Override
+    public void disconnectSensors() {
+        super.disconnectSensors();
+        if (jmxFeed != null) jmxFeed.stop();
+    }
+    
+    @Override
+    public Class<JBoss6Driver> getDriverInterface() {
+        return JBoss6Driver.class;
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6SshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6SshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6SshDriver.java
new file mode 100644
index 0000000..10caeab
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss6SshDriver.java
@@ -0,0 +1,244 @@
+/*
+ * 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.webapp.jboss;
+
+import static brooklyn.util.text.Strings.isEmpty;
+import static java.lang.String.format;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSshDriver;
+
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.java.UsesJmx;
+import brooklyn.entity.java.UsesJmx.JmxAgentModes;
+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;
+
+public class JBoss6SshDriver extends JavaWebAppSshDriver implements JBoss6Driver {
+
+    public static final String SERVER_TYPE = "standard";
+    public static final int DEFAULT_HTTP_PORT = 8080;
+    private static final String PORT_GROUP_NAME = "ports-brooklyn";
+
+    public JBoss6SshDriver(JBoss6ServerImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    @Override
+    public JBoss6ServerImpl getEntity() {
+        return (JBoss6ServerImpl) super.getEntity();
+    }
+    
+    protected String getLogFileLocation() {
+        return Os.mergePathsUnix(getRunDir(), "server", SERVER_TYPE, "log/server.log");
+    }
+
+    protected String getDeploySubdir() {
+        return Os.mergePathsUnix("server", SERVER_TYPE, "deploy");
+    } // FIXME what is this in as6?
+
+    protected String getBindAddress() {
+        return entity.getAttribute(JBoss6Server.BIND_ADDRESS);
+    }
+
+    protected Integer getPortIncrement() {
+        return entity.getAttribute(JBoss6Server.PORT_INCREMENT);
+    }
+
+    protected String getClusterName() {
+        return entity.getAttribute(JBoss6Server.CLUSTER_NAME);
+    }
+
+    // FIXME Should this pattern be used elsewhere? How?
+    @Override
+    public String getExpandedInstallDir() {
+        // Ensure never returns null, so if stop called even if install/start was not then don't throw exception.
+        String result = super.getExpandedInstallDir();
+        return (result != null) ? result : getInstallDir()+"/" + "jboss-"+getVersion();
+    }
+
+    @Override
+    public void postLaunch() {
+        entity.setAttribute(JBoss6Server.HTTP_PORT, DEFAULT_HTTP_PORT + getPortIncrement());
+        super.postLaunch();
+    }
+
+    @Override
+    public void preInstall() {
+        resolver = Entities.newDownloader(this);
+        setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(format("jboss-%s", getVersion()))));
+    }
+
+    @Override
+    public void install() {
+        List<String> urls = resolver.getTargets();
+        String saveAs = resolver.getFilename();
+
+        // Note the -o option to unzip, to overwrite existing files without warning.
+        // The JBoss zip file contains lgpl.txt (at least) twice and the prompt to
+        // overwrite interrupts the installer.
+
+        List<String> commands = new LinkedList<String>();
+        commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, saveAs));
+        commands.add(BashCommands.INSTALL_UNZIP);
+        commands.add(format("unzip -o %s",saveAs));
+
+        newScript(INSTALLING)
+                .body.append(commands)
+                .execute();
+    }
+
+    @Override
+    public void customize() {
+        newScript(CUSTOMIZING)
+                .body.append(
+                        format("mkdir -p %s/server", getRunDir()),
+                        format("cd %s/server", getRunDir()),
+                        format("cp -r %s/server/%s %s", getExpandedInstallDir(), SERVER_TYPE, SERVER_TYPE),
+                        format("cd %s/conf/bindingservice.beans/META-INF/",SERVER_TYPE),
+                        "BJB=\"bindings-jboss-beans.xml\"",
+                        format("sed -i.bk 's/ports-03/%s/' $BJB",PORT_GROUP_NAME),
+                        format("sed -i.bk 's/<parameter>300<\\/parameter>/<parameter>%s<\\/parameter>/' $BJB",getPortIncrement())
+                )
+                .execute();
+
+        getEntity().deployInitialWars();
+    }
+
+    @Override
+    public void launch() {
+        Map<String,Integer> ports = new HashMap<String, Integer>();
+        ports.put("httpPort",getHttpPort());
+        ports.put("jmxPort",getJmxPort());
+
+        Networking.checkPortsValid(ports);
+
+        String clusterArg = isEmpty(getClusterName()) ? "":"-g "+getClusterName();
+        // run.sh must be backgrounded otherwise the script will never return.
+
+        // Don't automatically create pid; instead set JBOSS_PIDFILE to create the pid file we need
+        // 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("export JBOSS_CLASSPATH=%s/lib/jboss-logmanager.jar",getExpandedInstallDir()),
+                        format("export JBOSS_PIDFILE=%s/%s", getRunDir(), PID_FILENAME),
+                        format("%s/bin/run.sh -Djboss.service.binding.set=%s -Djboss.server.base.dir=$RUN_DIR/server ",getExpandedInstallDir(),PORT_GROUP_NAME) +
+                                format("-Djboss.server.base.url=file://$RUN_DIR/server -Djboss.messaging.ServerPeerID=%s ",entity.getId())+
+                                format("-Djboss.boot.server.log.dir=%s/server/%s/log ",getRunDir(),SERVER_TYPE) +
+                                format("-b %s %s -c %s ", getBindAddress(), clusterArg,SERVER_TYPE) +
+                                ">>$RUN_DIR/console 2>&1 </dev/null &",
+                        "for i in {1..10}\n" +
+                                "do\n" +
+                                "    grep -i 'starting' "+getRunDir()+"/console && exit\n" +
+                                "    sleep 1\n" +
+                                "done\n" +
+                                "echo \"Couldn't determine if process is running (console output does not contain 'starting'); continuing but may subsequently fail\""
+                    )
+                .execute();
+    }
+
+    @Override
+    public boolean isRunning() {
+        JmxAgentModes jmxMode = entity.getConfig(UsesJmx.JMX_AGENT_MODE);
+        if (jmxMode == JmxAgentModes.JMX_RMI_CUSTOM_AGENT) {
+            String host = entity.getAttribute(Attributes.HOSTNAME);
+            Integer port = entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT);
+
+            List<String> checkRunningScript = new LinkedList<String>();
+            checkRunningScript.add(
+                    format("%s/bin/twiddle.sh --host %s --port %s get \"jboss.system:type=Server\" Started | grep true || exit 1",
+                            getExpandedInstallDir(), host, port));
+
+            //have to override the CLI/JMX options
+
+            Map<String, Object> flags = new LinkedHashMap<String, Object>();
+            flags.put("env", new LinkedHashMap<String, String>());
+
+            int result = execute(flags, checkRunningScript, "checkRunning " + entity + " on " + getMachine());
+            if (result == 0) return true;
+            if (result == 1) return false;
+            throw new IllegalStateException(format("%s running check gave result code %s",getEntity(),result));
+        } else {
+            return newScript(MutableMap.of(USE_PID_FILE, true), CHECK_RUNNING).execute() == 0;
+        }
+    }
+
+    @Override
+    public void stop() {
+        JmxAgentModes jmxMode = entity.getConfig(UsesJmx.JMX_AGENT_MODE);
+        if (jmxMode == JmxAgentModes.JMX_RMI_CUSTOM_AGENT) {
+            String host = entity.getAttribute(Attributes.HOSTNAME);
+            Integer port = entity.getAttribute(UsesJmx.RMI_REGISTRY_PORT);
+            List<String> shutdownScript = new LinkedList<String>();
+            shutdownScript.add(format("%s/bin/shutdown.sh --host %s --port %s -S", getExpandedInstallDir(), host, port));
+
+            //again, messy copy of parent; but new driver scheme could allow script-helper to customise parameters
+            log.debug("invoking shutdown script for {}: {}", entity, shutdownScript);
+            Map<String, Object> flags = new LinkedHashMap<String, Object>();
+            flags.put("env", new LinkedHashMap<String, String>());
+            int result = execute(flags, shutdownScript, "shutdown " + entity + " on " + getMachine());
+            if (result != 0) log.warn("non-zero result code terminating {}: {}", entity, result);
+            log.debug("done invoking shutdown script for {}", entity);
+        } else {
+            newScript(MutableMap.of(USE_PID_FILE, true), STOPPING).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() {
+        return MutableMap.<String, String>builder()
+                .putAll(super.getShellEnvironment())
+                .put("LAUNCH_JBOSS_IN_BACKGROUND", "1")
+                .put("RUN", getRunDir())
+                .build();
+    }
+
+    @Override
+    protected Map getCustomJavaSystemProperties() {
+        return MutableMap.<String, String>builder()
+                .put("jboss.platform.mbeanserver", null)
+                .put("javax.management.builder.initial", "org.jboss.system.server.jmx.MBeanServerBuilderImpl")
+                .put("java.util.logging.manager", "org.jboss.logmanager.LogManager")
+                .put("org.jboss.logging.Logger.pluginClass", "org.jboss.logging.logmanager.LoggerPluginImpl")
+                .build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Driver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Driver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Driver.java
new file mode 100644
index 0000000..ad5e101
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Driver.java
@@ -0,0 +1,30 @@
+/*
+ * 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.webapp.jboss;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppDriver;
+
+public interface JBoss7Driver extends JavaWebAppDriver{
+
+    /**
+     * The path to the keystore file on the AS7 server machine.
+     * Result is undefined if SSL is not enabled/configured.
+     */
+    public String getSslKeystoreFile();
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Server.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Server.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Server.java
new file mode 100644
index 0000000..090cd6c
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7Server.java
@@ -0,0 +1,105 @@
+/*
+ * 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.webapp.jboss;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcess;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.entity.trait.HasShortName;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey.StringAttributeSensorAndConfigKey;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.javalang.JavaClassNames;
+
+@Catalog(name="JBoss Application Server 7", description="AS7: an open source Java application server from JBoss", iconUrl="classpath:///jboss-logo.png")
+@ImplementedBy(JBoss7ServerImpl.class)
+public interface JBoss7Server extends JavaWebAppSoftwareProcess, HasShortName {
+
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION =
+            ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "7.1.1.Final");
+    // note: 7.1.2.Final fixes many bugs but is not available for download,
+    // see https://community.jboss.org/thread/197780
+    // 7.2.0.Final should be out during Q3 2012
+
+    @SetFromFlag("downloadUrl")
+    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new StringAttributeSensorAndConfigKey(
+            SoftwareProcess.DOWNLOAD_URL, "http://download.jboss.org/jbossas/7.1/jboss-as-${version}/jboss-as-${version}.tar.gz");
+
+    @SetFromFlag("bindAddress")
+    BasicAttributeSensorAndConfigKey<String> BIND_ADDRESS =
+            new StringAttributeSensorAndConfigKey("jboss.bind.address",
+                "Address of interface JBoss should listen on, defaulting 0.0.0.0 (but could set e.g. to attributeWhenReady(HOSTNAME)", 
+                "0.0.0.0");
+
+    @SetFromFlag("managementHttpPort")
+    PortAttributeSensorAndConfigKey MANAGEMENT_HTTP_PORT =
+            new PortAttributeSensorAndConfigKey("webapp.jboss.managementHttpPort", "Management port", "9990+");
+
+    @SetFromFlag("managementHttpsPort")
+    PortAttributeSensorAndConfigKey MANAGEMENT_HTTPS_PORT =
+            new PortAttributeSensorAndConfigKey("webapp.jboss.managementHttpsPort", "Management port", "9443+");
+
+    @SetFromFlag("managementNativePort")
+    PortAttributeSensorAndConfigKey MANAGEMENT_NATIVE_PORT =
+            new PortAttributeSensorAndConfigKey("webapp.jboss.managementNativePort", "Management native port", "10999+");
+
+    /**
+     * Port increments are the standard way to run multiple instances of AS7 on the same machine.
+     */
+    @SetFromFlag("portIncrement")
+    ConfigKey<Integer> PORT_INCREMENT =
+            ConfigKeys.newConfigKey("webapp.jboss.portIncrement", "Port increment for all ports in config file", 0);
+
+    @SetFromFlag("deploymentTimeout")
+    ConfigKey<Integer> DEPLOYMENT_TIMEOUT =
+            ConfigKeys.newConfigKey("webapp.jboss.deploymentTimeout", "Deployment timeout, in seconds", 600);
+    
+    ConfigKey<String> TEMPLATE_CONFIGURATION_URL = ConfigKeys.newConfigKey(
+            "webapp.jboss.templateConfigurationUrl", "Template file (in freemarker format) for the standalone.xml file", 
+            JavaClassNames.resolveClasspathUrl(JBoss7Server.class, "jboss7-standalone.xml"));
+
+    @SetFromFlag("managementUser")
+    ConfigKey<String> MANAGEMENT_USER = ConfigKeys.newConfigKey("webapp.jboss.managementUser",
+            "A user to be placed in the management realm. Brooklyn will use this user to poll sensors",
+            "brooklyn");
+
+    @SetFromFlag("managementPassword")
+    ConfigKey<String> MANAGEMENT_PASSWORD =
+            ConfigKeys.newStringConfigKey("webapp.jboss.managementPassword", "Password for MANAGEMENT_USER.");
+
+    AttributeSensor<String> MANAGEMENT_URL =
+            Sensors.newStringSensor("webapp.jboss.managementUrl", "URL where management endpoint is available");
+
+    AttributeSensor<Integer> MANAGEMENT_STATUS =
+            Sensors.newIntegerSensor("webapp.jboss.managementStatus", "HTTP response code for the management server");
+
+    AttributeSensor<Boolean> MANAGEMENT_URL_UP = 
+            Sensors.newBooleanSensor("webapp.jboss.managementUp", "Management server is responding with OK");
+    
+    public static final AttributeSensor<String> PID_FILE = Sensors.newStringSensor( "jboss.pid.file", "PID file");
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
new file mode 100644
index 0000000..5105f0f
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7ServerImpl.java
@@ -0,0 +1,199 @@
+/*
+ * 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.webapp.jboss;
+
+import java.util.Map;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.config.render.RendererHints;
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.event.feed.http.HttpFeed;
+import brooklyn.event.feed.http.HttpPollConfig;
+import brooklyn.event.feed.http.HttpValueFunctions;
+import brooklyn.location.access.BrooklynAccessUtils;
+import brooklyn.util.guava.Functionals;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HostAndPort;
+
+public class JBoss7ServerImpl extends JavaWebAppSoftwareProcessImpl implements JBoss7Server {
+
+    public static final Logger log = LoggerFactory.getLogger(JBoss7ServerImpl.class);
+
+    private volatile HttpFeed httpFeed;
+    
+    public JBoss7ServerImpl(){
+        super();
+    }
+
+    public JBoss7ServerImpl(@SuppressWarnings("rawtypes") Map flags){
+        this(flags, null);
+    }
+
+    public JBoss7ServerImpl(@SuppressWarnings("rawtypes") Map flags, Entity parent) {
+        super(flags, parent);
+    }
+
+    @Override
+    public Class<?> getDriverInterface() {
+        return JBoss7Driver.class;
+    }
+
+    @Override
+    public JBoss7Driver getDriver() {
+        return (JBoss7Driver) super.getDriver();
+    }
+    
+    static {
+        RendererHints.register(MANAGEMENT_URL, RendererHints.namedActionWithUrl());
+    }
+
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+
+        HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(this,
+                getAttribute(MANAGEMENT_HTTP_PORT) + getConfig(PORT_INCREMENT));
+        
+        String managementUri = String.format("http://%s:%s/management/subsystem/web/connector/http/read-resource",
+                hp.getHostText(), hp.getPort());
+        setAttribute(MANAGEMENT_URL, managementUri);
+        log.debug("JBoss sensors for "+this+" reading from "+managementUri);
+        Map<String, String> includeRuntimeUriVars = ImmutableMap.of("include-runtime","true");
+        boolean retrieveUsageMetrics = getConfig(RETRIEVE_USAGE_METRICS);
+        
+        httpFeed = HttpFeed.builder()
+                .entity(this)
+                .period(200)
+                .baseUri(managementUri)
+                .credentials(getConfig(MANAGEMENT_USER), getConfig(MANAGEMENT_PASSWORD))
+                .poll(new HttpPollConfig<Integer>(MANAGEMENT_STATUS)
+                        .onSuccess(HttpValueFunctions.responseCode())
+                        .suppressDuplicates(true))
+                .poll(new HttpPollConfig<Boolean>(MANAGEMENT_URL_UP)
+                        .onSuccess(HttpValueFunctions.responseCodeEquals(200))
+                        .onFailureOrException(Functions.constant(false))
+                        .suppressDuplicates(true))
+                .poll(new HttpPollConfig<Integer>(REQUEST_COUNT)
+                        .vars(includeRuntimeUriVars)
+                        .onSuccess(HttpValueFunctions.jsonContents("requestCount", Integer.class))
+                        .enabled(retrieveUsageMetrics))
+                .poll(new HttpPollConfig<Integer>(ERROR_COUNT)
+                        .vars(includeRuntimeUriVars)
+                        .onSuccess(HttpValueFunctions.jsonContents("errorCount", Integer.class))
+                        .enabled(retrieveUsageMetrics))
+                .poll(new HttpPollConfig<Integer>(TOTAL_PROCESSING_TIME)
+                        .vars(includeRuntimeUriVars)
+                        .onSuccess(HttpValueFunctions.jsonContents("processingTime", Integer.class))
+                        .enabled(retrieveUsageMetrics))
+                .poll(new HttpPollConfig<Integer>(MAX_PROCESSING_TIME)
+                        .vars(includeRuntimeUriVars)
+                        .onSuccess(HttpValueFunctions.jsonContents("maxTime", Integer.class))
+                        .enabled(retrieveUsageMetrics))
+                .poll(new HttpPollConfig<Long>(BYTES_RECEIVED)
+                        .vars(includeRuntimeUriVars)
+                        // jboss seems to report 0 even if it has received lots of requests; dunno why.
+                        .onSuccess(HttpValueFunctions.jsonContents("bytesReceived", Long.class))
+                        .enabled(retrieveUsageMetrics))
+                .poll(new HttpPollConfig<Long>(BYTES_SENT)
+                        .vars(includeRuntimeUriVars)
+                        .onSuccess(HttpValueFunctions.jsonContents("bytesSent", Long.class))
+                        .enabled(retrieveUsageMetrics))
+                .build();
+        
+        connectServiceUp();
+    }
+    
+    protected void connectServiceUp() {
+        addEnricher(Enrichers.builder().updatingMap(Attributes.SERVICE_NOT_UP_INDICATORS)
+            .from(MANAGEMENT_URL_UP)
+            .computing(Functionals.ifNotEquals(true).value("Management URL not reachable") )
+            .suppressDuplicates(true)
+            .build());
+    }
+    
+    protected void disconnectServiceUp() {
+        disconnectServiceUpIsRunning();
+    }
+    
+    @Override
+    protected void disconnectSensors() {
+        super.disconnectSensors();
+        
+        if (httpFeed != null) httpFeed.stop();
+        disconnectServiceUp();
+    }
+    
+    public int getManagementHttpsPort() {
+        return getAttribute(MANAGEMENT_HTTPS_PORT);
+    }
+    
+    public int getManagementHttpPort() {
+        return getAttribute(MANAGEMENT_HTTP_PORT);
+    }
+    
+    public int getManagementNativePort() {
+        return getAttribute(MANAGEMENT_NATIVE_PORT);
+    }
+    
+    public int getPortOffset() {
+        return getConfig(PORT_INCREMENT);
+    }
+    
+    public boolean isWelcomeRootEnabled() {
+        return false;
+    }
+
+    public String getBindAddress() {
+        return getConfig(BIND_ADDRESS);
+    }
+    
+    public String getManagementBindAddress() {
+        return getConfig(BIND_ADDRESS);
+    }
+    
+    public String getUnsecureBindAddress() {
+        return getConfig(BIND_ADDRESS);
+    }
+    
+    // If empty-string, disables Management security (!) by excluding the security-realm attribute
+    public String getHttpManagementInterfaceSecurityRealm() {
+        return "";
+    }
+
+    public int getDeploymentTimeoutSecs() {
+        return getConfig(DEPLOYMENT_TIMEOUT);
+    }
+
+    /** Path of the keystore file on the AS7 server */
+    public String getHttpsSslKeystoreFile() {
+        return getDriver().getSslKeystoreFile();
+    }
+    
+    @Override
+    public String getShortName() {
+        return "JBossAS7";
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
new file mode 100644
index 0000000..e9db6ba
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jboss/JBoss7SshDriver.java
@@ -0,0 +1,276 @@
+/*
+ * 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.webapp.jboss;
+
+import static java.lang.String.format;
+
+import java.io.InputStream;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSshDriver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.SoftwareProcess;
+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.Strings;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+
+public class JBoss7SshDriver extends JavaWebAppSshDriver implements JBoss7Driver {
+
+    private static final Logger LOG = LoggerFactory.getLogger(JBoss7SshDriver.class);
+
+    // TODO more configurability of config files, java memory, etc
+
+    public static final String SERVER_TYPE = "standalone";
+    public static final String CONFIG_FILE = "standalone-brooklyn.xml";
+    public static final String KEYSTORE_FILE = ".keystore";
+    public static final String MANAGEMENT_REALM = "ManagementRealm";
+
+    public JBoss7SshDriver(JBoss7ServerImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    @Override
+    public JBoss7ServerImpl getEntity() {
+        return (JBoss7ServerImpl) super.getEntity();
+    }
+
+    @Override
+    public String getSslKeystoreFile() {
+        return Os.mergePathsUnix(getRunDir(), SERVER_TYPE, "configuration", KEYSTORE_FILE);
+    }
+    
+    protected String getTemplateConfigurationUrl() {
+        return entity.getConfig(JBoss7Server.TEMPLATE_CONFIGURATION_URL);
+    }
+
+    @Override
+    protected String getLogFileLocation() {
+        return Os.mergePathsUnix(getRunDir(), SERVER_TYPE, "log/server.log");
+    }
+
+    @Override
+    protected String getDeploySubdir() {
+        return Os.mergePathsUnix(SERVER_TYPE, "deployments");
+    }
+
+    private Integer getManagementHttpPort() {
+        return entity.getAttribute(JBoss7Server.MANAGEMENT_HTTP_PORT);
+    }
+
+    private Integer getManagementHttpsPort() {
+        return entity.getAttribute(JBoss7Server.MANAGEMENT_HTTPS_PORT);
+    }
+
+    private Integer getManagementNativePort() {
+        return entity.getAttribute(JBoss7Server.MANAGEMENT_NATIVE_PORT);
+    }
+
+    private String getManagementUsername() {
+        return entity.getConfig(JBoss7Server.MANAGEMENT_USER);
+    }
+
+    private String getManagementPassword() {
+        return entity.getConfig(JBoss7Server.MANAGEMENT_PASSWORD);
+    }
+
+    @Override
+    public void preInstall() {
+        resolver = Entities.newDownloader(this);
+        setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(format("jboss-as-%s", 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("tar xzfv " + saveAs);
+
+        newScript(INSTALLING)
+                // don't set vars yet -- it resolves dependencies (e.g. DB) which we don't want until we start
+                .environmentVariablesReset()
+                .body.append(commands)
+                .execute();
+    }
+
+    /**
+     * AS7 config notes and TODOs:
+     * We're using the http management interface on port managementPort
+     * We're not using any JMX.
+     * - AS 7 simply doesn't boot with Sun JMX enabled (https://issues.jboss.org/browse/JBAS-7427)
+     * - 7.1 onwards uses Remoting 3, which we haven't configured
+     * - We have generic support for jmxmp, which one could configure
+     * We're completely disabling security on the management interface.
+     * - In the future we probably want to use the as7/bin/add-user.sh script using config keys for user and password
+     * - Or we could create our own security realm and use that.
+     * We disable the root welcome page, since we can't deploy our own root otherwise
+     * We bind all interfaces to entity.hostname, rather than 127.0.0.1.
+     */
+    @Override
+    public void customize() {
+        // Check that a password was set for the management user
+        Preconditions.checkState(Strings.isNonBlank(getManagementUsername()), "User for management realm required");
+        String managementPassword = getManagementPassword();
+        if (Strings.isBlank(managementPassword)) {
+            LOG.debug(this+" has no password specified for "+JBoss7Server.MANAGEMENT_PASSWORD.getName()+"; using a random string");
+            entity.setConfig(JBoss7Server.MANAGEMENT_PASSWORD, Strings.makeRandomId(8));
+        }
+        String hashedPassword = hashPassword(getManagementUsername(), getManagementPassword(), MANAGEMENT_REALM);
+
+        // Check that ports are all configured
+        Map<String,Integer> ports = MutableMap.<String,Integer>builder()
+                .put("managementHttpPort", getManagementHttpPort()) 
+                .put("managementHttpsPort", getManagementHttpsPort())
+                .put("managementNativePort", getManagementNativePort())
+                .build();
+        if (isProtocolEnabled("HTTP")) {
+            ports.put("httpPort", getHttpPort());
+        }
+        if (isProtocolEnabled("HTTPS")) {
+            ports.put("httpsPort", getHttpsPort());
+        }
+        Networking.checkPortsValid(ports);
+
+        // Check hostname is defined
+        String hostname = entity.getAttribute(SoftwareProcess.HOSTNAME);
+        Preconditions.checkNotNull(hostname, "AS 7 entity must set hostname otherwise server will only be visible on localhost");
+
+        // Copy the install files to the run-dir and add the management user
+        newScript(CUSTOMIZING)
+                // don't set vars yet -- it resolves dependencies (e.g. DB) which we don't want until we start
+                .environmentVariablesReset()
+                .body.append(
+                        format("cp -r %s/%s . || exit $!", getExpandedInstallDir(), SERVER_TYPE),
+                        format("echo -e '\n%s=%s' >> %s/%s/configuration/mgmt-users.properties",
+                                getManagementUsername(), hashedPassword, getRunDir(), SERVER_TYPE)
+                    )
+                .execute();
+
+        // Copy the keystore across, if there is one
+        if (isProtocolEnabled("HTTPS")) {
+            String keystoreUrl = Preconditions.checkNotNull(getSslKeystoreUrl(), "keystore URL must be specified if using HTTPS for "+entity);
+            String destinationSslKeystoreFile = getSslKeystoreFile();
+            InputStream keystoreStream = resource.getResourceFromUrl(keystoreUrl);
+            getMachine().copyTo(keystoreStream, destinationSslKeystoreFile);
+        }
+
+        // Copy the configuration file across
+        String destinationConfigFile = Os.mergePathsUnix(getRunDir(), SERVER_TYPE, "configuration", CONFIG_FILE);
+        copyTemplate(getTemplateConfigurationUrl(), destinationConfigFile);
+
+        // Copy the initial wars to the deploys directory
+        getEntity().deployInitialWars();
+    }
+
+    @Override
+    public void launch() {
+        entity.setAttribute(JBoss7Server.PID_FILE, Os.mergePathsUnix(getRunDir(), PID_FILENAME));
+
+        // We wait for evidence of JBoss running because, using
+        // brooklyn.ssh.config.tool.class=brooklyn.util.internal.ssh.cli.SshCliTool,
+        // we saw the ssh session return before the JBoss process was fully running
+        // so the process failed to start.
+        newScript(MutableMap.of(USE_PID_FILE, false), LAUNCHING)
+                .body.append(
+                        "export LAUNCH_JBOSS_IN_BACKGROUND=true",
+                        format("export JBOSS_HOME=%s", getExpandedInstallDir()),
+                        format("export JBOSS_PIDFILE=%s/%s", getRunDir(), PID_FILENAME),
+                        format("%s/bin/%s.sh ", getExpandedInstallDir(), SERVER_TYPE) +
+                                format("--server-config %s ", CONFIG_FILE) +
+                                format("-Djboss.server.base.dir=%s/%s ", getRunDir(), SERVER_TYPE) +
+                                format("\"-Djboss.server.base.url=file://%s/%s\" ", getRunDir(), SERVER_TYPE) +
+                                "-Djava.net.preferIPv4Stack=true " +
+                                "-Djava.net.preferIPv6Addresses=false " +
+                                format(" >> %s/console 2>&1 </dev/null &", getRunDir()),
+                        "for i in {1..10}\n" +
+                                "do\n" +
+                                "    grep -i 'starting' "+getRunDir()+"/console && exit\n" +
+                                "    sleep 1\n" +
+                                "done\n" +
+                                "echo \"Couldn't determine if process is running (console output does not contain 'starting'); continuing but may subsequently fail\""
+                    )
+                .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).environmentVariablesReset().execute();
+    }
+
+    @Override
+    public void kill() {
+        newScript(MutableMap.of(USE_PID_FILE, true), KILLING).execute();
+    }
+
+    @Override
+    protected List<String> getCustomJavaConfigOptions() {
+        return MutableList.<String>builder()
+                .addAll(super.getCustomJavaConfigOptions())
+                .add("-Xms200m")
+                .add("-Xmx800m")
+                .add("-XX:MaxPermSize=400m")
+                .build();
+    }
+
+    /**
+     * Creates a hash of a username, password and security realm that is suitable for use
+     * with AS7 and Wildfire.
+     * <p/>
+     * Although AS7 has an <code>add-user.sh</code> script it is unsuitable for use in
+     * non-interactive modes. (See AS7-5061 for details.) Versions 7.1.2+ (EAP) accept
+     * a <code>--silent</code> flag. When this entity is updated past 7.1.1 we should
+     * probably use that instead.
+     * <p/>
+     * This method mirrors AS7 and Wildfire's method of hashing user's passwords. Refer
+     * to its class <code>UsernamePasswordHashUtil.generateHashedURP</code> for their
+     * implementation.
+     *
+     * @see <a href="https://issues.jboss.org/browse/AS7-5061">AS7-5061</a>
+     * @see <a href="https://github.com/jboss-remoting/jboss-sasl/blob/master/src/main/java/org/jboss/sasl/util/UsernamePasswordHashUtil.java">
+     *     UsernamePasswordHashUtil.generateHashedURP</a>
+     * @return <code>HEX(MD5(username ':' realm ':' password))</code>
+     */
+    public static String hashPassword(String username, String password, String realm) {
+        String concat = username + ":" + realm + ":" + password;
+        byte[] hashed = Hashing.md5().hashString(concat, Charsets.UTF_8).asBytes();
+        return BaseEncoding.base16().lowerCase().encode(hashed);
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Driver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Driver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Driver.java
new file mode 100644
index 0000000..4732cf5
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Driver.java
@@ -0,0 +1,24 @@
+/*
+ * 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.webapp.jetty;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppDriver;
+
+public interface Jetty6Driver extends JavaWebAppDriver {
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Server.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Server.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Server.java
new file mode 100644
index 0000000..5532661
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6Server.java
@@ -0,0 +1,61 @@
+/*
+ * 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.webapp.jetty;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcess;
+
+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.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.time.Duration;
+
+/**
+ * An {@link brooklyn.entity.Entity} that represents a single Jetty instance.
+ */
+@Catalog(name="Jetty6 Server", description="Old version (v6 @ Mortbay) of the popular Jetty webapp container", iconUrl="classpath:///jetty-logo.png")
+@ImplementedBy(Jetty6ServerImpl.class)
+public interface Jetty6Server extends JavaWebAppSoftwareProcess, UsesJmx, HasShortName {
+
+    @SetFromFlag("version")
+    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "6.1.26");
+
+    ConfigKey<Duration> START_TIMEOUT = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.START_TIMEOUT, Duration.FIVE_MINUTES);
+
+    @SetFromFlag("configXmlTemplateUrl")
+    ConfigKey<String> CONFIG_XML_TEMPLATE_URL = ConfigKeys.newStringConfigKey("jetty.configXml.templateUrl", "Extra XML configuration file template URL if required");
+
+    @SetFromFlag("downloadUrl")
+    BasicAttributeSensorAndConfigKey<String> DOWNLOAD_URL = new BasicAttributeSensorAndConfigKey<String>(
+            SoftwareProcess.DOWNLOAD_URL, "http://get.jenv.mvnsearch.org/download/jetty/jetty-${version}.zip");
+
+    AttributeSensor<Integer> RESPONSES_4XX_COUNT =
+            Sensors.newIntegerSensor("webapp.responses.4xx", "Responses in the 400's");
+
+    AttributeSensor<Integer> RESPONSES_5XX_COUNT =
+            Sensors.newIntegerSensor("webapp.responses.5xx", "Responses in the 500's");
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java
new file mode 100644
index 0000000..1e9e533
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6ServerImpl.java
@@ -0,0 +1,141 @@
+/*
+ * 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.webapp.jetty;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSoftwareProcessImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.java.JavaAppUtils;
+import brooklyn.entity.java.UsesJmx;
+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 Jetty instance.
+ */
+public class Jetty6ServerImpl extends JavaWebAppSoftwareProcessImpl implements Jetty6Server {
+
+    private static final Logger log = LoggerFactory.getLogger(Jetty6ServerImpl.class);
+
+    private volatile JmxFeed jmxFeedJetty, jmxFeedMx;
+
+    @Override
+    public void connectSensors() {
+        super.connectSensors();
+        
+        if (getDriver().isJmxEnabled()) {
+            String serverMbeanName = "org.mortbay.jetty:type=server,id=0";
+            String statsMbeanName = "org.mortbay.jetty.handler:type=atomicstatisticshandler,id=0";
+
+            jmxFeedJetty = JmxFeed.builder()
+                    .entity(this)
+                    .period(500, TimeUnit.MILLISECONDS)
+                    .pollAttribute(new JmxAttributePollConfig<Boolean>(SERVICE_UP)
+                            .objectName(serverMbeanName)
+                            .attributeName("running")
+                            .onSuccess(Functions.forPredicate(Predicates.<Object>equalTo(true)))
+                            .setOnFailureOrException(false))
+                    .pollAttribute(new JmxAttributePollConfig<Integer>(REQUEST_COUNT)
+                            .objectName(statsMbeanName)
+                            .attributeName("requests"))
+                    .pollAttribute(new JmxAttributePollConfig<Integer>(RESPONSES_4XX_COUNT)
+                            .objectName(statsMbeanName)
+                            .attributeName("responses4xx"))
+                    .pollAttribute(new JmxAttributePollConfig<Integer>(RESPONSES_5XX_COUNT)
+                            .objectName(statsMbeanName)
+                            .attributeName("responses5xx"))
+                    .pollAttribute(new JmxAttributePollConfig<Integer>(TOTAL_PROCESSING_TIME)
+                            .objectName(statsMbeanName)
+                            .attributeName("requestTimeTotal"))
+                    .pollAttribute(new JmxAttributePollConfig<Integer>(MAX_PROCESSING_TIME)
+                            .objectName(statsMbeanName)
+                            .attributeName("requestTimeMax"))
+                    // NB: requestsActive may be useful
+                    .build();
+            
+            addEnricher(Enrichers.builder()
+                    .combining(RESPONSES_4XX_COUNT, RESPONSES_5XX_COUNT)
+                    .publishing(ERROR_COUNT)
+                    .computingSum()
+                    .build());
+
+            jmxFeedMx = JavaAppUtils.connectMXBeanSensors(this);
+        } else {
+            // if not using JMX
+            log.warn("Jetty running without JMX monitoring; limited visibility of service available");
+            // TODO we could do simple things, like check that web server is accepting connections
+        }
+    }
+
+    @Override
+    protected void disconnectSensors() {
+        if (jmxFeedJetty != null) jmxFeedJetty.stop();
+        if (jmxFeedMx != null) jmxFeedMx.stop();
+        super.disconnectSensors();
+    }
+
+    public Integer getJmxPort() {
+        if (((Jetty6Driver) getDriver()).isJmxEnabled()) {
+            return getAttribute(UsesJmx.JMX_PORT);
+        } else {
+            return Integer.valueOf(-1);
+        }
+    }
+
+    @Override
+    public Class getDriverInterface() {
+        return Jetty6Driver.class;
+    }
+    
+    @Override
+    public String getShortName() {
+        return "Jetty";
+    }
+    
+    @Override
+    public void deploy(String url, String targetName) {
+        super.deploy(url, targetName);
+        restartIfRunning();
+    }
+
+    @Override
+    public void undeploy(String targetName) {
+        super.undeploy(targetName);
+        restartIfRunning();
+    }
+    
+    protected void restartIfRunning() {
+        // TODO for now we simply restart jetty to achieve "hot deployment"; should use the config mechanisms
+        Lifecycle serviceState = getAttribute(SERVICE_STATE_ACTUAL);
+        if (serviceState == Lifecycle.RUNNING)
+            restart();
+        // may need a restart also if deploy effector is done in parallel to starting
+        // but note this routine is used by initialDeployWars so just being in starting state is not enough!
+    }
+
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6SshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6SshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6SshDriver.java
new file mode 100644
index 0000000..49f1f15
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/jetty/Jetty6SshDriver.java
@@ -0,0 +1,174 @@
+/*
+ * 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.webapp.jetty;
+
+import static java.lang.String.format;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.webapp.JavaWebAppSshDriver;
+
+import brooklyn.entity.basic.Entities;
+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.Strings;
+
+public class Jetty6SshDriver extends JavaWebAppSshDriver implements Jetty6Driver {
+
+    public Jetty6SshDriver(Jetty6ServerImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    @Override
+    protected String getLogFileLocation() {
+        // TODO no wildcard, also there is .requests.log
+        return Os.mergePathsUnix(getRunDir(), "logs", "*.stderrout.log");
+    }
+
+    @Override
+    protected String getDeploySubdir() {
+       return "webapps";
+    }
+
+    @Override
+    public void preInstall() {
+        resolver = Entities.newDownloader(this);
+        setExpandedInstallDir(Os.mergePaths(getInstallDir(), resolver.getUnpackedDirectoryName(format("jetty-%s", 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_ZIP);
+        commands.add("unzip "+saveAs);
+
+        newScript(INSTALLING)
+                .body.append(commands)
+                .execute();
+    }
+
+    @Override
+    public void customize() {
+        newScript(CUSTOMIZING)
+                .body.append(
+                        // create app-specific dirs
+                        "mkdir logs contexts webapps",
+                        // link to the binary directories; silly that we have to do this but jetty has only one notion of "jetty.home" 
+                        // (jetty.run is used only for writing the pid file, not for looking up webapps or even for logging)
+                        format("for x in start.jar bin contrib modules lib extras; do ln -s %s/$x $x ; done", getExpandedInstallDir()),
+                        // copy config files across
+                        format("for x in etc resources; do cp -r %s/$x $x ; done", getExpandedInstallDir())
+                    )
+                .execute();
+
+
+        // Copy configuration XML files across
+        String destinationBrooklynConfig = Os.mergePathsUnix(getRunDir(), "etc/jetty-brooklyn.xml");
+        copyTemplate("classpath://org/apache/brooklyn/entity/webapp/jetty/jetty-brooklyn.xml", destinationBrooklynConfig);
+        String customConfigTemplateUrl = getConfigXmlTemplateUrl();
+        if (Strings.isNonEmpty(customConfigTemplateUrl)) {
+            String destinationConfigFile = Os.mergePathsUnix(getRunDir(), "etc/jetty-custom.xml");
+            copyTemplate(customConfigTemplateUrl, destinationConfigFile);
+        }
+
+        getEntity().deployInitialWars();
+    }
+
+    private String getConfigXmlTemplateUrl() {
+        return getEntity().getConfig(Jetty6Server.CONFIG_XML_TEMPLATE_URL);
+    }
+
+    @Override
+    public void launch() {
+        Map ports = MutableMap.of("httpPort", getHttpPort(), "jmxPort", getJmxPort(), "rmiRegistryPort", getRmiRegistryPort());
+        Networking.checkPortsValid(ports);
+
+        newScript(MutableMap.of(USE_PID_FILE, false), LAUNCHING)
+                .body.append(
+                        "./bin/jetty.sh start jetty-brooklyn.xml jetty.xml jetty-logging.xml jetty-stats.xml " +
+                                (Strings.isEmpty(getConfigXmlTemplateUrl()) ? "" : "jetty-custom.xml ") +
+                                ">> $RUN_DIR/console 2>&1 < /dev/null",
+                        "for i in {1..10} ; do\n" +
+                        "    if [ -s "+getLogFileLocation()+" ]; then exit; fi\n" +
+                        "    sleep 1\n" +
+                        "done",
+                        "echo \"Couldn't determine if jetty-server is running (log file is still empty); continuing but may subsequently fail\""
+                    )
+                .execute();
+        log.debug("launched jetty");
+    }
+
+    @Override
+    public boolean isRunning() {
+        return newScript(MutableMap.of(USE_PID_FILE, "jetty.pid"), CHECK_RUNNING).execute() == 0;
+    }
+
+    @Override
+    public void stop() {
+        newScript(MutableMap.of(USE_PID_FILE, false), STOPPING)
+                .body.append("./bin/jetty.sh stop")
+                .execute();
+    }
+
+    // not used, but an alternative to stop which might be useful
+    public void kill1() {
+        newScript(MutableMap.of(USE_PID_FILE, "jetty.pid"), STOPPING).execute();
+    }
+
+    public void kill9() {
+        newScript(MutableMap.of(USE_PID_FILE, "jetty.pid"), KILLING).execute();
+    }
+
+    @Override
+    public void kill() {
+        kill9();
+    }
+
+    @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() {
+        return MutableMap.<String, String>builder()
+                .putAll(super.getShellEnvironment())
+                .put("JETTY_RUN", getRunDir())
+                .put("JETTY_HOME", getRunDir())
+                .put("JETTY_LOGS", Os.mergePathsUnix(getRunDir(), "logs"))
+                .put("JETTY_PORT", getHttpPort().toString())
+                .renameKey("JAVA_OPTS", "JAVA_OPTIONS")
+                .build();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppDriver.java
new file mode 100644
index 0000000..167f843
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppDriver.java
@@ -0,0 +1,29 @@
+/*
+ * 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.webapp.nodejs;
+
+import brooklyn.entity.basic.SoftwareProcessDriver;
+
+public interface NodeJsWebAppDriver extends SoftwareProcessDriver {
+
+    Integer getHttpPort();
+
+    String getAppDir();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppService.java
new file mode 100644
index 0000000..44e5885
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppService.java
@@ -0,0 +1,75 @@
+/*
+ * 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.webapp.nodejs;
+
+import java.util.List;
+
+import org.apache.brooklyn.catalog.Catalog;
+import org.apache.brooklyn.entity.webapp.WebAppService;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
+import brooklyn.entity.basic.SoftwareProcess;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.location.PortRange;
+import brooklyn.location.basic.PortRanges;
+import brooklyn.util.flags.SetFromFlag;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.reflect.TypeToken;
+
+@Catalog(name="Node.JS Application",
+        description="Node.js is a cross-platform runtime environment for server-side and networking applications. Node.js applications are written in JavaScriptq",
+        iconUrl="classpath:///nodejs-logo.png")
+@ImplementedBy(NodeJsWebAppServiceImpl.class)
+public interface NodeJsWebAppService extends SoftwareProcess, WebAppService {
+
+    ConfigKey<String> SUGGESTED_VERSION = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.SUGGESTED_VERSION, "stable");
+
+    @SetFromFlag("httpPort")
+    ConfigKey<PortRange> HTTP_PORT = ConfigKeys.newConfigKeyWithDefault(Attributes.HTTP_PORT.getConfigKey(), PortRanges.fromInteger(3000));
+
+    @SetFromFlag("gitRepoUrl")
+    ConfigKey<String> APP_GIT_REPOSITORY_URL = ConfigKeys.newStringConfigKey("nodejs.gitRepo.url", "The Git repository where the application is hosted");
+
+    @SetFromFlag("archiveUrl")
+    ConfigKey<String> APP_ARCHIVE_URL = ConfigKeys.newStringConfigKey("nodejs.archive.url", "The URL where the application archive is hosted");
+
+    @SetFromFlag("appFileName")
+    ConfigKey<String> APP_FILE = ConfigKeys.newStringConfigKey("nodejs.app.fileName", "The NodeJS application file to start", "app.js");
+
+    @SetFromFlag("appName")
+    ConfigKey<String> APP_NAME = ConfigKeys.newStringConfigKey("nodejs.app.name", "The name of the NodeJS application");
+
+    @SetFromFlag("appCommand")
+    ConfigKey<String> APP_COMMAND = ConfigKeys.newStringConfigKey("nodejs.app.command", "Command to start the NodeJS application (defaults to node)", "node");
+
+    @SetFromFlag("appCommandLine")
+    ConfigKey<String> APP_COMMAND_LINE = ConfigKeys.newStringConfigKey("nodejs.app.commandLine", "Replacement command line to start the NodeJS application (ignores command and file if set)");
+
+    @SetFromFlag("nodePackages")
+    ConfigKey<List<String>> NODE_PACKAGE_LIST = ConfigKeys.newConfigKey(new TypeToken<List<String>>() { },
+            "nodejs.packages", "The NPM packages to install", ImmutableList.<String>of());
+
+    ConfigKey<String> SERVICE_UP_PATH = ConfigKeys.newStringConfigKey("nodejs.serviceUp.path", "Path to use when checking the NodeJS application is running", "/");
+
+    Integer getHttpPort();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppServiceImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppServiceImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppServiceImpl.java
new file mode 100644
index 0000000..74eb500
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/nodejs/NodeJsWebAppServiceImpl.java
@@ -0,0 +1,92 @@
+/*
+ * 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.webapp.nodejs;
+
+import org.apache.brooklyn.entity.webapp.WebAppServiceMethods;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.SoftwareProcessImpl;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.event.feed.http.HttpFeed;
+import brooklyn.event.feed.http.HttpPollConfig;
+import brooklyn.event.feed.http.HttpValueFunctions;
+import brooklyn.location.access.BrooklynAccessUtils;
+
+import com.google.common.base.Predicates;
+import com.google.common.net.HostAndPort;
+
+public class NodeJsWebAppServiceImpl extends SoftwareProcessImpl implements NodeJsWebAppService {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NodeJsWebAppService.class);
+
+    private transient HttpFeed httpFeed;
+
+    @Override
+    public Class<?> getDriverInterface() {
+        return NodeJsWebAppDriver.class;
+    }
+
+    @Override
+    public NodeJsWebAppDriver getDriver() {
+        return (NodeJsWebAppDriver) super.getDriver();
+    }
+
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+
+        ConfigToAttributes.apply(this);
+
+        HostAndPort accessible = BrooklynAccessUtils.getBrooklynAccessibleAddress(this, getHttpPort());
+        String nodeJsUrl = String.format("http://%s:%d", accessible.getHostText(), accessible.getPort());
+        LOG.info("Connecting to {}", nodeJsUrl);
+
+        httpFeed = HttpFeed.builder()
+                .entity(this)
+                .baseUri(nodeJsUrl)
+                .poll(new HttpPollConfig<Boolean>(SERVICE_UP)
+                        .suburl(getConfig(NodeJsWebAppService.SERVICE_UP_PATH))
+                        .checkSuccess(Predicates.alwaysTrue())
+                        .onSuccess(HttpValueFunctions.responseCodeEquals(200))
+                        .setOnException(false))
+                .build();
+
+        WebAppServiceMethods.connectWebAppServerPolicies(this);
+    }
+
+    @Override
+    public void disconnectSensors() {
+        if (httpFeed != null) httpFeed.stop();
+        super.disconnectSensors();
+    }
+
+    @Override
+    protected void postStop() {
+        super.postStop();
+
+        setAttribute(REQUESTS_PER_SECOND_LAST, 0D);
+        setAttribute(REQUESTS_PER_SECOND_IN_WINDOW, 0D);
+    }
+
+    @Override
+    public Integer getHttpPort() { return getAttribute(Attributes.HTTP_PORT); }
+
+}