brooklyn-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From aleds...@apache.org
Subject [19/35] incubator-brooklyn git commit: [BROOKLYN-162] package rename to org.apache.brooklyn: software/webapp
Date Wed, 12 Aug 2015 15:55:37 GMT
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
new file mode 100644
index 0000000..a9e9809
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ControlledDynamicWebAppClusterImpl.java
@@ -0,0 +1,327 @@
+/*
+ * 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 java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.entity.proxy.LoadBalancer;
+import org.apache.brooklyn.entity.proxy.nginx.NginxController;
+import org.apache.brooklyn.entity.webapp.tomcat.TomcatServer;
+import org.apache.brooklyn.management.Task;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.Entity;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigurableEntityFactory;
+import brooklyn.entity.basic.DynamicGroupImpl;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityPredicates;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.basic.ServiceStateLogic;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Startable;
+import brooklyn.entity.trait.StartableMethods;
+import brooklyn.event.SensorEvent;
+import brooklyn.event.SensorEventListener;
+import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.location.Location;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.QuorumCheck.QuorumChecks;
+import brooklyn.util.exceptions.Exceptions;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+public class ControlledDynamicWebAppClusterImpl extends DynamicGroupImpl implements ControlledDynamicWebAppCluster {
+
+    public static final Logger log = LoggerFactory.getLogger(ControlledDynamicWebAppClusterImpl.class);
+
+    public ControlledDynamicWebAppClusterImpl() {
+        this(MutableMap.of(), null);
+    }
+    
+    public ControlledDynamicWebAppClusterImpl(Map<?,?> flags) {
+        this(flags, null);
+    }
+    
+    public ControlledDynamicWebAppClusterImpl(Entity parent) {
+        this(MutableMap.of(), parent);
+    }
+    
+    @Deprecated
+    public ControlledDynamicWebAppClusterImpl(Map<?,?> flags, Entity parent) {
+        super(flags, parent);
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        
+        ConfigToAttributes.apply(this, FACTORY);
+        ConfigToAttributes.apply(this, MEMBER_SPEC);
+        ConfigToAttributes.apply(this, CONTROLLER);
+        ConfigToAttributes.apply(this, CONTROLLER_SPEC);
+        ConfigToAttributes.apply(this, WEB_CLUSTER_SPEC);
+        ConfigToAttributes.apply(this, CONTROLLED_GROUP);
+        
+        ConfigurableEntityFactory<? extends WebAppService> webServerFactory = getAttribute(FACTORY);
+        EntitySpec<? extends WebAppService> webServerSpec = getAttribute(MEMBER_SPEC);
+        if (webServerFactory == null && webServerSpec == null) {
+            log.debug("creating default web server spec for {}", this);
+            webServerSpec = EntitySpec.create(TomcatServer.class);
+            setAttribute(MEMBER_SPEC, webServerSpec);
+        }
+        
+        log.debug("creating cluster child for {}", this);
+        // Note relies on initial_size being inherited by DynamicWebAppCluster, because key id is identical
+        EntitySpec<? extends DynamicWebAppCluster> webClusterSpec = getAttribute(WEB_CLUSTER_SPEC);
+        Map<String,Object> webClusterFlags;
+        if (webServerSpec != null) {
+            webClusterFlags = MutableMap.<String,Object>of("memberSpec", webServerSpec);
+        } else {
+            webClusterFlags = MutableMap.<String,Object>of("factory", webServerFactory);
+        }
+        if (webClusterSpec == null) {
+            log.debug("creating default web cluster spec for {}", this);
+            webClusterSpec = EntitySpec.create(DynamicWebAppCluster.class);
+        }
+        boolean hasMemberSpec = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.MEMBER_SPEC) || webClusterSpec.getFlags().containsKey("memberSpec");
+        @SuppressWarnings("deprecation")
+        boolean hasMemberFactory = webClusterSpec.getConfig().containsKey(DynamicWebAppCluster.FACTORY) || webClusterSpec.getFlags().containsKey("factory");
+        if (!(hasMemberSpec || hasMemberFactory)) {
+            webClusterSpec.configure(webClusterFlags);
+        } else {
+            log.warn("In {}, not setting cluster's {} because already set on webClusterSpec", new Object[] {this, webClusterFlags.keySet()});
+        }
+        setAttribute(WEB_CLUSTER_SPEC, webClusterSpec);
+        
+        DynamicWebAppCluster cluster = addChild(webClusterSpec);
+        if (Entities.isManaged(this)) Entities.manage(cluster);
+        setAttribute(CLUSTER, cluster);
+        setEntityFilter(EntityPredicates.isMemberOf(cluster));
+        
+        LoadBalancer controller = getAttribute(CONTROLLER);
+        if (controller == null) {
+            EntitySpec<? extends LoadBalancer> controllerSpec = getAttribute(CONTROLLER_SPEC);
+            if (controllerSpec == null) {
+                log.debug("creating controller using default spec for {}", this);
+                controllerSpec = EntitySpec.create(NginxController.class);
+                setAttribute(CONTROLLER_SPEC, controllerSpec);
+            } else {
+                log.debug("creating controller using custom spec for {}", this);
+            }
+            controller = addChild(controllerSpec);
+            addEnricher(Enrichers.builder().propagating(LoadBalancer.PROXY_HTTP_PORT, LoadBalancer.PROXY_HTTPS_PORT).from(controller).build());
+            if (Entities.isManaged(this)) Entities.manage(controller);
+            setAttribute(CONTROLLER, controller);
+        }
+        
+        Group controlledGroup = getAttribute(CONTROLLED_GROUP);
+        if (controlledGroup == null) {
+            log.debug("using cluster as controlledGroup for {}", this);
+            controlledGroup = cluster;
+            setAttribute(CONTROLLED_GROUP, cluster);
+        } else {
+            log.debug("using custom controlledGroup {} for {}", controlledGroup, this);
+        }
+        
+        doBind();
+    }
+
+    @Override
+    protected void initEnrichers() {
+        if (config().getLocalRaw(UP_QUORUM_CHECK).isAbsent()) {
+            config().set(UP_QUORUM_CHECK, QuorumChecks.newInstance(2, 1.0, false));
+        }
+        super.initEnrichers();
+        ServiceStateLogic.newEnricherFromChildrenUp().checkChildrenOnly().requireUpChildren(getConfig(UP_QUORUM_CHECK)).addTo(this);
+    }
+    
+    @Override
+    public void rebind() {
+        super.rebind();
+        doBind();
+    }
+
+    protected void doBind() {
+        DynamicWebAppCluster cluster = getAttribute(CLUSTER);
+        if (cluster != null) {
+            subscribe(cluster, DynamicWebAppCluster.GROUP_MEMBERS, new SensorEventListener<Object>() {
+                @Override public void onEvent(SensorEvent<Object> event) {
+                    // TODO inefficient impl; also worth extracting this into a mixin of some sort.
+                    rescanEntities();
+                }});
+        }
+    }
+    
+    @Override
+    public LoadBalancer getController() {
+        return getAttribute(CONTROLLER);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public synchronized ConfigurableEntityFactory<WebAppService> getFactory() {
+        return (ConfigurableEntityFactory<WebAppService>) getAttribute(FACTORY);
+    }
+    
+    // TODO convert to an entity reference which is serializable
+    @Override
+    public synchronized DynamicWebAppCluster getCluster() {
+        return getAttribute(CLUSTER);
+    }
+    
+    @Override
+    public Group getControlledGroup() {
+        return getAttribute(CONTROLLED_GROUP);
+    }
+    
+    @Override
+    public void start(Collection<? extends Location> locations) {
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STARTING);
+
+        try {
+            if (isLegacyConstruction()) {
+                init();
+            }
+
+            if (locations.isEmpty()) locations = getLocations();
+            addLocations(locations);
+
+            LoadBalancer loadBalancer = getController();
+            loadBalancer.bind(MutableMap.of("serverPool", getControlledGroup()));
+
+            List<Entity> childrenToStart = MutableList.<Entity>of(getCluster());
+            // Set controller as child of cluster, if it does not already have a parent
+            if (getController().getParent() == null) {
+                addChild(getController());
+            }
+
+            // And only start controller if we are parent. Favour locations defined on controller, if present
+            Task<List<Void>> startControllerTask = null;
+            if (this.equals(getController().getParent())) {
+                if (getController().getLocations().size() == 0) {
+                    childrenToStart.add(getController());
+                } else {
+                     startControllerTask = Entities.invokeEffectorList(this, MutableList.<Entity>of(getController()), Startable.START, ImmutableMap.of("locations", getController().getLocations()));
+                }
+            }
+
+            Entities.invokeEffectorList(this, childrenToStart, Startable.START, ImmutableMap.of("locations", locations)).get();
+            if (startControllerTask != null) {
+                startControllerTask.get();
+            }
+
+            // wait for everything to start, then update controller, to ensure it is up to date
+            // (will happen asynchronously as members come online, but we want to force it to happen)
+            getController().update();
+
+            ServiceStateLogic.setExpectedState(this, Lifecycle.RUNNING);
+        } catch (Exception e) {
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
+            throw Exceptions.propagate(e);
+        } finally {
+            connectSensors();
+        }
+    }
+
+    @Override
+    public void stop() {
+        ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPING);
+
+        try {
+            List<Startable> tostop = Lists.newArrayList();
+            if (this.equals(getController().getParent())) tostop.add(getController());
+            tostop.add(getCluster());
+
+            StartableMethods.stopSequentially(tostop);
+
+            clearLocations();
+
+            ServiceStateLogic.setExpectedState(this, Lifecycle.STOPPED);
+        } catch (Exception e) {
+            ServiceStateLogic.setExpectedState(this, Lifecycle.ON_FIRE);
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    @Override
+    public void restart() {
+        // TODO prod the entities themselves to restart, instead?
+        Collection<Location> locations = Lists.newArrayList(getLocations());
+
+        stop();
+        start(locations);
+    }
+    
+    void connectSensors() {
+        // FIXME no longer needed
+        addEnricher(Enrichers.builder()
+                .propagatingAllButUsualAnd(Attributes.MAIN_URI, ROOT_URL, GROUP_MEMBERS, GROUP_SIZE)
+                .from(getCluster())
+                .build());
+        addEnricher(Enrichers.builder()
+                // include hostname and address of controller (need both in case hostname only resolves to internal/private ip)
+                .propagating(LoadBalancer.HOSTNAME, Attributes.ADDRESS, Attributes.MAIN_URI, ROOT_URL)
+                .from(getController())
+                .build());
+    }
+
+    @Override
+    public Integer resize(Integer desiredSize) {
+        return getCluster().resize(desiredSize);
+    }
+
+    @Override
+    public String replaceMember(String memberId) {
+        return getCluster().replaceMember(memberId);
+    }
+
+    /**
+     * @return the current size of the group.
+     */
+    @Override
+    public Integer getCurrentSize() {
+        return getCluster().getCurrentSize();
+    }
+
+    @Override
+    public void deploy(String url, String targetName) {
+        DynamicWebAppClusterImpl.addToWarsByContext(this, url, targetName);
+        getCluster().deploy(url, targetName);
+    }
+
+    @Override
+    public void undeploy(String targetName) {
+        DynamicWebAppClusterImpl.removeFromWarsByContext(this, targetName);
+        getCluster().undeploy(targetName);
+    }
+
+    @Override
+    public void redeployAll() {
+        getCluster().redeployAll();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java
new file mode 100644
index 0000000..1c196c6
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppCluster.java
@@ -0,0 +1,69 @@
+/*
+ * 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 org.apache.brooklyn.catalog.Catalog;
+import brooklyn.config.render.RendererHints;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.util.time.Duration;
+
+/**
+ * DynamicWebAppClusters provide cluster-wide aggregates of entity attributes.  Currently totals and averages:
+ * <ul>
+ *   <li>Entity request counts</li>
+ *   <li>Entity error counts</li>
+ *   <li>Requests per second</li>
+ *   <li>Entity processing time</li>
+ * </ul>
+ */
+@Catalog(name="Dynamic Web-app Cluster", description="A cluster of web-apps, which can be dynamically re-sized; this does not include a load-balancer")
+@ImplementedBy(DynamicWebAppClusterImpl.class)
+public interface DynamicWebAppCluster extends DynamicCluster, WebAppService, JavaWebAppService,
+        JavaWebAppService.CanDeployAndUndeploy, JavaWebAppService.CanRedeployAll {
+
+    public static final AttributeSensor<Double> REQUEST_COUNT_PER_NODE = new BasicAttributeSensor<Double>(
+            Double.class, "webapp.reqs.total.perNode", "Cluster entity request average");
+
+    public static final AttributeSensor<Integer> ERROR_COUNT_PER_NODE = new BasicAttributeSensor<Integer>(
+            Integer.class, "webapp.reqs.errors.perNode", "Cluster entity request error average");
+
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_LAST_PER_NODE = new BasicAttributeSensor<Double>(
+            Double.class, "webapp.reqs.perSec.last.perNode", "Reqs/sec (last datapoint) averaged over all nodes");
+
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE = new BasicAttributeSensor<Double>(
+            Double.class, "webapp.reqs.perSec.windowed.perNode", "Reqs/sec (over time window) averaged over all nodes");
+
+    public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = ApplyDisplayHints.TOTAL_PROCESSING_TIME_PER_NODE;
+
+    public static final AttributeSensor<Double> PROCESSING_TIME_FRACTION_IN_WINDOW_PER_NODE = new BasicAttributeSensor<Double>(
+            Double.class, "webapp.reqs.processingTime.fraction.windowed.perNode", "Fraction of time spent processing " +
+            "reported by webserver (percentage, over time window) averaged over all nodes");
+
+    class ApplyDisplayHints {
+        public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = new BasicAttributeSensor<Integer>(
+            Integer.class, "webapp.reqs.processingTime.perNode", "Total processing time per node (millis)");
+        static {
+            RendererHints.register(TOTAL_PROCESSING_TIME_PER_NODE, RendererHints.displayValue(Duration.millisToStringRounded()));
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java
new file mode 100644
index 0000000..d1f1da7
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppClusterImpl.java
@@ -0,0 +1,263 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import org.apache.brooklyn.management.Task;
+import org.apache.brooklyn.management.TaskAdaptable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.effector.Effectors;
+import brooklyn.entity.group.DynamicCluster;
+import brooklyn.entity.group.DynamicClusterImpl;
+import brooklyn.event.AttributeSensor;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.MutableSet;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.TaskBuilder;
+import brooklyn.util.task.TaskTags;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+
+/**
+ * DynamicWebAppClusters provide cluster-wide aggregates of entity attributes.  Currently totals and averages:
+ * <ul>
+ *   <li>Entity request counts</li>
+ *   <li>Entity error counts</li>
+ *   <li>Requests per second</li>
+ *   <li>Entity processing time</li>
+ * </ul>
+ */
+public class DynamicWebAppClusterImpl extends DynamicClusterImpl implements DynamicWebAppCluster {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicWebAppClusterImpl.class);
+    private static final FilenameToWebContextMapper FILENAME_TO_WEB_CONTEXT_MAPPER = new FilenameToWebContextMapper();
+    
+    /**
+     * Instantiate a new DynamicWebAppCluster.  Parameters as per {@link DynamicCluster#DynamicCluster()}
+     */
+    public DynamicWebAppClusterImpl() {
+        super();
+    }
+    
+    @Override
+    public void init() {
+        super.init();
+        // Enricher attribute setup.  A way of automatically discovering these (but avoiding
+        // averaging things like HTTP port and response codes) would be neat.
+        List<? extends List<? extends AttributeSensor<? extends Number>>> summingEnricherSetup = ImmutableList.of(
+                ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT),
+                ImmutableList.of(ERROR_COUNT, ERROR_COUNT),
+                ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST),
+                ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW),
+                ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME),
+                ImmutableList.of(PROCESSING_TIME_FRACTION_IN_WINDOW, PROCESSING_TIME_FRACTION_IN_WINDOW)
+        );
+        
+        List<? extends List<? extends AttributeSensor<? extends Number>>> averagingEnricherSetup = ImmutableList.of(
+                ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT_PER_NODE),
+                ImmutableList.of(ERROR_COUNT, ERROR_COUNT_PER_NODE),
+                ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST_PER_NODE),
+                ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE),
+                ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME_PER_NODE),
+                ImmutableList.of(PROCESSING_TIME_FRACTION_IN_WINDOW, PROCESSING_TIME_FRACTION_IN_WINDOW_PER_NODE)
+        );
+        
+        for (List<? extends AttributeSensor<? extends Number>> es : summingEnricherSetup) {
+            AttributeSensor<? extends Number> t = es.get(0);
+            AttributeSensor<? extends Number> total = es.get(1);
+            addEnricher(Enrichers.builder()
+                    .aggregating(t)
+                    .publishing(total)
+                    .fromMembers()
+                    .computingSum()
+                    .build());
+        }
+        
+        for (List<? extends AttributeSensor<? extends Number>> es : averagingEnricherSetup) {
+            @SuppressWarnings("unchecked")
+            AttributeSensor<Number> t = (AttributeSensor<Number>) es.get(0);
+            @SuppressWarnings("unchecked")
+            AttributeSensor<Double> average = (AttributeSensor<Double>) es.get(1);
+            addEnricher(Enrichers.builder()
+                    .aggregating(t)
+                    .publishing(average)
+                    .fromMembers()
+                    .computingAverage()
+                    .defaultValueForUnreportedSensors(0)
+                    .build());
+        }
+    }
+    
+    // TODO this will probably be useful elsewhere ... but where to put it?
+    // TODO add support for this in DependentConfiguration (see TODO there)
+    /** Waits for the given target to report service up, then runs the given task
+     * (often an invocation on that entity), with the given name.
+     * If the target goes away, this task marks itself inessential
+     * before failing so as not to cause a parent task to fail. */
+    static <T> Task<T> whenServiceUp(final Entity target, final TaskAdaptable<T> task, String name) {
+        return Tasks.<T>builder().name(name).dynamic(true).body(new Callable<T>() {
+            @Override
+            public T call() {
+                try {
+                    while (true) {
+                        if (!Entities.isManaged(target)) {
+                            Tasks.markInessential();
+                            throw new IllegalStateException("Target "+target+" is no longer managed");
+                        }
+                        if (Boolean.TRUE.equals(target.getAttribute(Attributes.SERVICE_UP))) {
+                            Tasks.resetBlockingDetails();
+                            TaskTags.markInessential(task);
+                            DynamicTasks.queue(task);
+                            try {
+                                return task.asTask().getUnchecked();
+                            } catch (Exception e) {
+                                if (Entities.isManaged(target)) {
+                                    throw Exceptions.propagate(e);
+                                } else {
+                                    Tasks.markInessential();
+                                    throw new IllegalStateException("Target "+target+" is no longer managed", e);
+                                }
+                            }
+                        } else {
+                            Tasks.setBlockingDetails("Waiting on "+target+" to be ready");
+                        }
+                        // TODO replace with subscription?
+                        Time.sleep(Duration.ONE_SECOND);
+                    }
+                } finally {
+                    Tasks.resetBlockingDetails();
+                }
+            }
+        }).build();        
+    }
+
+    @Override
+    public void deploy(String url, String targetName) {
+        checkNotNull(url, "url");
+        checkNotNull(targetName, "targetName");
+        targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);
+
+        // set it up so future nodes get the right wars
+        addToWarsByContext(this, url, targetName);
+        
+        log.debug("Deploying "+targetName+"->"+url+" across cluster "+this+"; WARs now "+getConfig(WARS_BY_CONTEXT));
+
+        Iterable<CanDeployAndUndeploy> targets = Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
+        TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name("Deploy "+targetName+" to cluster (size "+Iterables.size(targets)+")");
+        for (Entity target: targets) {
+            tb.add(whenServiceUp(target, Effectors.invocation(target, DEPLOY, MutableMap.of("url", url, "targetName", targetName)),
+                "Deploy "+targetName+" to "+target+" when ready"));
+        }
+        DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();
+
+        // Update attribute
+        // TODO support for atomic sensor update (should be part of standard tooling; NB there is some work towards this, according to @aledsage)
+        Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS));
+        deployedWars.add(targetName);
+        setAttribute(DEPLOYED_WARS, deployedWars);
+    }
+    
+    @Override
+    public void undeploy(String targetName) {
+        checkNotNull(targetName, "targetName");
+        targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);
+        
+        // set it up so future nodes get the right wars
+        if (!removeFromWarsByContext(this, targetName)) {
+            DynamicTasks.submit(Tasks.warning("Context "+targetName+" not known at "+this+"; attempting to undeploy regardless", null), this);
+        }
+        
+        log.debug("Undeploying "+targetName+" across cluster "+this+"; WARs now "+getConfig(WARS_BY_CONTEXT));
+
+        Iterable<CanDeployAndUndeploy> targets = Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
+        TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name("Undeploy "+targetName+" across cluster (size "+Iterables.size(targets)+")");
+        for (Entity target: targets) {
+            tb.add(whenServiceUp(target, Effectors.invocation(target, UNDEPLOY, MutableMap.of("targetName", targetName)),
+                "Undeploy "+targetName+" at "+target+" when ready"));
+        }
+        DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();
+
+        // Update attribute
+        Set<String> deployedWars = MutableSet.copyOf(getAttribute(DEPLOYED_WARS));
+        deployedWars.remove( FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName) );
+        setAttribute(DEPLOYED_WARS, deployedWars);
+    }
+
+    static void addToWarsByContext(Entity entity, String url, String targetName) {
+        targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);
+        // TODO a better way to do atomic updates, see comment above
+        synchronized (entity) {
+            Map<String,String> newWarsMap = MutableMap.copyOf(entity.getConfig(WARS_BY_CONTEXT));
+            newWarsMap.put(targetName, url);
+            ((EntityInternal)entity).setConfig(WARS_BY_CONTEXT, newWarsMap);
+        }
+    }
+
+    static boolean removeFromWarsByContext(Entity entity, String targetName) {
+        targetName = FILENAME_TO_WEB_CONTEXT_MAPPER.convertDeploymentTargetNameToContext(targetName);
+        // TODO a better way to do atomic updates, see comment above
+        synchronized (entity) {
+            Map<String,String> newWarsMap = MutableMap.copyOf(entity.getConfig(WARS_BY_CONTEXT));
+            String url = newWarsMap.remove(targetName);
+            if (url==null) {
+                return false;
+            }
+            ((EntityInternal)entity).setConfig(WARS_BY_CONTEXT, newWarsMap);
+            return true;
+        }
+    }
+    
+    @Override
+    public void redeployAll() {
+        Map<String, String> wars = MutableMap.copyOf(getConfig(WARS_BY_CONTEXT));
+        String redeployPrefix = "Redeploy all WARs (count "+wars.size()+")";
+
+        log.debug("Redeplying all WARs across cluster "+this+": "+getConfig(WARS_BY_CONTEXT));
+        
+        Iterable<CanDeployAndUndeploy> targetEntities = Iterables.filter(getChildren(), CanDeployAndUndeploy.class);
+        TaskBuilder<Void> tb = Tasks.<Void>builder().parallel(true).name(redeployPrefix+" across cluster (size "+Iterables.size(targetEntities)+")");
+        for (Entity targetEntity: targetEntities) {
+            TaskBuilder<Void> redeployAllToTarget = Tasks.<Void>builder().name(redeployPrefix+" at "+targetEntity+" (after ready check)");
+            for (String warContextPath: wars.keySet()) {
+                redeployAllToTarget.add(Effectors.invocation(targetEntity, DEPLOY, MutableMap.of("url", wars.get(warContextPath), "targetName", warContextPath)));
+            }
+            tb.add(whenServiceUp(targetEntity, redeployAllToTarget.build(), redeployPrefix+" at "+targetEntity+" when ready"));
+        }
+        DynamicTasks.queueIfPossible(tb.build()).orSubmitAsync(this).asTask().getUnchecked();
+    }  
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java
new file mode 100644
index 0000000..2399b8c
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabric.java
@@ -0,0 +1,48 @@
+/*
+ * 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.entity.group.DynamicFabric;
+import brooklyn.entity.proxying.ImplementedBy;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+
+/**
+ * DynamicWebAppFabric provide a fabric of clusters, aggregating the entity attributes.  Currently totals and averages:
+ * <ul>
+ *   <li>Entity request counts</li>
+ *   <li>Entity error counts</li>
+ *   <li>Requests per second</li>
+ *   <li>Entity processing time</li>
+ * </ul>
+ */
+@ImplementedBy(DynamicWebAppFabricImpl.class)
+public interface DynamicWebAppFabric extends DynamicFabric, WebAppService {
+
+    public static final AttributeSensor<Double> REQUEST_COUNT_PER_NODE = new BasicAttributeSensor<Double>(
+            Double.class, "webapp.reqs.total.perNode", "Fabric entity request average");
+
+    public static final AttributeSensor<Integer> ERROR_COUNT_PER_NODE = new BasicAttributeSensor<Integer>(
+            Integer.class, "webapp.reqs.errors.perNode", "Fabric entity request error average");
+
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_LAST_PER_NODE = DynamicWebAppCluster.REQUESTS_PER_SECOND_LAST_PER_NODE;
+    public static final AttributeSensor<Double> REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE = DynamicWebAppCluster.REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE;
+    public static final AttributeSensor<Integer> TOTAL_PROCESSING_TIME_PER_NODE = DynamicWebAppCluster.TOTAL_PROCESSING_TIME_PER_NODE;
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java
new file mode 100644
index 0000000..f6d8e17
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/DynamicWebAppFabricImpl.java
@@ -0,0 +1,83 @@
+/*
+ * 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 java.util.List;
+
+import javax.annotation.Nullable;
+
+import brooklyn.enricher.Enrichers;
+import brooklyn.entity.group.DynamicFabricImpl;
+import brooklyn.event.AttributeSensor;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+
+public class DynamicWebAppFabricImpl extends DynamicFabricImpl implements DynamicWebAppFabric {
+
+    @Override
+    public void onManagementBecomingMaster() {
+        // Enricher attribute setup.  A way of automatically discovering these (but avoiding
+        // averaging things like HTTP port and response codes) would be neat.
+        List<? extends List<? extends AttributeSensor<? extends Number>>> summingEnricherSetup = ImmutableList.of(
+                ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT),
+                ImmutableList.of(ERROR_COUNT, ERROR_COUNT),
+                ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST),
+                ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW),
+                ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME)
+        );
+        
+        List<? extends List<? extends AttributeSensor<? extends Number>>> averagingEnricherSetup = ImmutableList.of(
+                ImmutableList.of(REQUEST_COUNT, REQUEST_COUNT_PER_NODE),
+                ImmutableList.of(ERROR_COUNT, ERROR_COUNT_PER_NODE),
+                ImmutableList.of(REQUESTS_PER_SECOND_LAST, REQUESTS_PER_SECOND_LAST_PER_NODE),
+                ImmutableList.of(REQUESTS_PER_SECOND_IN_WINDOW, REQUESTS_PER_SECOND_IN_WINDOW_PER_NODE),
+                ImmutableList.of(TOTAL_PROCESSING_TIME, TOTAL_PROCESSING_TIME_PER_NODE)
+        );
+        
+        for (List<? extends AttributeSensor<? extends Number>> es : summingEnricherSetup) {
+            AttributeSensor<? extends Number> t = es.get(0);
+            AttributeSensor<? extends Number> total = es.get(1);
+            addEnricher(Enrichers.builder()
+                    .aggregating(t)
+                    .publishing(total)
+                    .fromMembers()
+                    .computingSum()
+                    .build());
+        }
+        
+        for (List<? extends AttributeSensor<? extends Number>> es : averagingEnricherSetup) {
+            @SuppressWarnings("unchecked")
+            AttributeSensor<Number> t = (AttributeSensor<Number>) es.get(0);
+            @SuppressWarnings("unchecked")
+            AttributeSensor<Double> average = (AttributeSensor<Double>) es.get(1);
+            
+            // TODO This needs to respond to changes in FABRIC_SIZE as well, to recalculate
+            addEnricher(Enrichers.builder()
+                    .transforming(t)
+                    .publishing(average)
+                    .computing(new Function<Number, Double>() {
+                            @Override public Double apply(@Nullable Number input) {
+                                Integer size = getAttribute(DynamicWebAppFabric.FABRIC_SIZE);
+                                return (size != null && input != null) ? (input.doubleValue() / size) : null;
+                            }})
+                    .build());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java
new file mode 100644
index 0000000..2a2e00b
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/ElasticJavaWebAppService.java
@@ -0,0 +1,60 @@
+/*
+ * 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 java.util.Map;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.AbstractConfigurableEntityFactory;
+import brooklyn.entity.basic.ConfigurableEntityFactory;
+import brooklyn.entity.basic.EntityFactoryForLocation;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Startable;
+import brooklyn.location.Location;
+import brooklyn.location.MachineProvisioningLocation;
+
+public interface ElasticJavaWebAppService extends JavaWebAppService, Startable {
+
+    public interface ElasticJavaWebAppServiceAwareLocation {
+        ConfigurableEntityFactory<ElasticJavaWebAppService> newWebClusterFactory();
+    }
+
+    /** @deprecated since 0.7.0 use {@link EntitySpec} */
+    @Deprecated
+    public static class Factory extends AbstractConfigurableEntityFactory<ElasticJavaWebAppService>
+    implements EntityFactoryForLocation<ElasticJavaWebAppService> {
+
+        private static final long serialVersionUID = 6654647949712073832L;
+
+        public ElasticJavaWebAppService newEntity2(@SuppressWarnings("rawtypes") Map flags, Entity parent) {
+            return new ControlledDynamicWebAppClusterImpl(flags, parent);
+        }
+
+        public ConfigurableEntityFactory<ElasticJavaWebAppService> newFactoryForLocation(Location l) {
+            if (l instanceof ElasticJavaWebAppServiceAwareLocation) {
+                return ((ElasticJavaWebAppServiceAwareLocation)l).newWebClusterFactory().configure(config);
+            }
+            //optional, fail fast if location not supported
+            if (!(l instanceof MachineProvisioningLocation))
+                throw new UnsupportedOperationException("cannot create this entity in location "+l);
+            return this;
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.java
new file mode 100644
index 0000000..7da2975
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/FilenameToWebContextMapper.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;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** utilities for translating consistently between a filename (http://acme.org/foo.war) and a web context path (/foo) */ 
+public class FilenameToWebContextMapper {
+
+    public static final Logger log = LoggerFactory.getLogger(FilenameToWebContextMapper.class);
+    
+    public String findArchiveNameFromUrl(String url, boolean verbose) {
+        String name = url.substring(url.lastIndexOf('/') + 1);
+        if (name.indexOf("?")>0) {
+            Pattern p = Pattern.compile("[A-Za-z0-9_\\-]+\\..(ar|AR)($|(?=[^A-Za-z0-9_\\-]))");
+            Matcher wars = p.matcher(name);
+            if (wars.find()) {
+                // take first such string
+                name = wars.group();
+                if (wars.find()) {
+                    if (verbose) log.warn("Not clear which archive to deploy for "+url+": using "+name);
+                } else {
+                    if (verbose) log.info("Inferred archive to deploy for "+url+": using "+name);
+                }
+            } else {
+                if (verbose) log.warn("Not clear which archive to deploy for "+url+": using "+name);
+            }
+        }
+        return name;
+    }
+
+    public String convertDeploymentTargetNameToFilename(String targetName) {
+        String result = targetName;
+        if (result.isEmpty()) return "";
+        if (targetName.startsWith("/")) {
+            // treat input as a context
+            result = result.substring(1);
+            if (result.length()==0) result="ROOT";
+            result += ".war";
+        } else {
+            // treat input as a file, unless it has no dots in it
+            if (result.indexOf('.')==-1) result += ".war";
+        }
+        return result;
+    }
+    
+    public String convertDeploymentTargetNameToContext(String targetName) {
+        String result = targetName;
+        if (result.isEmpty()) return "";
+        if (targetName.startsWith("/")) {
+            // treat input as a context - noop
+        } else {
+            // make it look like a context
+            result = "/"+result;
+            if (result.indexOf('.')==-1) {
+                // no dot means no more processing
+            } else {
+                // look at extension
+                String extension = result.substring(result.lastIndexOf('.')+1).toUpperCase();
+                if (extension.matches(".AR")) {
+                    // looks like it was a WAR/EAR/etc
+                    result = result.substring(0, result.length()-4);
+                    if (result.equalsIgnoreCase("/ROOT")) result = "/"; 
+                } else {
+                    // input didn't look like a war filename, no more processing
+                }
+            }
+        }
+        return result;        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java
new file mode 100644
index 0000000..a750411
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/HttpsSslConfig.java
@@ -0,0 +1,74 @@
+/*
+ * 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 java.util.Map;
+
+import brooklyn.util.guava.Maybe;
+
+public class HttpsSslConfig {
+
+    private String keystoreUrl;
+    private String keystorePassword;
+    private String keyAlias;
+    
+    public HttpsSslConfig() {
+    }
+    
+    public HttpsSslConfig keystoreUrl(String val) {
+        keystoreUrl = val; return this;
+    }
+    
+    public HttpsSslConfig keystorePassword(String val) {
+        keystorePassword = val; return this;
+    }
+    
+    public HttpsSslConfig keyAlias(String val) {
+        keyAlias = val; return this;
+    }
+    
+    public String getKeystoreUrl() {
+        return keystoreUrl;
+    }
+    
+    public String getKeystorePassword() {
+        return keystorePassword;
+    }
+    
+    public String getKeyAlias() {
+        return keyAlias;
+    }
+
+    // method naming convention allows it to be used by TypeCoercions
+    public static HttpsSslConfig fromMap(Map<String,String> map) {
+        HttpsSslConfig result = new HttpsSslConfig();
+        result.keystoreUrl = first(map, "keystoreUrl", "url").orNull();
+        result.keystorePassword = first(map, "keystorePassword", "password").orNull();
+        result.keyAlias = first(map, "keyAlias", "alias", "key").orNull();
+        return result;
+    }
+
+    private static Maybe<String> first(Map<String,String> map, String ...keys) {
+        for (String key: keys) {
+            if (map.containsKey(key))
+                return Maybe.of(map.get(key));
+        }
+        return Maybe.<String>absent();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java
new file mode 100644
index 0000000..822519b
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppDriver.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.io.File;
+import java.util.Set;
+
+import brooklyn.entity.java.JavaSoftwareProcessDriver;
+
+public interface JavaWebAppDriver extends JavaSoftwareProcessDriver {
+
+    Set<String> getEnabledProtocols();
+
+    Integer getHttpPort();
+
+    Integer getHttpsPort();
+
+    HttpsSslConfig getHttpsSslConfig();
+    
+    void deploy(File file);
+
+    void deploy(File f, String targetName);
+
+    /**
+     * Deploys a URL as a webapp at the appserver.
+     * <p>
+     * See {@link JavaWebAppSoftwareProcess#deploy(String, String)} for details of how input filenames are handled.
+     *
+     * @return A token which can be used as an argument to undeploy.
+     *     Typically the web context with leading slash where the app can be reached (just "/" for ROOT)
+     */
+    String deploy(String url, String targetName);
+    
+    void undeploy(String targetName);
+    
+    FilenameToWebContextMapper getFilenameContextMapper();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java
new file mode 100644
index 0000000..8b17bc3
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppService.java
@@ -0,0 +1,109 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.entity.Entity;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.annotation.EffectorParam;
+import brooklyn.entity.basic.MethodEffector;
+import brooklyn.entity.java.UsesJava;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensor;
+import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.util.flags.SetFromFlag;
+
+public interface JavaWebAppService extends WebAppService, UsesJava {
+
+    @SetFromFlag("war")
+    public static final ConfigKey<String> ROOT_WAR = new BasicConfigKey<String>(
+            String.class, "wars.root", "WAR file to deploy as the ROOT, as URL (supporting file: and classpath: prefixes)");
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @SetFromFlag("wars")
+    public static final ConfigKey<List<String>> NAMED_WARS = new BasicConfigKey(
+            List.class, "wars.named", "Archive files to deploy, as URL strings (supporting file: and classpath: prefixes); context (path in user-facing URL) will be inferred by name");
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @SetFromFlag("warsByContext")
+    public static final ConfigKey<Map<String,String>> WARS_BY_CONTEXT = new BasicConfigKey(
+            Map.class, "wars.by.context", "Map of context keys (path in user-facing URL, typically without slashes) to archives (e.g. WARs by URL) to deploy, supporting file: and classpath: prefixes)");
+    
+    /** Optional marker interface for entities which support 'deploy' and 'undeploy' */
+    public interface CanDeployAndUndeploy extends Entity {
+
+        @SuppressWarnings({ "unchecked", "rawtypes" })
+        public static final AttributeSensor<Set<String>> DEPLOYED_WARS = new BasicAttributeSensor(
+                Set.class, "webapp.deployedWars", "Names of archives/contexts that are currently deployed");
+
+        public static final MethodEffector<Void> DEPLOY = new MethodEffector<Void>(CanDeployAndUndeploy.class, "deploy");
+        public static final MethodEffector<Void> UNDEPLOY = new MethodEffector<Void>(CanDeployAndUndeploy.class, "undeploy");
+
+        /**
+         * Deploys the given artifact, from a source URL, to a given deployment filename/context.
+         * There is some variance in expected filename/context at various servers,
+         * so the following conventions are followed:
+         * <p>
+         *   either ROOT.WAR or /       denotes root context
+         * <p>
+         *   anything of form  FOO.?AR  (ending .?AR) is copied with that name (unless copying not necessary)
+         *                              and is expected to be served from /FOO
+         * <p>
+         *   anything of form  /FOO     (with leading slash) is expected to be served from /FOO
+         *                              (and is copied as FOO.WAR)
+         * <p>
+         *   anything of form  FOO      (without a dot) is expected to be served from /FOO
+         *                              (and is copied as FOO.WAR)
+         * <p>
+         *   otherwise <i>please note</i> behaviour may vary on different appservers;
+         *   e.g. FOO.FOO would probably be ignored on appservers which expect a file copied across (usually),
+         *   but served as /FOO.FOO on systems that take a deployment context.
+         * <p>
+         * See {@link FileNameToContextMappingTest} for definitive examples!
+         *
+         * @param url  where to get the war, as a URL, either classpath://xxx or file:///home/xxx or http(s)...
+         * @param targetName  where to tell the server to serve the WAR, see above
+         */
+        @Effector(description="Deploys the given artifact, from a source URL, to a given deployment filename/context")
+        public void deploy(
+                @EffectorParam(name="url", description="URL of WAR file") String url, 
+                @EffectorParam(name="targetName", description="context path where WAR should be deployed (/ for ROOT)") String targetName);
+
+        /** 
+         * For the DEPLOYED_WARS to be updated, the input must match the result of the call to deploy,
+         * e.g. the transformed name using 
+         */
+        @Effector(description="Undeploys the given context/artifact")
+        public void undeploy(
+                @EffectorParam(name="targetName") String targetName);
+    }
+
+    /** Optional marker interface for entities which support 'redeployAll' */
+    public interface CanRedeployAll {
+        public static final MethodEffector<Void> REDEPLOY_ALL = new MethodEffector<Void>(CanRedeployAll.class, "redeployAll");
+        
+        @Effector(description="Redeploys all web apps known here across the cluster (e.g. if it gets into an inconsistent state)")
+        public void redeployAll();
+    }
+        
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java
new file mode 100644
index 0000000..b0563fd
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcess.java
@@ -0,0 +1,34 @@
+/*
+ * 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.entity.basic.SoftwareProcess;
+
+public interface JavaWebAppSoftwareProcess extends SoftwareProcess, JavaWebAppService, JavaWebAppService.CanDeployAndUndeploy {
+    
+    // exist on the interface for freemarker to pick it up
+    
+    public boolean isHttpEnabled();
+    public boolean isHttpsEnabled();
+    public Integer getHttpPort();
+    public Integer getHttpsPort();
+    public String getHttpsSslKeyAlias();
+    public String getHttpsSslKeystorePassword();
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java
new file mode 100644
index 0000000..1eb3660
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSoftwareProcessImpl.java
@@ -0,0 +1,206 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.annotation.Effector;
+import brooklyn.entity.annotation.EffectorParam;
+import brooklyn.entity.basic.SoftwareProcessImpl;
+import brooklyn.entity.java.JavaAppUtils;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Sets;
+
+public abstract class JavaWebAppSoftwareProcessImpl extends SoftwareProcessImpl implements JavaWebAppService, JavaWebAppSoftwareProcess {
+
+    private static final Logger LOG = LoggerFactory.getLogger(JavaWebAppSoftwareProcessImpl.class);
+
+    public JavaWebAppSoftwareProcessImpl(){
+        super();
+    }
+
+    @SuppressWarnings("rawtypes")
+    public JavaWebAppSoftwareProcessImpl(Entity parent){
+        this(new LinkedHashMap(),parent);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public JavaWebAppSoftwareProcessImpl(Map flags){
+        this(flags, null);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public JavaWebAppSoftwareProcessImpl(Map flags, Entity parent) {
+        super(flags, parent);
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        
+        WebAppServiceMethods.connectWebAppServerPolicies(this);
+        JavaAppUtils.connectJavaAppServerPolicies(this);
+    }
+    
+    //just provide better typing
+    public JavaWebAppDriver getDriver() {
+        return (JavaWebAppDriver) super.getDriver();
+    }
+
+    // TODO thread-safety issues: if multiple concurrent calls, may break (e.g. deployment_wars being reset)
+    public void deployInitialWars() {
+        if (getAttribute(DEPLOYED_WARS) == null) setAttribute(DEPLOYED_WARS, Sets.<String>newLinkedHashSet());
+        
+        String rootWar = getConfig(ROOT_WAR);
+        if (rootWar!=null) deploy(rootWar, "ROOT.war");
+
+        List<String> namedWars = getConfig(NAMED_WARS, Collections.<String>emptyList());
+        for(String war: namedWars){
+            deploy(war, getDriver().getFilenameContextMapper().findArchiveNameFromUrl(war, true));
+        }
+        
+        Map<String,String> warsByContext = getConfig(WARS_BY_CONTEXT);
+        if (warsByContext!=null) {
+            for (String context: warsByContext.keySet()) {
+                deploy(warsByContext.get(context), context);
+            }
+        }
+    }
+
+    /**
+     * Deploys the given artifact, from a source URL, to a given deployment filename/context.
+     * There is some variance in expected filename/context at various servers,
+     * so the following conventions are followed:
+     * <p>
+     *   either ROOT.WAR or /       denotes root context
+     * <p>
+     *   anything of form  FOO.?AR  (ending .?AR) is copied with that name (unless copying not necessary)
+     *                              and is expected to be served from /FOO
+     * <p>
+     *   anything of form  /FOO     (with leading slash) is expected to be served from /FOO
+     *                              (and is copied as FOO.WAR)
+     * <p>
+     *   anything of form  FOO      (without a dot) is expected to be served from /FOO
+     *                              (and is copied as FOO.WAR)
+     * <p>                            
+     *   otherwise <i>please note</i> behaviour may vary on different appservers;
+     *   e.g. FOO.FOO would probably be ignored on appservers which expect a file copied across (usually),
+     *   but served as /FOO.FOO on systems that take a deployment context.
+     * <p>
+     * See {@link FileNameToContextMappingTest} for definitive examples!
+     * 
+     * @param url  where to get the war, as a URL, either classpath://xxx or file:///home/xxx or http(s)...
+     * @param targetName  where to tell the server to serve the WAR, see above
+     */
+    @Effector(description="Deploys the given artifact, from a source URL, to a given deployment filename/context")
+    public void deploy(
+            @EffectorParam(name="url", description="URL of WAR file") String url, 
+            @EffectorParam(name="targetName", description="context path where WAR should be deployed (/ for ROOT)") String targetName) {
+        try {
+            checkNotNull(url, "url");
+            checkNotNull(targetName, "targetName");
+            JavaWebAppDriver driver = getDriver();
+            String deployedName = driver.deploy(url, targetName);
+            
+            // Update attribute
+            Set<String> deployedWars = getAttribute(DEPLOYED_WARS);
+            if (deployedWars == null) {
+                deployedWars = Sets.newLinkedHashSet();
+            }
+            deployedWars.add(deployedName);
+            setAttribute(DEPLOYED_WARS, deployedWars);
+        } catch (RuntimeException e) {
+            // Log and propagate, so that log says which entity had problems...
+            LOG.warn("Error deploying '"+url+"' to "+targetName+" on "+toString()+"; rethrowing...", e);
+            throw Throwables.propagate(e);
+        }
+    }
+
+    /** For the DEPLOYED_WARS to be updated, the input must match the result of the call to deploy */
+    @Override
+    @Effector(description="Undeploys the given context/artifact")
+    public void undeploy(
+            @EffectorParam(name="targetName") String targetName) {
+        try {
+            JavaWebAppDriver driver = getDriver();
+            driver.undeploy(targetName);
+            
+            // Update attribute
+            Set<String> deployedWars = getAttribute(DEPLOYED_WARS);
+            if (deployedWars == null) {
+                deployedWars = Sets.newLinkedHashSet();
+            }
+            deployedWars.remove( driver.getFilenameContextMapper().convertDeploymentTargetNameToContext(targetName) );
+            setAttribute(DEPLOYED_WARS, deployedWars);
+        } catch (RuntimeException e) {
+            // Log and propagate, so that log says which entity had problems...
+            LOG.warn("Error undeploying '"+targetName+"' on "+toString()+"; rethrowing...", e);
+            throw Throwables.propagate(e);
+        }
+    }
+    
+    @Override
+    protected void postStop() {
+        super.postStop();
+        // zero our workrate derived workrates.
+        // TODO might not be enough, as policy may still be executing and have a record of historic vals; should remove policies
+        // (also not sure we want this; implies more generally a responsibility for sensors to announce things when disconnected,
+        // vs them just showing the last known value...)
+        setAttribute(REQUESTS_PER_SECOND_LAST, 0D);
+        setAttribute(REQUESTS_PER_SECOND_IN_WINDOW, 0D);
+    }
+
+    public boolean isHttpEnabled() {
+        return WebAppServiceMethods.isProtocolEnabled(this, "HTTP");
+    }
+
+    public boolean isHttpsEnabled() {
+        return WebAppServiceMethods.isProtocolEnabled(this, "HTTPS");
+    }
+
+    public Integer getHttpPort() {
+        return getAttribute(HTTP_PORT);
+    }
+
+    public Integer getHttpsPort() {
+        return getAttribute(HTTPS_PORT);
+    }
+
+    public String getHttpsSslKeyAlias() {
+        HttpsSslConfig config = getAttribute(HTTPS_SSL_CONFIG);
+        return (config == null) ? null : config.getKeyAlias();
+    }
+
+    public String getHttpsSslKeystorePassword() {
+        HttpsSslConfig config = getAttribute(HTTPS_SSL_CONFIG);
+        return (config == null) ? "" : config.getKeystorePassword();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java
new file mode 100644
index 0000000..0f2df03
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/JavaWebAppSshDriver.java
@@ -0,0 +1,201 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Set;
+
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.java.JavaSoftwareProcessSshDriver;
+import brooklyn.location.basic.SshMachineLocation;
+import brooklyn.util.task.DynamicTasks;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.task.ssh.SshTasks;
+import brooklyn.util.text.Strings;
+
+import com.google.common.collect.ImmutableList;
+
+public abstract class JavaWebAppSshDriver extends JavaSoftwareProcessSshDriver implements JavaWebAppDriver {
+
+    public JavaWebAppSshDriver(JavaWebAppSoftwareProcessImpl entity, SshMachineLocation machine) {
+        super(entity, machine);
+    }
+
+    public JavaWebAppSoftwareProcessImpl getEntity() {
+        return (JavaWebAppSoftwareProcessImpl) super.getEntity();
+    }
+
+    protected boolean isProtocolEnabled(String protocol) {
+        Set<String> protocols = getEnabledProtocols();
+        for (String contender : protocols) {
+            if (protocol.equalsIgnoreCase(contender)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public Set<String> getEnabledProtocols() {
+        return entity.getAttribute(JavaWebAppSoftwareProcess.ENABLED_PROTOCOLS);
+    }
+    
+    @Override
+    public Integer getHttpPort() {
+        return entity.getAttribute(Attributes.HTTP_PORT);
+    }
+
+    @Override
+    public Integer getHttpsPort() {
+        return entity.getAttribute(Attributes.HTTPS_PORT);
+    }
+
+    @Override
+    public HttpsSslConfig getHttpsSslConfig() {
+        return entity.getAttribute(WebAppServiceConstants.HTTPS_SSL_CONFIG);
+    }
+
+    protected String getSslKeystoreUrl() {
+        HttpsSslConfig ssl = getHttpsSslConfig();
+        return (ssl == null) ? null : ssl.getKeystoreUrl();
+    }
+    
+    protected String getSslKeystorePassword() {
+        HttpsSslConfig ssl = getHttpsSslConfig();
+        return (ssl == null) ? null : ssl.getKeystorePassword();
+    }
+    
+    protected String getSslKeyAlias() {
+        HttpsSslConfig ssl = getHttpsSslConfig();
+        return (ssl == null) ? null : ssl.getKeyAlias();
+    }
+
+    protected String inferRootUrl() {
+        if (isProtocolEnabled("https")) {
+            Integer port = getHttpsPort();
+            checkNotNull(port, "HTTPS_PORT sensors not set; is an acceptable port available?");
+            return String.format("https://%s:%s/", getSubnetHostname(), port);
+        } else if (isProtocolEnabled("http")) {
+            Integer port = getHttpPort();
+            checkNotNull(port, "HTTP_PORT sensors not set; is an acceptable port available?");
+            return String.format("http://%s:%s/", getSubnetHostname(), port);
+        } else {
+            throw new IllegalStateException("HTTP and HTTPS protocols not enabled for "+entity+"; enabled protocols are "+getEnabledProtocols());
+        }
+    }
+    
+    @Override
+    public void postLaunch() {
+        String rootUrl = inferRootUrl();
+        entity.setAttribute(Attributes.MAIN_URI, URI.create(rootUrl));
+        entity.setAttribute(WebAppService.ROOT_URL, rootUrl);
+    }
+
+    /** 
+     * if files should be placed on the server for deployment,
+     * override this to be the sub-directory of the runDir where they should be stored
+     * (or override getDeployDir() if they should be copied somewhere else,
+     * and set this null);
+     * if files are not copied to the server, but injected (e.g. JMX or uploaded)
+     * then override {@link #deploy(String, String)} as appropriate,
+     * using getContextFromDeploymentTargetName(targetName)
+     * and override this to return null
+     */
+    protected abstract String getDeploySubdir();
+    
+    protected String getDeployDir() {
+        if (getDeploySubdir()==null)
+            throw new IllegalStateException("no deployment directory available for "+this);
+        return getRunDir() + "/" + getDeploySubdir();
+    }
+
+    @Override
+    public void deploy(File file) {
+        deploy(file, null);
+    }
+
+    @Override
+    public void deploy(File f, String targetName) {
+        if (targetName == null) {
+            targetName = f.getName();
+        }
+        deploy(f.toURI().toASCIIString(), targetName);
+    }
+
+    /**
+     * Deploys a URL as a webapp at the appserver.
+     *
+     * Returns a token which can be used as an argument to undeploy,
+     * typically the web context with leading slash where the app can be reached (just "/" for ROOT)
+     *
+     * @see JavaWebAppSoftwareProcess#deploy(String, String) for details of how input filenames are handled
+     */
+    @Override
+    public String deploy(final String url, final String targetName) {
+        final String canonicalTargetName = getFilenameContextMapper().convertDeploymentTargetNameToFilename(targetName);
+        final String dest = getDeployDir() + "/" + canonicalTargetName;
+        //write to a .tmp so autodeploy is not triggered during upload
+        final String tmpDest = dest + "." + Strings.makeRandomId(8) + ".tmp";
+        final String msg = String.format("deploying %s to %s:%s", new Object[]{url, getHostname(), dest});
+        log.info(entity + " " + msg);
+        Tasks.setBlockingDetails(msg);
+        try {
+            final String copyTaskMsg = String.format("copying %s to %s:%s", new Object[]{url, getHostname(), tmpDest});
+            DynamicTasks.queue(copyTaskMsg, new Runnable() {
+                @Override
+                public void run() {
+                    int result = copyResource(url, tmpDest);
+                    if (result != 0) {
+                        throw new IllegalStateException("Invalud result " + result + " while " + copyTaskMsg);
+                    }
+                }
+            });
+
+            // create a backup
+            DynamicTasks.queue(SshTasks.newSshExecTaskFactory(getMachine(), String.format("mv -f %s %s.bak", dest, dest))
+                    .allowingNonZeroExitCode());
+
+            //rename temporary upload file to .war to be picked up for deployment
+            DynamicTasks.queue(SshTasks.newSshExecTaskFactory(getMachine(), String.format("mv -f %s %s", tmpDest, dest))
+                    .requiringExitCodeZero());
+            log.debug("{} deployed {} to {}:{}", new Object[]{entity, url, getHostname(), dest});
+
+            DynamicTasks.waitForLast();
+        } finally {
+            Tasks.resetBlockingDetails();
+        }
+        return getFilenameContextMapper().convertDeploymentTargetNameToContext(canonicalTargetName);
+    }
+    
+    @Override
+    public void undeploy(String targetName) {
+        String dest = getDeployDir() + "/" + getFilenameContextMapper().convertDeploymentTargetNameToFilename(targetName);
+        log.info("{} undeploying {}:{}", new Object[]{entity, getHostname(), dest});
+        int result = getMachine().execCommands("removing war on undeploy", ImmutableList.of(String.format("rm -f %s", dest)));
+        log.debug("{} undeployed {}:{}: result {}", new Object[]{entity, getHostname(), dest, result});
+    }
+    
+    @Override
+    public FilenameToWebContextMapper getFilenameContextMapper() {
+        return new FilenameToWebContextMapper();
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.java
new file mode 100644
index 0000000..f131932
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppService.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;
+
+import brooklyn.entity.Entity;
+
+public interface WebAppService extends WebAppServiceConstants, Entity {
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.java
new file mode 100644
index 0000000..4c5713c
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceConstants.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;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+
+import brooklyn.config.render.RendererHints;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.event.AttributeSensor;
+import brooklyn.event.basic.BasicAttributeSensorAndConfigKey;
+import brooklyn.event.basic.PortAttributeSensorAndConfigKey;
+import brooklyn.event.basic.Sensors;
+import brooklyn.util.flags.SetFromFlag;
+
+public interface WebAppServiceConstants extends WebAppServiceMetrics {
+
+    @SetFromFlag("httpPort")
+    public static final PortAttributeSensorAndConfigKey HTTP_PORT = Attributes.HTTP_PORT;
+
+    @SetFromFlag("httpsPort")
+    public static final PortAttributeSensorAndConfigKey HTTPS_PORT = Attributes.HTTPS_PORT;
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @SetFromFlag("enabledProtocols")
+    public static final BasicAttributeSensorAndConfigKey<Set<String>> ENABLED_PROTOCOLS = new BasicAttributeSensorAndConfigKey(
+            Set.class, "webapp.enabledProtocols", "List of enabled protocols (e.g. http, https)", ImmutableSet.of("http"));
+
+    @SetFromFlag("httpsSsl")
+    public static final BasicAttributeSensorAndConfigKey<HttpsSslConfig> HTTPS_SSL_CONFIG = new BasicAttributeSensorAndConfigKey<HttpsSslConfig>(
+            HttpsSslConfig.class, "webapp.https.ssl", "SSL Configuration for HTTPS", null);
+    
+    public static final AttributeSensor<String> ROOT_URL = RootUrl.ROOT_URL;
+
+}
+
+// this class is added because the ROOT_URL relies on a static initialization which unfortunately can't be added to an interface.
+class RootUrl {
+    public static final AttributeSensor<String> ROOT_URL = Sensors.newStringSensor("webapp.url", "URL");
+
+    static {
+        RendererHints.register(ROOT_URL, RendererHints.namedActionWithUrl());
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/77dff880/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
new file mode 100644
index 0000000..e5e0570
--- /dev/null
+++ b/software/webapp/src/main/java/org/apache/brooklyn/entity/webapp/WebAppServiceMethods.java
@@ -0,0 +1,89 @@
+/*
+ * 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 static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import brooklyn.enricher.RollingTimeWindowMeanEnricher;
+import brooklyn.enricher.TimeFractionDeltaEnricher;
+import brooklyn.enricher.TimeWeightedDeltaEnricher;
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.EntityLocal;
+import brooklyn.location.access.BrooklynAccessUtils;
+import brooklyn.util.time.Duration;
+
+import com.google.common.net.HostAndPort;
+
+public class WebAppServiceMethods implements WebAppServiceConstants {
+
+    public static final Duration DEFAULT_WINDOW_DURATION = Duration.TEN_SECONDS;
+
+    public static void connectWebAppServerPolicies(EntityLocal entity) {
+        connectWebAppServerPolicies(entity, DEFAULT_WINDOW_DURATION);
+    }
+
+    public static void connectWebAppServerPolicies(EntityLocal entity, Duration windowPeriod) {
+        entity.addEnricher(TimeWeightedDeltaEnricher.<Integer>getPerSecondDeltaEnricher(entity, REQUEST_COUNT, REQUESTS_PER_SECOND_LAST));
+
+        if (windowPeriod!=null) {
+            entity.addEnricher(new RollingTimeWindowMeanEnricher<Double>(entity, REQUESTS_PER_SECOND_LAST,
+                    REQUESTS_PER_SECOND_IN_WINDOW, windowPeriod));
+        }
+
+        entity.addEnricher(new TimeFractionDeltaEnricher<Integer>(entity, TOTAL_PROCESSING_TIME, PROCESSING_TIME_FRACTION_LAST, TimeUnit.MILLISECONDS));
+
+        if (windowPeriod!=null) {
+            entity.addEnricher(new RollingTimeWindowMeanEnricher<Double>(entity, PROCESSING_TIME_FRACTION_LAST,
+                    PROCESSING_TIME_FRACTION_IN_WINDOW, windowPeriod));
+        }
+
+    }
+
+    public static Set<String> getEnabledProtocols(Entity entity) {
+        return entity.getAttribute(WebAppService.ENABLED_PROTOCOLS);
+    }
+
+    public static boolean isProtocolEnabled(Entity entity, String protocol) {
+        for (String contender : getEnabledProtocols(entity)) {
+            if (protocol.equalsIgnoreCase(contender)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static String inferBrooklynAccessibleRootUrl(Entity entity) {
+        if (isProtocolEnabled(entity, "https")) {
+            Integer rawPort = entity.getAttribute(HTTPS_PORT);
+            checkNotNull(rawPort, "HTTPS_PORT sensors not set for %s; is an acceptable port available?", entity);
+            HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, rawPort);
+            return String.format("https://%s:%s/", hp.getHostText(), hp.getPort());
+        } else if (isProtocolEnabled(entity, "http")) {
+            Integer rawPort = entity.getAttribute(HTTP_PORT);
+            checkNotNull(rawPort, "HTTP_PORT sensors not set for %s; is an acceptable port available?", entity);
+            HostAndPort hp = BrooklynAccessUtils.getBrooklynAccessibleAddress(entity, rawPort);
+            return String.format("http://%s:%s/", hp.getHostText(), hp.getPort());
+        } else {
+            throw new IllegalStateException("HTTP and HTTPS protocols not enabled for "+entity+"; enabled protocols are "+getEnabledProtocols(entity));
+        }
+    }
+}









Mime
View raw message