blob: 23dbdcb79bfbcd50c573ce01f1523e475d629da9 [file] [log] [blame]
/*
* 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.rest.resources;
import static org.testng.Assert.assertEquals;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import net.minidev.json.JSONObject;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Enricher;
import org.apache.brooklyn.api.sensor.Feed;
import org.apache.brooklyn.core.config.render.RendererHints;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.EntityPredicates;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.core.test.policy.TestEnricher;
import org.apache.brooklyn.core.test.policy.TestPolicy;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.rest.api.SensorApi;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.EntitySpec;
import org.apache.brooklyn.rest.test.config.render.TestRendererHints;
import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.http.HttpAsserts;
import org.apache.brooklyn.util.stream.Streams;
import org.apache.brooklyn.util.text.StringFunctions;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.apache.cxf.jaxrs.client.WebClient;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.common.base.Functions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Callables;
/**
* Test the {@link SensorApi} implementation.
* <p>
* Check that {@link SensorResource} correctly renders {@link AttributeSensor}
* values, including {@link RendererHints.DisplayValue} hints.
*/
@Test(singleThreaded = true,
// by using a different suite name we disallow interleaving other tests between the methods of this test class, which wrecks the test fixtures
suiteName = "SensorResourceTest")
public class SensorResourceTest extends BrooklynRestResourceTest {
final static ApplicationSpec SIMPLE_SPEC = ApplicationSpec.builder()
.name("simple-app")
.entities(ImmutableSet.of(new EntitySpec("simple-ent", RestMockSimpleEntity.class.getName())))
.locations(ImmutableSet.of("localhost"))
.build();
static final String SENSORS_ENDPOINT = "/applications/simple-app/entities/simple-ent/sensors";
static final String SENSOR_NAME = "amphibian.count";
static final AttributeSensor<Integer> SENSOR = Sensors.newIntegerSensor(SENSOR_NAME);
static final String SECRET_SENSOR_NAME = "secret.count";
static final AttributeSensor<Integer> SECRET_SENSOR = Sensors.newIntegerSensor(SECRET_SENSOR_NAME);
static final Integer SECRET_VALUE = 99999;
EntityInternal entity;
/**
* Sets up the application and entity.
* <p>
* Adds a sensor and sets its value to {@code 12345}. Configures a display value
* hint that appends {@code frogs} to the value of the sensor.
*/
@BeforeClass(alwaysRun = true)
public void setUp() throws Exception {
// Deploy application
startServer();
Response deploy = clientDeploy(SIMPLE_SPEC);
waitForApplicationToBeRunning(deploy.getLocation());
entity = (EntityInternal) Iterables.find(getManagementContext().getEntityManager().getEntities(), EntityPredicates.displayNameEqualTo("simple-ent"));
addAmphibianSensor(entity);
addSecretSensor(entity);
}
static void addAmphibianSensor(EntityInternal entity) {
// Add new sensor
entity.getMutableEntityType().addSensor(SENSOR);
entity.sensors().set(SENSOR, 12345);
// Register display value hint
RendererHints.register(SENSOR, RendererHints.displayValue(Functions.compose(StringFunctions.append(" frogs"), Functions.toStringFunction())));
}
static void addSecretSensor(EntityInternal entity) {
// Add new sensor
entity.getMutableEntityType().addSensor(SECRET_SENSOR);
entity.sensors().set(SECRET_SENSOR, SECRET_VALUE);
}
@AfterClass(alwaysRun = true)
public void tearDown() throws Exception {
TestRendererHints.clearRegistry();
}
/** Check default is to use display value hint. */
@Test
public void testBatchSensorRead() throws Exception {
Response response = client().path(SENSORS_ENDPOINT + "/current-state")
.accept(MediaType.APPLICATION_JSON)
.get();
Map<String, ?> currentState = response.readEntity(new GenericType<Map<String,?>>(Map.class) {});
int found = 0;
for (String sensor : currentState.keySet()) {
if (sensor.equals(SENSOR_NAME)) {
found++;
assertEquals(currentState.get(sensor), "12345 frogs");
}
if (sensor.equals(SECRET_SENSOR_NAME)) {
found++;
assertEquals(currentState.get(sensor), SECRET_VALUE);
}
}
Asserts.assertEquals(found, 2);
}
/** Check setting {@code raw} to {@code true} ignores display value hint. */
@Test
public void testBatchSensorReadRaw() throws Exception {
Response response = client().path(SENSORS_ENDPOINT + "/current-state")
.query("raw", "true")
.accept(MediaType.APPLICATION_JSON)
.get();
Map<String, ?> currentState = response.readEntity(new GenericType<Map<String,?>>(Map.class) {});
int found = 0;
for (String sensor : currentState.keySet()) {
if (sensor.equals(SENSOR_NAME)) {
found++;
assertEquals(currentState.get(sensor), Integer.valueOf(12345));
}
if (sensor.equals(SECRET_SENSOR_NAME)) {
found++;
assertEquals(currentState.get(sensor), SECRET_VALUE);
}
}
Asserts.assertEquals(found, 2);
}
protected Response doSensorTest(Boolean displayHints, Boolean raw, MediaType acceptsType, Object expectedValue) {
return doSensorTestUntyped(
displayHints==null ? null : (""+displayHints).toLowerCase(),
raw==null ? null : (""+raw).toLowerCase(),
acceptsType==null ? null : new String[] { acceptsType.toString() },
expectedValue);
}
protected Response doSensorTestUntyped(String displayHints,String raw, String[] acceptsTypes, Object expectedValue) {
WebClient req = client().path(SENSORS_ENDPOINT + "/" + SENSOR_NAME);
if (displayHints!=null) req = req.query("displayHints", displayHints);
if (raw!=null) req = req.query("raw", raw);
Response response;
if (acceptsTypes!=null) {
response = req.accept(acceptsTypes).get();
} else {
response = req.get();
}
if (expectedValue!=null) {
HttpAsserts.assertHealthyStatusCode(response.getStatus());
Object value = response.readEntity(expectedValue.getClass());
assertEquals(value, expectedValue);
}
return response;
}
/**
* Check we can get a sensor, explicitly requesting json; gives a string picking up the rendering hint.
*
* If no "Accepts" header is given, then we don't control whether json or plain text comes back.
* It is dependent on the method order, which is compiler-specific.
*/
@Test
public void testGetJson() throws Exception {
doSensorTest(null,null, MediaType.APPLICATION_JSON_TYPE, "\"12345 frogs\"");
doSensorTest(true,null, MediaType.APPLICATION_JSON_TYPE, "\"12345 frogs\"");
}
@Test
public void testGetJsonBytes() throws Exception {
Response response = doSensorTest(null,null, MediaType.APPLICATION_JSON_TYPE, null);
byte[] bytes = Streams.readFullyAndClose(response.readEntity(InputStream.class));
// assert we have one set of surrounding quotes
assertEquals(bytes.length, 13);
}
/** Check that plain returns a string without quotes, with the rendering hint */
@Test
public void testGetPlain() throws Exception {
doSensorTest(null,null, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
doSensorTest(true,null, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
}
/**
* Check that when we set {@code raw = true}, the result ignores the display value hint.
*
* If no "Accepts" header is given, then we don't control whether json or plain text comes back.
* It is dependent on the method order, which is compiler-specific.
*/
@Test
public void testGetRawJson() throws Exception {
doSensorTest(null,true, MediaType.APPLICATION_JSON_TYPE, 12345);
doSensorTest(false,true, MediaType.APPLICATION_JSON_TYPE, 12345);
doSensorTest(true,true, MediaType.APPLICATION_JSON_TYPE, 12345);
}
/** As {@link #testGetRawJson()} but with plain set, returns the number */
@Test
public void testGetPlainRaw() throws Exception {
// have to pass a string because that's how PLAIN is deserialized
doSensorTest(null,true, MediaType.TEXT_PLAIN_TYPE, "12345");
doSensorTest(false,true, MediaType.TEXT_PLAIN_TYPE, "12345");
doSensorTest(true,true, MediaType.TEXT_PLAIN_TYPE, "12345");
}
/** Check explicitly setting {@code raw} to {@code false} is as before */
@Test
public void testGetPlainRawFalse() throws Exception {
doSensorTest(null,false, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
doSensorTest(true,false, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
doSensorTest(false,false, MediaType.TEXT_PLAIN_TYPE, "12345 frogs");
}
/** Check empty vaue for {@code raw} will revert to using default. */
@Test
public void testGetPlainRawEmpty() throws Exception {
doSensorTestUntyped(null,"", new String[] { MediaType.TEXT_PLAIN }, "12345 frogs");
doSensorTestUntyped("","", new String[] { MediaType.TEXT_PLAIN }, "12345 frogs");
}
/** Check unparseable vaue for {@code raw} will revert to using default. */
@Test
public void testGetPlainRawError() throws Exception {
doSensorTestUntyped(null,"biscuits", new String[] { MediaType.TEXT_PLAIN }, "12345 frogs");
}
/** Check we can set a value */
@Test
public void testSet() throws Exception {
try {
Response response = client().path(SENSORS_ENDPOINT + "/" + SENSOR_NAME)
.type(MediaType.APPLICATION_JSON_TYPE)
.post(67890);
assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
assertEquals(entity.getAttribute(SENSOR), (Integer)67890);
String value = client().path(SENSORS_ENDPOINT + "/" + SENSOR_NAME).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
assertEquals(value, "67890 frogs");
} finally { addAmphibianSensor(entity); }
}
@Test
public void testSetFromMap() throws Exception {
try {
Response response = client().path(SENSORS_ENDPOINT)
.type(MediaType.APPLICATION_JSON_TYPE)
.post(MutableMap.of(SENSOR_NAME, 67890));
assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
assertEquals(entity.getAttribute(SENSOR), (Integer)67890);
} finally { addAmphibianSensor(entity); }
}
/** Check we can delete a value */
@Test
public void testDelete() throws Exception {
try {
Response response = client().path(SENSORS_ENDPOINT + "/" + SENSOR_NAME)
.delete();
assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
String value = client().path(SENSORS_ENDPOINT + "/" + SENSOR_NAME).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
assertEquals(value, "");
} finally { addAmphibianSensor(entity); }
}
@Test
public void testGetSensorValueOfTypeManagementContext() throws Exception {
entity.sensors().set(Sensors.newSensor(ManagementContext.class, "myMgmt"), getManagementContext());
doGetSensorTest("myMgmt", Map.class, ImmutableMap.of("type", ManagementContext.class.getName()));
}
@Test
public void testGetSensorValueOfTypeEntity() throws Exception {
// Note that non-raw will render it with just the entity's displayName
entity.sensors().set(Sensors.newSensor(Entity.class, "myEntity"), entity);
doGetSensorTest("myEntity", Map.class, ImmutableMap.of("type", Entity.class.getName(), "id", entity.getId()), true);
doGetSensorTest("myEntity", String.class, "\"simple-ent\"", false);
}
@Test
public void testGetSensorValueOfTypeLocation() throws Exception {
Location loc = getManagementContext().getLocationRegistry().getLocationManaged("localhost");
entity.sensors().set(Sensors.newSensor(Location.class, "myLocation"), loc);
doGetSensorTest("myLocation", Map.class, ImmutableMap.of("type", Location.class.getName(), "id", loc.getId()));
}
@Test
public void testGetSensorValueOfTypePolicy() throws Exception {
Policy policy = entity.policies().add(org.apache.brooklyn.api.policy.PolicySpec.create(TestPolicy.class));
entity.sensors().set(Sensors.newSensor(Policy.class, "myPolicy"), policy);
doGetSensorTest("myPolicy", Map.class, ImmutableMap.of("type", Policy.class.getName(), "id", policy.getId(), "entityId", entity.getId()));
}
@Test
public void testGetSensorValueOfTypeEnricher() throws Exception {
Enricher enricher = entity.enrichers().add(org.apache.brooklyn.api.sensor.EnricherSpec.create(TestEnricher.class));
entity.sensors().set(Sensors.newSensor(Enricher.class, "myEnricher"), enricher);
doGetSensorTest("myEnricher", Map.class, ImmutableMap.of("type", Enricher.class.getName(), "id", enricher.getId(), "entityId", entity.getId()));
}
@Test
public void testGetSensorValueOfTypeFeed() throws Exception {
Feed feed = entity.feeds().add(FunctionFeed.builder().entity(entity).build(true));
entity.sensors().set(Sensors.newSensor(Feed.class, "myFeed"), feed);
doGetSensorTest("myFeed", Map.class, ImmutableMap.of("type", Feed.class.getName(), "id", feed.getId(), "entityId", entity.getId()));
}
@Test
public void testGetSensorValueOfTypeCompletedTask() throws Exception {
Task<String> task = entity.getExecutionContext().submit("returning myval", Callables.returning("myval"));
task.get();
entity.sensors().set(Sensors.newSensor(Task.class, "myTask"), task);
doGetSensorTest("myTask", String.class, "\"myval\"");
}
@Test
public void testGetSensorValueOfTypeInprogressTask() throws Exception {
Task<Void> task = Tasks.<Void>builder().displayName("my-task-name").body(new SleepingCallable(Duration.THIRTY_SECONDS)).build();
entity.getExecutionContext().submit(task);
entity.sensors().set(Sensors.newSensor(Task.class, "myTask"), task);
doGetSensorTest("myTask", Map.class, ImmutableMap.of("type", Task.class.getName(), "id", task.getId(), "displayName", "my-task-name"));
}
@Test
public void testGetSensorValueOfTypeEffectorTask() throws Exception {
TestEntity testEntity = entity.addChild(org.apache.brooklyn.api.entity.EntitySpec.create(TestEntity.class));
Task<Void> task = testEntity.invoke(TestEntity.SLEEP_EFFECTOR, ImmutableMap.of("duration", Duration.THIRTY_SECONDS));
entity.sensors().set(Sensors.newSensor(Task.class, "myTask"), task);
doGetSensorTest("myTask", Map.class, ImmutableMap.of("type", Task.class.getName(), "id", task.getId(), "displayName", "sleepEffector"));
}
protected <T> void doGetSensorTest(String sensorName, Class<T> expectedType, T expectedVal) throws Exception {
doGetSensorTest(sensorName, expectedType, expectedVal, true);
doGetSensorTest(sensorName, expectedType, expectedVal, false);
}
protected <T> void doGetSensorTest(String sensorName, Class<T> expectedType, T expectedVal, boolean raw) throws Exception {
Response response = client().path(SENSORS_ENDPOINT + "/" + sensorName)
.query("raw", raw)
.accept(MediaType.APPLICATION_JSON_TYPE)
.get();
HttpAsserts.assertHealthyStatusCode(response.getStatus());
T value = response.readEntity(expectedType);
assertEquals(value, expectedVal);
}
public static class SleepingCallable implements Callable<Void> {
private final Duration duration;
SleepingCallable(Duration duration) {
this.duration = duration;
}
@Override
public Void call() throws Exception {
Time.sleep(duration);
return null;
}
}
@Test
public void testSecretSensorSuppressed() throws Exception {
Response response = client().path(SENSORS_ENDPOINT + "/current-state")
.query("raw", "true")
.query("suppressSecrets", "true")
.accept(MediaType.APPLICATION_JSON)
.get();
Map<String, ?> currentState = response.readEntity(new GenericType<Map<String,?>>(Map.class) {});
int found = 0;
for (String sensor : currentState.keySet()) {
if (sensor.equals(SECRET_SENSOR_NAME)) {
found++;
Asserts.assertStringDoesNotContain(""+currentState.get(sensor), ""+SECRET_VALUE);
}
}
Asserts.assertEquals(found, 1);
response = client().path(SENSORS_ENDPOINT + "/" + SECRET_SENSOR_NAME)
.query("suppressSecrets", "true")
.accept(MediaType.APPLICATION_JSON)
.get();
Asserts.assertStringDoesNotContain(""+response.readEntity(Object.class), ""+SECRET_VALUE);
response = client().path(SENSORS_ENDPOINT + "/" + SECRET_SENSOR_NAME)
.query("raw", "true")
.query("suppressSecrets", "true")
.accept(MediaType.APPLICATION_JSON)
.get();
Asserts.assertStringDoesNotContain(""+response.readEntity(Object.class), ""+SECRET_VALUE);
}
@Test
public void testSetFromMapPreservesMaps() throws Exception {
final AttributeSensor<Object> OBJ_SENSOR = Sensors.newSensor(Object.class, "obj_sensor");
final Runnable REMOVE = () -> entity.sensors().remove(OBJ_SENSOR);
try {
Response response = client().path(SENSORS_ENDPOINT)
.type(MediaType.APPLICATION_JSON_TYPE)
.post(MutableMap.of(OBJ_SENSOR.getName(), MutableMap.of("a", 1, "b", MutableMap.of("bb", "2"))));
assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
final Runnable CHECK = () -> {
// we don't want minidev JSON objects ending up in sensors; convert to normal maps
Map osm = (Map) entity.getAttribute(OBJ_SENSOR);
assertEquals(osm.get("a"), 1);
Asserts.assertFalse(osm instanceof JSONObject);
Map osmb = (Map) osm.get("b");
assertEquals(osmb.get("bb"), "2");
Asserts.assertFalse(osmb instanceof JSONObject);
};
CHECK.run();
REMOVE.run();
// and should be similar when posting to the endpont
response = client().path(SENSORS_ENDPOINT+"/"+OBJ_SENSOR.getName())
.type(MediaType.APPLICATION_JSON_TYPE)
.post(MutableMap.of("a", 1, "b", MutableMap.of("bb", "2")));
assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode());
CHECK.run();
} finally { REMOVE.run(); }
}
}