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:37 UTC
[19/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/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));
+ }
+ }
+}