diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlRebindTest.java index 9322866944..0d6d5c259b 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlRebindTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/AbstractYamlRebindTest.java @@ -29,11 +29,7 @@ import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.Task; -import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatform; -import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer; import org.apache.brooklyn.camp.brooklyn.spi.creation.CampTypePlanTransformer; -import org.apache.brooklyn.camp.spi.Assembly; -import org.apache.brooklyn.camp.spi.AssemblyTemplate; import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.StartableApplication; @@ -45,7 +41,6 @@ import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; -import org.apache.brooklyn.util.core.config.ConfigBag; import org.apache.brooklyn.util.stream.Streams; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,19 +48,16 @@ import org.testng.annotations.BeforeMethod; import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; public class AbstractYamlRebindTest extends RebindTestFixture { - private static final Logger LOG = LoggerFactory.getLogger(AbstractYamlTest.class); + private static final Logger LOG = LoggerFactory.getLogger(AbstractYamlRebindTest.class); protected static final String TEST_VERSION = "0.1.2"; protected BrooklynCampPlatform platform; protected BrooklynCampPlatformLauncherNoServer launcher; private boolean forceUpdate; - + @BeforeMethod(alwaysRun = true) @Override public void setUp() throws Exception { @@ -106,7 +98,7 @@ protected LocalManagementContext newMgmtContext() { } return result; } - + @Override protected StartableApplication createApp() { return null; @@ -116,11 +108,11 @@ protected StartableApplication createApp() { protected ManagementContext mgmt() { return (newManagementContext != null) ? newManagementContext : origManagementContext; } - + /////////////////////////////////////////////////// // TODO code below is duplicate of AbstractYamlTest /////////////////////////////////////////////////// - + protected void waitForApplicationTasks(Entity app) { Set> tasks = BrooklynTaskTags.getTasksInEntityContext(mgmt().getExecutionManager(), app); getLogger().info("Waiting on " + tasks.size() + " task(s)"); @@ -149,7 +141,7 @@ protected Entity createAndStartApplication(Reader input) throws Exception { protected Entity createAndStartApplication(String... multiLineYaml) throws Exception { return createAndStartApplication(joinLines(multiLineYaml)); } - + protected Entity createAndStartApplication(String input) throws Exception { return createAndStartApplication(input, MutableMap.of()); } @@ -168,18 +160,18 @@ protected Entity createAndStartApplication(String input, Map startPara protected Entity createStartWaitAndLogApplication(String... input) throws Exception { return createStartWaitAndLogApplication(joinLines(input)); } - + protected Entity createStartWaitAndLogApplication(String input) throws Exception { return createStartWaitAndLogApplication(new StringReader(input)); } - + protected Entity createStartWaitAndLogApplication(Reader input) throws Exception { Entity app = createAndStartApplication(input); waitForApplicationTasks(app); getLogger().info("App started:"); Entities.dumpInfo(app); - + return app; } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlRebindTest.java new file mode 100644 index 0000000000..09a9075efe --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlRebindTest.java @@ -0,0 +1,102 @@ +/* + * 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.camp.brooklyn; + +import static org.apache.brooklyn.test.Asserts.assertFalse; +import static org.testng.Assert.assertEquals; + +import java.util.List; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.effector.CompositeEffector; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.core.entity.EntityPredicates; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.util.guava.Maybe; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +@Test +public class CompositeEffectorYamlRebindTest extends AbstractYamlRebindTest { + + private final static String appId = "my-app-with-composite-effector"; + private final static String appVersion = "1.0.0-SNAPSHOT"; + static final String appVersionedId = appId + ":" + appVersion; + + static final String catalogYamlSimple = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appId, + " version: " + appVersion, + " itemType: entity", + " item:", + " type: " + TestEntity.class.getName(), + " name: targetEntity", + " brooklyn.initializers:", + " - type: " + HttpCommandEffector.class.getName(), + " brooklyn.config:", + " name: myEffector", + " description: myDescription", + " uri: https://httpbin.org/get?id=myId", + " httpVerb: GET", + " jsonPath: $.args.id", + " publishSensor: results", + " - type: " + CompositeEffector.class.getName(), + " brooklyn.config:", + " name: start", + " override: true", + " effectors:", + " - myEffector" + ); + + @Test + public void testRebindWhenHealthy() throws Exception { + runRebindWhenIsUp(catalogYamlSimple, appVersionedId); + } + + protected void runRebindWhenIsUp(String catalogYaml, String appId) throws Exception { + addCatalogItems(catalogYaml); + + String appYaml = Joiner.on("\n").join( + "services: ", + "- type: " + appId); + createStartWaitAndLogApplication(appYaml); + + // Rebind + StartableApplication newApp = rebind(); + TestEntity testEntity = (TestEntity) Iterables.find(newApp.getChildren(), EntityPredicates.displayNameEqualTo("targetEntity")); + Effector effector = assertHasInitializers(testEntity, "start"); + + // Confirm HttpCommandEffector still functions + Object results = testEntity.invoke(effector, ImmutableMap.of()).get(); + assertEquals(((List)results).get(0), "myId"); + } + + + protected static Effector assertHasInitializers(Entity entity, String effectorName) { + Maybe> effectorMaybe = entity.getEntityType().getEffectorByName(effectorName); + assertFalse(effectorMaybe.isAbsent()); + return effectorMaybe.get(); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlTest.java new file mode 100644 index 0000000000..f114f238e2 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/CompositeEffectorYamlTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.brooklyn; + +import static org.testng.Assert.assertEquals; + +import java.util.List; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.effector.CompositeEffector; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +public class CompositeEffectorYamlTest extends AbstractYamlTest { + private static final Logger log = LoggerFactory.getLogger(CompositeEffectorYamlTest.class); + + @Test + public void testCompositeEffector() throws Exception { + Entity app = createAndStartApplication( + "location: localhost", + "services:", + "- type: " + EmptySoftwareProcess.class.getName(), + " brooklyn.config:", + " onbox.base.dir.skipResolution: true", + " softwareProcess.serviceProcessIsRunningPollPeriod: forever", + " brooklyn.initializers:", + " - type: " + HttpCommandEffector.class.getName(), + " brooklyn.config:", + " name: myEffector", + " description: myDescription", + " uri: https://httpbin.org/get?id=myId", + " httpVerb: GET", + " jsonPath: $.args.id", + " publishSensor: results", + " - type: " + CompositeEffector.class.getName(), + " brooklyn.config:", + " name: start", + " override: true", + " effectors:", + " - myEffector" + ); + waitForApplicationTasks(app); + + EmptySoftwareProcess entity = (EmptySoftwareProcess) Iterables.getOnlyElement(app.getChildren()); + Effector effector = entity.getEntityType().getEffectorByName("start").get(); + + // Invoke without parameter + Object results = entity.invoke(effector, ImmutableMap.of()).get(); + assertEquals(((List)results).get(0), "myId"); + } + + @Override + protected Logger getLogger() { + return log; + } +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlRebindTest.java new file mode 100644 index 0000000000..df57bcbeb8 --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlRebindTest.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.camp.brooklyn; + +import static org.apache.brooklyn.test.Asserts.assertFalse; +import static org.testng.Assert.assertEquals; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.core.entity.EntityPredicates; +import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.util.guava.Maybe; +import org.testng.annotations.Test; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +@Test +public class HttpCommandEffectorYamlRebindTest extends AbstractYamlRebindTest { + + private final static String appId = "my-app-with-http-effector"; + private final static String appVersion = "1.0.0-SNAPSHOT"; + static final String appVersionedId = appId + ":" + appVersion; + + static final String catalogYamlSimple = Joiner.on("\n").join( + "brooklyn.catalog:", + " id: " + appId, + " version: " + appVersion, + " itemType: entity", + " item:", + " type: " + TestEntity.class.getName(), + " name: targetEntity", + " brooklyn.initializers:", + " - type: " + HttpCommandEffector.class.getName(), + " brooklyn.config:", + " name: myEffector", + " description: myDescription", + " uri: https://httpbin.org/get?id=myId", + " httpVerb: GET", + " jsonPath: $.args.id", + " publishSensor: results"); + + @Test + public void testRebindWhenHealthy() throws Exception { + runRebindWhenIsUp(catalogYamlSimple, appVersionedId); + } + + protected void runRebindWhenIsUp(String catalogYaml, String appId) throws Exception { + addCatalogItems(catalogYaml); + + String appYaml = Joiner.on("\n").join( + "services: ", + "- type: " + appId); + createStartWaitAndLogApplication(appYaml); + + // Rebind + StartableApplication newApp = rebind(); + TestEntity testEntity = (TestEntity) Iterables.find(newApp.getChildren(), EntityPredicates.displayNameEqualTo("targetEntity")); + Effector effector = assertHasInitializers(testEntity, "myEffector"); + + // Confirm HttpCommandEffector still functions + Object result = testEntity.invoke(effector, ImmutableMap.of()).get(); + assertEquals(((String)result).trim(), "myId"); + } + + + protected static Effector assertHasInitializers(Entity entity, String effectorName) { + Maybe> effectorMaybe = entity.getEntityType().getEffectorByName(effectorName); + assertFalse(effectorMaybe.isAbsent()); + return effectorMaybe.get(); + } + +} diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlTest.java new file mode 100644 index 0000000000..b36771cf7f --- /dev/null +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/HttpCommandEffectorYamlTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.camp.brooklyn; + +import static org.testng.Assert.assertEquals; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.Entity; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.entity.software.base.EmptySoftwareProcess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +public class HttpCommandEffectorYamlTest extends AbstractYamlTest { + private static final Logger log = LoggerFactory.getLogger(HttpCommandEffectorYamlTest.class); + + @Test + public void testHttpCommandEffectorWithParameters() throws Exception { + Entity app = createAndStartApplication( + "location: localhost", + "services:", + "- type: " + EmptySoftwareProcess.class.getName(), + " brooklyn.config:", + " onbox.base.dir.skipResolution: true", + " softwareProcess.serviceProcessIsRunningPollPeriod: forever", + " brooklyn.initializers:", + " - type: " + HttpCommandEffector.class.getName(), + " brooklyn.config:", + " name: myEffector", + " description: myDescription", + " uri: https://httpbin.org/get?id=myId", + " httpVerb: GET", + " jsonPath: $.args.id", + " publishSensor: results" + ); + waitForApplicationTasks(app); + + EmptySoftwareProcess entity = (EmptySoftwareProcess) Iterables.getOnlyElement(app.getChildren()); + Effector effector = entity.getEntityType().getEffectorByName("myEffector").get(); + + // Invoke with parameters + { + Object result = entity.invoke(effector, ImmutableMap.of("uri", "https://httpbin.org/get?pwd=passwd", "jsonPath", "$.args.pwd")).get(); + assertEquals(((String)result).trim(), "passwd"); + + } + + // Invoke with default parameter + { + Object result = entity.invoke(effector, ImmutableMap.of()).get(); + assertEquals(((String)result).trim(), "myId"); + } + } + + @Override + protected Logger getLogger() { + return log; + } +} diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java b/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java index ba8d679046..92cc4ec140 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/AddSensor.java @@ -57,6 +57,7 @@ public class AddSensor implements EntityInitializer { protected final Duration period; protected final String type; protected AttributeSensor sensor; + protected final ConfigBag params; public AddSensor(Map params) { this(ConfigBag.newInstance(params)); @@ -66,6 +67,7 @@ public AddSensor(final ConfigBag params) { this.name = Preconditions.checkNotNull(params.get(SENSOR_NAME), "Name must be supplied when defining a sensor"); this.period = params.get(SENSOR_PERIOD); this.type = params.get(SENSOR_TYPE); + this.params = params; } @Override diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/CompositeEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/CompositeEffector.java new file mode 100644 index 0000000000..5345b4ae2c --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/effector/CompositeEffector.java @@ -0,0 +1,135 @@ +/* + * 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.core.effector; + +import static org.apache.brooklyn.core.entity.trait.Startable.START; +import static org.apache.brooklyn.core.entity.trait.Startable.STOP; + +import java.util.List; +import java.util.Map; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntityLocal; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder; +import org.apache.brooklyn.core.entity.EntityInitializers; +import org.apache.brooklyn.core.entity.EntityInternal; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.guava.Maybe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.reflect.TypeToken; + +@Beta +public class CompositeEffector extends AddEffector { + + private static final Logger LOG = LoggerFactory.getLogger(CompositeEffector.class); + private static final String ORIGINAL_PREFIX = "original-"; + + public static final ConfigKey> EFFECTORS = ConfigKeys.newConfigKey(new TypeToken>() { + }, "effectors", + "Effector names to be chained together in the composite effector", ImmutableList.of()); + public static final ConfigKey OVERRIDE = ConfigKeys.newBooleanConfigKey("override", + "Wheter additional defined effectors should override pre-existing effector with same name or not (default: false)", Boolean.FALSE); + + public CompositeEffector(ConfigBag params) { + super(newEffectorBuilder(params).build()); + } + + public CompositeEffector(Map params) { + this(ConfigBag.newInstance(params)); + } + + public static EffectorBuilder newEffectorBuilder(ConfigBag params) { + EffectorBuilder eff = AddEffector.newEffectorBuilder(List.class, params); + eff.impl(new Body(eff.buildAbstract(), params)); + return eff; + } + + @Override + public void apply(EntityLocal entity) { + Maybe> effectorMaybe = entity.getEntityType().getEffectorByName(effector.getName()); + if (!effectorMaybe.isAbsentOrNull()) { + Effector original = Effectors.effector(effectorMaybe.get()).name(ORIGINAL_PREFIX + effector.getName()).build(); + ((EntityInternal) entity).getMutableEntityType().addEffector(original); + } + super.apply(entity); + } + + protected static class Body extends EffectorBody { + private final Effector effector; + private final ConfigBag params; + + public Body(Effector eff, ConfigBag params) { + this.effector = eff; + Preconditions.checkNotNull(params.getAllConfigRaw().get(EFFECTORS.getName()), "Effector names must be supplied when defining this effector"); + this.params = params; + } + + @Override + public List call(final ConfigBag params) { + ConfigBag allConfig = ConfigBag.newInstanceCopying(this.params).putAll(params); + final List effectorNames = EntityInitializers.resolve(allConfig, EFFECTORS); + final Boolean override = allConfig.get(OVERRIDE); + + List results = Lists.newArrayList(); + + if (!override && isStartRedefined()) { + results.add(invokeEffectorNamed(ORIGINAL_PREFIX + START.getName(), params)); + } + for (String eff : effectorNames) { + results.add(invokeEffectorNamed(eff, params)); + } + if (!override && isStopRedefined()) { + results.add(invokeEffectorNamed(ORIGINAL_PREFIX + STOP.getName(), params)); + } + return results; + } + + private boolean isStartRedefined() { + return isEffectorRedefined(ORIGINAL_PREFIX + START.getName()); + } + + private boolean isStopRedefined() { + return isEffectorRedefined(ORIGINAL_PREFIX + STOP.getName()); + } + + private boolean isEffectorRedefined(String effectorName) { + return entity().getEntityType().getEffectorByName(effectorName).isPresent(); + } + + private Object invokeEffectorNamed(String effectorName, ConfigBag params) { + LOG.info("{} invoking effector on {}, effector={}, parameters={}", + new Object[]{this, entity(), effectorName, params}); + Maybe> effector = entity().getEntityType().getEffectorByName(effectorName); + if (effector.isAbsent()) { + throw new IllegalStateException("Cannot find effector " + effectorName); + } + return entity().invoke(effector.get(), params.getAllConfig()).getUnchecked(); + } + + } + +} diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java b/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java index c644001a86..53db25ae82 100644 --- a/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java +++ b/core/src/main/java/org/apache/brooklyn/core/effector/Effectors.java @@ -64,6 +64,10 @@ private EffectorBuilder(Class returnType, String effectorName) { this.returnType = returnType; this.effectorName = effectorName; } + public EffectorBuilder name(String name) { + this.effectorName = name; + return this; + } public EffectorBuilder description(String description) { this.description = description; return this; diff --git a/core/src/main/java/org/apache/brooklyn/core/effector/http/HttpCommandEffector.java b/core/src/main/java/org/apache/brooklyn/core/effector/http/HttpCommandEffector.java new file mode 100644 index 0000000000..1388a926d6 --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/effector/http/HttpCommandEffector.java @@ -0,0 +1,209 @@ +/* + * 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.core.effector.http; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.mgmt.Task; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.config.MapConfigKey; +import org.apache.brooklyn.core.effector.AddEffector; +import org.apache.brooklyn.core.effector.EffectorBody; +import org.apache.brooklyn.core.effector.Effectors.EffectorBuilder; +import org.apache.brooklyn.core.entity.EntityInitializers; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.util.collections.Jsonya; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.task.Tasks; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.http.executor.HttpConfig; +import org.apache.brooklyn.util.http.executor.HttpExecutor; +import org.apache.brooklyn.util.http.executor.HttpRequest; +import org.apache.brooklyn.util.http.executor.HttpResponse; +import org.apache.brooklyn.util.http.executor.UsernamePassword; +import org.apache.brooklyn.util.http.executor.apacheclient.HttpExecutorImpl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Enums; +import com.google.common.base.Joiner; +import com.google.common.base.Optional; +import com.google.common.io.ByteStreams; +import com.google.common.net.HttpHeaders; +import com.jayway.jsonpath.JsonPath; + +public final class HttpCommandEffector extends AddEffector { + + private static final Logger LOG = LoggerFactory.getLogger(HttpCommandEffector.class); + + public static final ConfigKey EFFECTOR_URI = ConfigKeys.newStringConfigKey("uri"); + public static final ConfigKey EFFECTOR_HTTP_VERB = ConfigKeys.newStringConfigKey("httpVerb"); + public static final ConfigKey EFFECTOR_HTTP_USERNAME = ConfigKeys.newStringConfigKey("httpUsername"); + public static final ConfigKey EFFECTOR_HTTP_PASSWORD = ConfigKeys.newStringConfigKey("httpPassword"); + public static final ConfigKey> EFFECTOR_HTTP_HEADERS = new MapConfigKey(String.class, "headers"); + public static final ConfigKey EFFECTOR_HTTP_PAYLOAD = ConfigKeys.newConfigKey(Object.class, "httpPayload"); + public static final ConfigKey JSON_PATH = ConfigKeys.newStringConfigKey("jsonPath", "JSON path to select in HTTP response"); + public static final ConfigKey PUBLISH_SENSOR = ConfigKeys.newStringConfigKey("publishSensor", "Sensor name where to store json path extracted value"); + + public static final String APPLICATION_JSON = "application/json"; + + private enum HttpVerb { + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE + } + + public HttpCommandEffector(ConfigBag params) { + super(newEffectorBuilder(params).build()); + } + + public static EffectorBuilder newEffectorBuilder(ConfigBag params) { + EffectorBuilder eff = AddEffector.newEffectorBuilder(String.class, params); + eff.impl(new Body(eff.buildAbstract(), params)); + return eff; + } + + protected static class Body extends EffectorBody { + private final Effector effector; + private final ConfigBag params; + + public Body(Effector eff, final ConfigBag params) { + this.effector = eff; + checkNotNull(params.getAllConfigRaw().get(EFFECTOR_URI.getName()), "uri must be supplied when defining this effector"); + checkNotNull(params.getAllConfigRaw().get(EFFECTOR_HTTP_VERB.getName()), "HTTP verb must be supplied when defining this effector"); + this.params = params; + } + + @Override + public String call(final ConfigBag params) { + ConfigBag allConfig = ConfigBag.newInstanceCopying(this.params).putAll(params); + final URI uri = convertToURI(EntityInitializers.resolve(allConfig, EFFECTOR_URI)); + final String httpVerb = isValidHttpVerb(EntityInitializers.resolve(allConfig, EFFECTOR_HTTP_VERB)); + final String httpUsername = EntityInitializers.resolve(allConfig, EFFECTOR_HTTP_USERNAME); + final String httpPassword = EntityInitializers.resolve(allConfig, EFFECTOR_HTTP_PASSWORD); + final Map headers = EntityInitializers.resolve(allConfig, EFFECTOR_HTTP_HEADERS); + final Object payload = EntityInitializers.resolve(allConfig, EFFECTOR_HTTP_PAYLOAD); + final String jsonPath = EntityInitializers.resolve(allConfig, JSON_PATH); + final String publishSensor = EntityInitializers.resolve(allConfig, PUBLISH_SENSOR); + final HttpExecutor httpExecutor = HttpExecutorImpl.newInstance(); + + final HttpRequest request = buildHttpRequest(httpVerb, uri, headers, httpUsername, httpPassword, payload); + Task t = Tasks.builder().displayName(effector.getName()).body(new Callable() { + @Override + public Object call() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + HttpResponse response = httpExecutor.execute(request); + validateResponse(response); + ByteStreams.copy(response.getContent(), out); + return new String(out.toByteArray()); + } catch (IOException e) { + throw Exceptions.propagate(e); + } + } + }).build(); + + String responseBody = (String) queue(t).getUnchecked(); + + if (jsonPath == null) return responseBody; + + String extractedValue = JsonPath.parse(responseBody).read(jsonPath, String.class); + if (publishSensor != null) { + entity().sensors().set(Sensors.newStringSensor(publishSensor), extractedValue); + } + return extractedValue; + } + + private URI convertToURI(String url) { + try { + return new URL(url).toURI(); + } catch (MalformedURLException e) { + throw Exceptions.propagate(e); + } catch (URISyntaxException e) { + throw Exceptions.propagate(e); + } + } + + private void validateResponse(HttpResponse response) { + int statusCode = response.code(); + if (statusCode == 401) { + throw new RuntimeException("Authorization exception"); + } else if (statusCode == 404) { + throw new RuntimeException("Resource not found"); + } else if (statusCode >= 500) { + throw new RuntimeException("Server error"); + } + } + + private HttpRequest buildHttpRequest(String httpVerb, URI uri, Map headers, String httpUsername, String httpPassword, Object payload) { + HttpRequest.Builder httpRequestBuilder = new HttpRequest.Builder() + .uri(uri) + .method(httpVerb) + .config(HttpConfig.builder() + .trustSelfSigned(true) + .trustAll(true) + .laxRedirect(true) + .build()); + + if (headers != null) { + httpRequestBuilder.headers(headers); + } + + if (payload != null) { + String body = ""; + String contentType = headers.get(HttpHeaders.CONTENT_TYPE); + if (contentType == null || contentType.equalsIgnoreCase(APPLICATION_JSON)) { + LOG.warn("Content-Type not specified. Using {}, as default (continuing)", APPLICATION_JSON); + body = toJsonString(payload); + } else if (!(payload instanceof String) && !contentType.equalsIgnoreCase(APPLICATION_JSON)) { + LOG.warn("the http request may fail with payload {} and 'Content-Type= {}, (continuing)", payload, contentType); + body = payload.toString(); + } + httpRequestBuilder.body(body.getBytes()); + } + + if (httpUsername != null && httpPassword != null) { + httpRequestBuilder.credentials(new UsernamePassword(httpUsername, httpPassword)); + } + + return httpRequestBuilder.build(); + } + + private String isValidHttpVerb(String httpVerb) { + Optional state = Enums.getIfPresent(HttpVerb.class, httpVerb.toUpperCase()); + checkArgument(state.isPresent(), "Expected one of %s but was %s", Joiner.on(',').join(HttpVerb.values()), httpVerb); + return httpVerb; + } + + private String toJsonString(Object payload) { + return Jsonya.newInstance().add(payload).toString(); + } + + } +} diff --git a/core/src/main/java/org/apache/brooklyn/core/entity/EntityInitializers.java b/core/src/main/java/org/apache/brooklyn/core/entity/EntityInitializers.java index a258007b5b..cffcced74b 100644 --- a/core/src/main/java/org/apache/brooklyn/core/entity/EntityInitializers.java +++ b/core/src/main/java/org/apache/brooklyn/core/entity/EntityInitializers.java @@ -22,6 +22,11 @@ import org.apache.brooklyn.api.entity.EntityInitializer; import org.apache.brooklyn.api.entity.EntityLocal; +import org.apache.brooklyn.api.mgmt.ExecutionContext; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.internal.ConfigKeySelfExtracting; +import org.apache.brooklyn.util.core.task.BasicExecutionContext; import com.google.common.collect.ImmutableList; @@ -41,9 +46,32 @@ public void apply(EntityLocal entity) { } } - public static EntityInitializer addingTags(Object... tags) { return new AddTags(tags); } - + + /** + * Resolves key in the + * {@link BasicExecutionContext#getCurrentExecutionContext current execution context}. + * @see #resolve(ConfigBag, ConfigKey, ExecutionContext) + */ + public static T resolve(ConfigBag configBag, ConfigKey key) { + return resolve(configBag, key, BasicExecutionContext.getCurrentExecutionContext()); + } + + /** + * Gets the value for key from configBag. + *

+ * If key is an instance of {@link ConfigKeySelfExtracting} and executionContext is + * not null then its value will be retrieved per the key's implementation of + * {@link ConfigKeySelfExtracting#extractValue extractValue}. Otherwise, the value + * will be retrieved from configBag directly. + */ + public static T resolve(ConfigBag configBag, ConfigKey key, ExecutionContext executionContext) { + if (key instanceof ConfigKeySelfExtracting && executionContext != null) { + ConfigKeySelfExtracting ckse = ((ConfigKeySelfExtracting) key); + return ckse.extractValue(configBag.getAllConfigAsConfigKeyMap(), executionContext); + } + return configBag.get(key); + } } diff --git a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java index dea44d3644..966a88cdbb 100644 --- a/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java +++ b/core/src/main/java/org/apache/brooklyn/core/sensor/http/HttpRequestSensor.java @@ -19,13 +19,14 @@ package org.apache.brooklyn.core.sensor.http; import java.net.URI; - -import net.minidev.json.JSONObject; +import java.util.Map; import org.apache.brooklyn.api.entity.EntityLocal; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.brooklyn.core.config.MapConfigKey; import org.apache.brooklyn.core.effector.AddSensor; +import org.apache.brooklyn.core.entity.EntityInitializers; import org.apache.brooklyn.core.sensor.ssh.SshCommandSensor; import org.apache.brooklyn.feed.http.HttpFeed; import org.apache.brooklyn.feed.http.HttpPollConfig; @@ -38,6 +39,8 @@ import com.google.common.base.Functions; import com.google.common.base.Supplier; +import net.minidev.json.JSONObject; + /** * Configurable {@link org.apache.brooklyn.api.entity.EntityInitializer} which adds an HTTP sensor feed to retrieve the * {@link JSONObject} from a JSON response in order to populate the sensor with the data at the {@code jsonPath}. @@ -53,24 +56,10 @@ public final class HttpRequestSensor extends AddSensor { public static final ConfigKey JSON_PATH = ConfigKeys.newStringConfigKey("jsonPath", "JSON path to select in HTTP response; default $", "$"); public static final ConfigKey USERNAME = ConfigKeys.newStringConfigKey("username", "Username for HTTP request, if required"); public static final ConfigKey PASSWORD = ConfigKeys.newStringConfigKey("password", "Password for HTTP request, if required"); - - protected final Supplier uri; - protected final String jsonPath; - protected final String username; - protected final String password; + public static final ConfigKey> HEADERS = new MapConfigKey(String.class, "headers"); public HttpRequestSensor(final ConfigBag params) { super(params); - - uri = new Supplier() { - @Override - public URI get() { - return URI.create(params.get(SENSOR_URI)); - } - }; - jsonPath = params.get(JSON_PATH); - username = params.get(USERNAME); - password = params.get(PASSWORD); } @Override @@ -81,18 +70,36 @@ public void apply(final EntityLocal entity) { LOG.debug("Adding HTTP JSON sensor {} to {}", name, entity); } + final ConfigBag allConfig = ConfigBag.newInstanceCopying(this.params).putAll(params); + final Supplier uri = new Supplier() { + @Override + public URI get() { + return URI.create(EntityInitializers.resolve(allConfig, SENSOR_URI)); + } + }; + final String jsonPath = EntityInitializers.resolve(allConfig, JSON_PATH); + final String username = EntityInitializers.resolve(allConfig, USERNAME); + final String password = EntityInitializers.resolve(allConfig, PASSWORD); + final Map headers = EntityInitializers.resolve(allConfig, HEADERS); + + HttpPollConfig pollConfig = new HttpPollConfig(sensor) .checkSuccess(HttpValueFunctions.responseCodeEquals(200)) .onFailureOrException(Functions.constant((T) null)) .onSuccess(HttpValueFunctions.jsonContentsFromPath(jsonPath)) .period(period); - HttpFeed feed = HttpFeed.builder().entity(entity) + HttpFeed.Builder httpRequestBuilder = HttpFeed.builder().entity(entity) .baseUri(uri) .credentialsIfNotNull(username, password) - .poll(pollConfig) - .build(); + .poll(pollConfig); + if (headers != null) { + httpRequestBuilder.headers(headers); + } + + HttpFeed feed = httpRequestBuilder.build(); entity.addFeed(feed); } + } diff --git a/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorIntegrationTest.java new file mode 100644 index 0000000000..dd168ed5ac --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorIntegrationTest.java @@ -0,0 +1,91 @@ +/* + * 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.core.effector; + +import static org.apache.brooklyn.test.Asserts.assertEquals; +import static org.apache.brooklyn.test.Asserts.assertNull; +import static org.apache.brooklyn.test.Asserts.assertTrue; + +import java.util.List; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntityLocal; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; + +public class CompositeEffectorIntegrationTest { + + final static Effector EFFECTOR_START = Effectors.effector(List.class, "start").buildAbstract(); + + private TestApplication app; + private EntityLocal entity; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + app = TestApplication.Factory.newManagedInstanceForTests(); + entity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC)); + app.start(ImmutableList.of()); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (app != null) Entities.destroyAll(app.getManagementContext()); + } + + @Test(groups="Integration") + public void testCompositeEffector() throws Exception { + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "eff1") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://api.github.com/users/apache") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.login")) + .apply(entity); + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "eff2") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://api.github.com/users/brooklyncentral") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.login")) + .apply(entity); + new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, "start") + .configure(CompositeEffector.EFFECTORS, ImmutableList.of("eff1", "eff2"))) + .apply(entity); + + List results = entity.invoke(EFFECTOR_START, MutableMap.of()).get(); + + assertEquals(results.size(), 3); + assertNull(results.get(0)); + assertTrue(results.get(1) instanceof String); + assertEquals(results.get(1), "apache"); + assertTrue(results.get(2) instanceof String); + assertEquals(results.get(2), "brooklyncentral"); + } + +} diff --git a/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorTest.java b/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorTest.java new file mode 100644 index 0000000000..3bf0018e5f --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/effector/CompositeEffectorTest.java @@ -0,0 +1,262 @@ +/* + * 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.core.effector; + +import static org.apache.brooklyn.core.effector.http.HttpCommandEffectorTest.EFFECTOR_HTTP_COMMAND; +import static org.apache.brooklyn.test.Asserts.assertNotNull; +import static org.apache.brooklyn.test.Asserts.assertNull; +import static org.apache.brooklyn.test.Asserts.assertTrue; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.core.effector.http.HttpCommandEffector; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.test.Asserts; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.http.BetterMockWebServer; +import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException; +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Charsets; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import com.google.mockwebserver.MockResponse; + +public class CompositeEffectorTest extends BrooklynAppUnitTestSupport { + + private static final Logger log = LoggerFactory.getLogger(CompositeEffectorTest.class); + private static final String DEFAULT_ENDPOINT = "/"; + + final static Effector EFFECTOR_COMPOSITE = Effectors.effector(List.class, "CompositeEffector").buildAbstract(); + + protected BetterMockWebServer server; + protected URL baseUrl; + + protected Location loc; + protected CompositeEffector compositeEffector; + + @BeforeMethod + public void start() throws IOException { + server = BetterMockWebServer.newInstanceLocalhost(); + server.play(); + } + + @AfterMethod(alwaysRun = true) + public void stop() throws IOException { + server.shutdown(); + } + + protected String url(String path) { + return server.getUrl(path).toString(); + } + + protected MockResponse jsonResponse(String resource) { + return new MockResponse().addHeader("Content-Type", "application/json").setBody(stringFromResource(resource)); + } + + protected MockResponse response404() { + return new MockResponse().setStatus("HTTP/1.1 404 Not Found"); + } + + protected MockResponse response204() { + return new MockResponse().setStatus("HTTP/1.1 204 No Content"); + } + + protected String stringFromResource(String resourceName) { + return stringFromResource("/org/apache/brooklyn/core/effector", resourceName); + } + + private String stringFromResource(String prefix, String resourceName) { + try { + return Resources.toString(getClass().getResource(String.format("%s/%s", prefix, resourceName)), Charsets.UTF_8) + .replace(DEFAULT_ENDPOINT, url("")); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Test + public void testCompositeEffectorWithNonExistingName() throws InterruptedException { + server.enqueue(jsonResponse("test.json")); + + HttpCommandEffector httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, EFFECTOR_COMPOSITE.getName()) + .configure(CompositeEffector.EFFECTORS, ImmutableList.of(EFFECTOR_HTTP_COMMAND.getName())) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector, compositeEffector)); + List results = testEntity.invoke(EFFECTOR_COMPOSITE, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + Asserts.assertEquals(results.size(), 1); + + assertTrue(results.get(0) instanceof String); + Asserts.assertEquals(results.get(0), "myLogin"); + } + + @Test + public void testCompositeEffectorWithStartName() throws InterruptedException { + server.enqueue(jsonResponse("test.json")); + + HttpCommandEffector httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, "start") + .configure(CompositeEffector.EFFECTORS, ImmutableList.of(EFFECTOR_HTTP_COMMAND.getName())) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector, compositeEffector)); + List results = testEntity.invoke(Effectors.effector(List.class, "start").buildAbstract(), ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + Asserts.assertEquals(results.size(), 2); + assertNull(results.get(0)); + assertTrue(results.get(1) instanceof String); + Asserts.assertEquals(results.get(1), "myLogin"); + } + + @Test + public void testCompositeEffectorWithStartNameAndOverriding() throws InterruptedException { + server.enqueue(jsonResponse("test.json")); + + HttpCommandEffector httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, "start") + .configure(CompositeEffector.OVERRIDE, true) + .configure(CompositeEffector.EFFECTORS, ImmutableList.of(EFFECTOR_HTTP_COMMAND.getName())) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector, compositeEffector)); + List results = testEntity.invoke(Effectors.effector(List.class, "start").buildAbstract(), ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + Asserts.assertEquals(results.size(), 1); + assertTrue(results.get(0) instanceof String); + Asserts.assertEquals(results.get(0), "myLogin"); + } + + @Test + public void testCompositeEffectorWithStopName() throws InterruptedException { + server.enqueue(jsonResponse("test.json")); + + HttpCommandEffector httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, "stop") + .configure(CompositeEffector.EFFECTORS, ImmutableList.of(EFFECTOR_HTTP_COMMAND.getName())) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector, compositeEffector)); + List results = testEntity.invoke(Effectors.effector(List.class, "stop").buildAbstract(), ImmutableMap.of()).getUnchecked(Duration.minutes(1)); + Asserts.assertEquals(results.size(), 2); + assertTrue(results.get(0) instanceof String); + Asserts.assertEquals(results.get(0), "myLogin"); + assertNull(results.get(1)); + } + + @Test + public void testCompositeEffectorWithStopNameAndOverriding() throws InterruptedException { + server.enqueue(jsonResponse("test.json")); + + HttpCommandEffector httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, "stop") + .configure(CompositeEffector.OVERRIDE, true) + .configure(CompositeEffector.EFFECTORS, ImmutableList.of(EFFECTOR_HTTP_COMMAND.getName())) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector, compositeEffector)); + List results = testEntity.invoke(Effectors.effector(List.class, "stop").buildAbstract(), ImmutableMap.of()).getUnchecked(Duration.minutes(1)); + Asserts.assertEquals(results.size(), 1); + assertTrue(results.get(0) instanceof String); + Asserts.assertEquals(results.get(0), "myLogin"); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testMissingEffectors() { + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, EFFECTOR_COMPOSITE.getName()) + .configure(CompositeEffector.EFFECTORS, null) + ); + } + + @Test(expectedExceptions = PropagatedRuntimeException.class) + public void testWhenOneEffectorFails() throws InterruptedException { + server.enqueue(response404()); + + HttpCommandEffector eff1 = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "eff1") + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login")); + compositeEffector = new CompositeEffector(ConfigBag.newInstance() + .configure(CompositeEffector.EFFECTOR_NAME, EFFECTOR_COMPOSITE.getName()) + .configure(CompositeEffector.EFFECTORS, ImmutableList.of("eff1", "eff2")) + ); + assertNotNull(compositeEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(eff1, compositeEffector)); + List results = testEntity.invoke(EFFECTOR_COMPOSITE, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + Asserts.assertEquals(results.size(), 2); + } + + private EntitySpec buildEntitySpec(AddEffector... effectors) { + EntitySpec testEntitySpec = EntitySpec.create(TestEntity.class); + for (AddEffector effector : effectors) { + testEntitySpec.addInitializer(effector); + } + return testEntitySpec; + } + +} diff --git a/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorIntegrationTest.java b/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorIntegrationTest.java new file mode 100644 index 0000000000..7d1a91a916 --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorIntegrationTest.java @@ -0,0 +1,125 @@ +/* + * 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.core.effector.http; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntityLocal; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.core.effector.Effectors; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.sensor.Sensors; +import org.apache.brooklyn.core.test.entity.TestApplication; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.jayway.jsonpath.JsonPath; + +public class HttpCommandEffectorIntegrationTest { + + final static Effector EFFECTOR_HTTPBIN = Effectors.effector(String.class, "Httpbin").buildAbstract(); + + private TestApplication app; + private EntityLocal entity; + + @BeforeMethod(alwaysRun=true) + public void setUp() throws Exception { + app = TestApplication.Factory.newManagedInstanceForTests(); + entity = app.createAndManageChild(EntitySpec.create(TestEntity.class).location(TestApplication.LOCALHOST_MACHINE_SPEC)); + app.start(ImmutableList.of()); + } + + @AfterMethod(alwaysRun=true) + public void tearDown() throws Exception { + if (app != null) Entities.destroyAll(app.getManagementContext()); + } + + @Test(groups="Integration") + public void testHttpEffector() throws Exception { + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "Httpbin") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://httpbin.org/get?login=myLogin") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + ).apply(entity); + + String val = entity.invoke(EFFECTOR_HTTPBIN, MutableMap.of()).get(); + Assert.assertEquals(JsonPath.parse(val).read("$.args.login", String.class), "myLogin"); + } + + @Test(groups="Integration") + public void testHttpEffectorWithPayload() throws Exception { + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "HttpbinPost") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://httpbin.org/post") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "POST") + .configure(HttpCommandEffector.EFFECTOR_HTTP_PAYLOAD, ImmutableMap.of( + "description", "Created via API", + "public", "false", + "files", ImmutableMap.of("demo.txt", ImmutableMap.of("content","Demo")))) + .configure(HttpCommandEffector.EFFECTOR_HTTP_HEADERS, ImmutableMap.of("Content-Type", "application/json")) + .configure(HttpCommandEffector.JSON_PATH, "$.url") + .configure(HttpCommandEffector.PUBLISH_SENSOR, "result") + ).apply(entity); + + String url = entity.invoke(Effectors.effector(String.class, "HttpbinPost").buildAbstract(), MutableMap.of()).get(); + Assert.assertNotNull(url, "url"); + } + + @Test(groups="Integration") + public void testHttpEffectorWithJsonPath() throws Exception { + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "Httpbin") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://httpbin.org/get?id=myId") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.id") + .configure(HttpCommandEffector.PUBLISH_SENSOR, "result") + ).apply(entity); + + String val = entity.invoke(EFFECTOR_HTTPBIN, MutableMap.of()).get(); + Assert.assertEquals(val, "myId"); + Assert.assertEquals(entity.sensors().get(Sensors.newStringSensor("result")), "myId"); + } + + @Test(groups="Integration") + public void testHttpEffectorWithParameters() throws Exception { + new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, "Httpbin") + .configure(HttpCommandEffector.EFFECTOR_URI, "https://httpbin.org/get") + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.EFFECTOR_PARAMETER_DEFS, + MutableMap.of("uri", MutableMap.of("defaultValue", "https://httpbin.org/get")))) + .apply(entity); + + String val; + // explicit value + val = entity.invoke(EFFECTOR_HTTPBIN, MutableMap.of("uri", "https://httpbin.org/ip")).get(); + Assert.assertNotNull(JsonPath.parse(val).read("$.origin", String.class)); + + // default value + val = entity.invoke(EFFECTOR_HTTPBIN, MutableMap.of()).get(); + Assert.assertEquals(JsonPath.parse(val).read("$.url", String.class), "https://httpbin.org/get"); + } +} diff --git a/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorTest.java b/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorTest.java new file mode 100644 index 0000000000..8d620a4881 --- /dev/null +++ b/core/src/test/java/org/apache/brooklyn/core/effector/http/HttpCommandEffectorTest.java @@ -0,0 +1,272 @@ +/* + * 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.core.effector.http; + +import static org.apache.brooklyn.test.Asserts.assertNotNull; +import static org.testng.Assert.assertEquals; + +import java.io.IOException; +import java.net.URL; +import java.util.concurrent.ExecutionException; + +import org.apache.brooklyn.api.effector.Effector; +import org.apache.brooklyn.api.entity.EntitySpec; +import org.apache.brooklyn.api.location.Location; +import org.apache.brooklyn.core.effector.Effectors; +import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport; +import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.core.config.ConfigBag; +import org.apache.brooklyn.util.core.http.BetterMockWebServer; +import org.apache.brooklyn.util.exceptions.PropagatedRuntimeException; +import org.apache.brooklyn.util.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Charsets; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import com.google.common.net.HttpHeaders; +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.RecordedRequest; + +public class HttpCommandEffectorTest extends BrooklynAppUnitTestSupport { + + private static final Logger log = LoggerFactory.getLogger(HttpCommandEffectorTest.class); + private static final String DEFAULT_ENDPOINT = "/"; + + public final static Effector EFFECTOR_HTTP_COMMAND = Effectors.effector(String.class, "http-command-effector").buildAbstract(); + + protected BetterMockWebServer server; + protected URL baseUrl; + + protected Location loc; + protected HttpCommandEffector httpCommandEffector; + + @BeforeMethod + public void start() throws IOException { + server = BetterMockWebServer.newInstanceLocalhost(); + server.play(); + } + + @AfterMethod(alwaysRun = true) + public void stop() throws IOException { + server.shutdown(); + } + + protected String url(String path) { + return server.getUrl(path).toString(); + } + + protected MockResponse jsonResponse(String resource) { + return new MockResponse().addHeader(HttpHeaders.CONTENT_TYPE, "application/json").setBody(stringFromResource(resource)); + } + + protected MockResponse response404() { + return new MockResponse().setStatus("HTTP/1.1 404 Not Found"); + } + + protected String stringFromResource(String resourceName) { + return stringFromResource("/org/apache/brooklyn/core/effector/http", resourceName); + } + + private String stringFromResource(String prefix, String resourceName) { + try { + return Resources.toString(getClass().getResource(String.format("%s/%s", prefix, resourceName)), Charsets.UTF_8) + .replace(DEFAULT_ENDPOINT, url("")); + } catch (IOException e) { + throw Throwables.propagate(e); + } + } + + protected RecordedRequest assertSent(BetterMockWebServer server, String method, String path) throws InterruptedException { + RecordedRequest request = server.takeRequest(); + assertEquals(request.getMethod(), method); + assertEquals(request.getPath(), path); + return request; + } + + @Test(expectedExceptions = NullPointerException.class) + public void testMissingURI() { + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + ); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testMissingVerb() { + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("")) + ); + } + + @Test(expectedExceptions = ExecutionException.class) + public void testInvalidURI() throws ExecutionException, InterruptedException { + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.EFFECTOR_URI, "invalid-uri") + ); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + testEntity.invoke(EFFECTOR_HTTP_COMMAND, MutableMap.of()).get(); + } + + @Test(expectedExceptions = ExecutionException.class) + public void testInvalidVerb() throws ExecutionException, InterruptedException { + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "CHANGE") + .configure(HttpCommandEffector.EFFECTOR_URI, url("")) + ); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + testEntity.invoke(EFFECTOR_HTTP_COMMAND, MutableMap.of()).get(); + } + + @Test + public void testPayloadWithContentTypeHeaderJson() throws InterruptedException { + server.enqueue(jsonResponse("map-response.json")); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/post")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "POST") + .configure(HttpCommandEffector.EFFECTOR_HTTP_PAYLOAD, ImmutableMap.of("key", "value")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_HEADERS, ImmutableMap.of(HttpHeaders.CONTENT_TYPE, "application/json")) + .configure(HttpCommandEffector.JSON_PATH, "$.data") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.minutes(1)); + assertEquals(output, "{\"key\", \"value\"}"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "POST", "/post"); + } + + @Test + public void testPayloadWithoutContentTypeHeader() throws InterruptedException { + server.enqueue(jsonResponse("map-response.json")); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/post")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "POST") + .configure(HttpCommandEffector.EFFECTOR_HTTP_PAYLOAD, ImmutableMap.of("key", "value")) + .configure(HttpCommandEffector.JSON_PATH, "$.data") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + assertEquals(output, "{\"key\", \"value\"}"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "POST", "/post"); + } + + @Test + public void testListPayloadWithoutContentTypeHeader() throws InterruptedException { + server.enqueue(jsonResponse("list-response.json")); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/post")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "POST") + .configure(HttpCommandEffector.EFFECTOR_HTTP_PAYLOAD, ImmutableList.of("key", "value")) + .configure(HttpCommandEffector.JSON_PATH, "$.data") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + assertEquals(output, "[\"key\", \"value\"]"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "POST", "/post"); + } + + @Test + public void testPayloadWithContentTypeHeaderXml() throws InterruptedException { + server.enqueue(jsonResponse("int-response.json")); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/post")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "POST") + .configure(HttpCommandEffector.EFFECTOR_HTTP_PAYLOAD, 1) + .configure(HttpCommandEffector.EFFECTOR_HTTP_HEADERS, ImmutableMap.of(HttpHeaders.CONTENT_TYPE, "application/xml")) + .configure(HttpCommandEffector.JSON_PATH, "$.data") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + assertEquals(output, "1"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "POST", "/post"); + } + + @Test + public void testHappyPath() throws InterruptedException { + server.enqueue(jsonResponse("login.json")); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + assertEquals(output, "myLogin"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "GET", "/get?login=myLogin"); + } + + @Test(expectedExceptions = PropagatedRuntimeException.class) + public void testWhen404() throws InterruptedException { + server.enqueue(response404()); + + httpCommandEffector = new HttpCommandEffector(ConfigBag.newInstance() + .configure(HttpCommandEffector.EFFECTOR_NAME, EFFECTOR_HTTP_COMMAND.getName()) + .configure(HttpCommandEffector.EFFECTOR_URI, url("/get?login=myLogin")) + .configure(HttpCommandEffector.EFFECTOR_HTTP_VERB, "GET") + .configure(HttpCommandEffector.JSON_PATH, "$.args.login") + ); + assertNotNull(httpCommandEffector); + TestEntity testEntity = app.createAndManageChild(buildEntitySpec(httpCommandEffector)); + Object output = testEntity.invoke(EFFECTOR_HTTP_COMMAND, ImmutableMap.of()).getUnchecked(Duration.seconds(1)); + assertEquals(output, "myLogin"); + + assertEquals(server.getRequestCount(), 1); + assertSent(server, "GET", "/get?login=myLogin"); + } + + private EntitySpec buildEntitySpec(HttpCommandEffector httpCommandEffector) { + return EntitySpec.create(TestEntity.class).addInitializer(httpCommandEffector); + } + +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/http/int-response.json b/core/src/test/resources/org/apache/brooklyn/core/effector/http/int-response.json new file mode 100644 index 0000000000..9b133f74f4 --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/http/int-response.json @@ -0,0 +1,16 @@ +{ + "args": {}, + "data": "1", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "1", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "curl/7.49.1" + }, + "json": 1, + "origin": "93.61.99.89", + "url": "http://httpbin.org/post" +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/http/list-response.json b/core/src/test/resources/org/apache/brooklyn/core/effector/http/list-response.json new file mode 100644 index 0000000000..88394ea389 --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/http/list-response.json @@ -0,0 +1,19 @@ +{ + "args": {}, + "data": "[\"key\", \"value\"]", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "16", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "curl/7.49.1" + }, + "json": [ + "key", + "value" + ], + "origin": "93.61.99.89", + "url": "http://httpbin.org/post" +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/http/login.json b/core/src/test/resources/org/apache/brooklyn/core/effector/http/login.json new file mode 100644 index 0000000000..b39889a34f --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/http/login.json @@ -0,0 +1,16 @@ +{ + "args": { + "login": "myLogin" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch, br", + "Accept-Language": "en-US,en;q=0.8,it;q=0.6", + "Cookie": "_ga=GA1.2.1060288368.1484053495; _gat=1", + "Host": "httpbin.org", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36" + }, + "origin": "93.61.99.89", + "url": "https://httpbin.org/get?login=myLogin" +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/http/map-response.json b/core/src/test/resources/org/apache/brooklyn/core/effector/http/map-response.json new file mode 100644 index 0000000000..959b763ec1 --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/http/map-response.json @@ -0,0 +1,16 @@ +{ + "args": {}, + "data": "{\"key\", \"value\"}", + "files": {}, + "form": {}, + "headers": { + "Accept": "*/*", + "Content-Length": "16", + "Content-Type": "application/json", + "Host": "httpbin.org", + "User-Agent": "curl/7.49.1" + }, + "json": null, + "origin": "93.61.99.89", + "url": "http://httpbin.org/post" +} diff --git a/core/src/test/resources/org/apache/brooklyn/core/effector/test.json b/core/src/test/resources/org/apache/brooklyn/core/effector/test.json new file mode 100644 index 0000000000..b39889a34f --- /dev/null +++ b/core/src/test/resources/org/apache/brooklyn/core/effector/test.json @@ -0,0 +1,16 @@ +{ + "args": { + "login": "myLogin" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch, br", + "Accept-Language": "en-US,en;q=0.8,it;q=0.6", + "Cookie": "_ga=GA1.2.1060288368.1484053495; _gat=1", + "Host": "httpbin.org", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36" + }, + "origin": "93.61.99.89", + "url": "https://httpbin.org/get?login=myLogin" +} diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/EffectorApi.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/EffectorApi.java index 2865223f10..6143b6fd23 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/EffectorApi.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/EffectorApi.java @@ -18,19 +18,27 @@ */ package org.apache.brooklyn.rest.api; -import io.swagger.annotations.Api; -import org.apache.brooklyn.rest.domain.EffectorSummary; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; +import java.util.List; +import java.util.Map; import javax.validation.Valid; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.List; -import java.util.Map; + +import org.apache.brooklyn.rest.domain.EffectorSummary; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; @Path("/applications/{application}/entities/{entity}/effectors") @Api("Entity Effectors") @@ -54,7 +62,7 @@ public List list( @POST @Path("/{effector}") @ApiOperation(value = "Trigger an effector", - notes="Returns the return value (status 200) if it completes, or an activity task ID (status 202) if it times out") + notes="Returns the return value (status 200) if it completes, or an activity task ID (status 202) if it times out", response = String.class) @ApiResponses(value = { @ApiResponse(code = 404, message = "Could not find application, entity or effector") })