diff --git a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java index 780729bf5a1effb97371964883d3e9356297544a..c45f02562fa72036b24749ab4fdca47fbf87ed41 100644 --- a/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java +++ b/eraser-base/src/main/java/de/tudresden/inf/st/eraser/util/ParserUtils.java @@ -24,6 +24,7 @@ import java.util.Objects; */ public class ParserUtils { + public static final String UNKNOWN_GROUP_NAME = "Unknown"; private static boolean verboseLoading = false; private static final Logger logger = LogManager.getLogger(ParserUtils.class); @@ -172,7 +173,7 @@ public class ParserUtils { */ public static void createUnknownGroup(OpenHAB2Model model, Collection<Item> danglingItems) { Group unknownGroup = new Group(); - unknownGroup.setID("Unknown"); + unknownGroup.setID(UNKNOWN_GROUP_NAME); model.addGroup(unknownGroup); danglingItems.forEach(unknownGroup::addItem); logger.info("Created new {}", unknownGroup.prettyPrint().trim()); @@ -234,4 +235,21 @@ public class ParserUtils { return result; } + public static Item parseItem(String definition) + throws IllegalArgumentException, IOException, Parser.Exception { + StringReader reader = new StringReader(definition); + EraserScanner scanner = new EraserScanner(reader); + EraserParser parser = new EraserParser(); + Root root = (Root) parser.parse(scanner); + reader.close(); + int size = root.getOpenHAB2Model().items().size(); + if (size == 0) { + throw new IllegalArgumentException("Model does not contain any items!"); + } + if (size > 1) { + logger.warn("Model does contain {} items, ignoring all but the first.", size); + } + return root.getOpenHAB2Model().items().get(0); + } + } diff --git a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java index ae15f566876bb233706d34a9b563fa85af195244..c1c8c2f40e4a3847e4dbfa7ea4b95a406df4d1d2 100644 --- a/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java +++ b/eraser.spark/src/main/java/de/tudresden/inf/st/eraser/spark/Application.java @@ -3,12 +3,15 @@ package de.tudresden.inf.st.eraser.spark; import com.fasterxml.jackson.databind.ObjectMapper; import de.tudresden.inf.st.eraser.jastadd.model.*; import de.tudresden.inf.st.eraser.util.JavaUtils; +import de.tudresden.inf.st.eraser.util.ParserUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.http.HttpStatus; import spark.Request; import spark.Response; import spark.Spark; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,14 +44,20 @@ public class Application { Spark.path("/", () -> Spark.before((request, response) -> logger.debug(request.pathInfo()))); Spark.path("/activity", () -> { + + //--- GET /activity --- Spark.get("", (request, response) -> wrapActivityList(root.getMachineLearningRoot().getActivityList()), mapper::writeValueAsString); + + //--- GET /activity/current --- Spark.get("/current", (request, response) -> JavaUtils.ifPresentOrElseReturn(root.currentActivity(), this::wrapActivity, () -> makeError(response, 204, "No activity recognized.")), mapper::writeValueAsString); + + //--- PUT /activity/current --- Spark.put("/current", (request, response) -> { logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength()); if (!root.getMachineLearningRoot().hasActivityRecognition()) { @@ -62,6 +71,8 @@ public class Application { return makeError(response, 501, "Activity not editable for " + activityRecognition.getClass().getSimpleName()); } }); + + //--- GET /activity/:identifier --- Spark.get("/:identifier", (request, response) -> JavaUtils.ifPresentOrElseReturn(root.resolveActivity(paramAsInt(request, "identifier")), @@ -71,9 +82,13 @@ public class Application { }); Spark.path("/events", () -> { + + //--- GET /events --- Spark.get("", (request, response) -> wrapChangeEventList(root.getMachineLearningRoot().getChangeEventList()), mapper::writeValueAsString); + + //--- GET /events/:identifier --- Spark.get("/:identifier", (request, response) -> JavaUtils.ifPresentOrElseReturn(root.resolveChangeEvent(paramAsInt(request, "identifier")), @@ -83,33 +98,63 @@ public class Application { }); Spark.path("/model", () -> { + + //--- GET /model/full --- Spark.get("/full", (request, response) -> { response.type("text/plain"); return root.prettyPrint(); }); - Spark.get("/items", - (request, response) -> wrapItemList(root.getOpenHAB2Model().items()), - mapper::writeValueAsString); - Spark.put("/items/:identifier/state", (request, response) -> { - logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength()); - return safeItemRoute(request, response, - item -> { - try { - item.setStateFromString(request.body()); - return "OK"; - } catch (Exception e) { - logger.catching(e); - return makeError(response, 500, e.getMessage()); - } - }); - }); - Spark.get("/items/:identifier/history", - (request, response) -> { + Spark.path("/items", () -> { + + //--- GET /model/items --- + Spark.get("", + (request, response) -> wrapItemList(root.getOpenHAB2Model().items()), + mapper::writeValueAsString); + Spark.path("/:identifier", () -> { + + Spark.put("", (request, response) -> { + OpenHAB2Model openHAB2Model = root.getOpenHAB2Model(); + Item item = ParserUtils.parseItem(request.body()); + if (!openHAB2Model.resolveItem(item.getID()).isPresent()) { + JavaUtils.ifPresentOrElse( + root.getOpenHAB2Model().resolveGroup(ParserUtils.UNKNOWN_GROUP_NAME), + group -> group.addItem(item), + () -> ParserUtils.createUnknownGroup(root.getOpenHAB2Model(), Collections.singletonList(item))); + response.status(201); + } + return "OK"; + }); + + //--- GET /model/items/:identifier/state --- + Spark.get("/state", (request, response) -> + safeItemRoute(request, response, Item::getStateAsString)); + + //--- PUT /model/items/:identifier/state --- + Spark.put("/state", (request, response) -> { logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength()); - return safeItemRoute(request, response, item -> makeHistory(item, response)); + return safeItemRoute(request, response, + item -> { + try { + item.setStateFromString(request.body()); + return "OK"; + } catch (Exception e) { + logger.catching(e); + return makeError(response, 500, e.getMessage()); + } + }); }); + }); + + //--- GET /model/items/:identifier/history --- + Spark.get("/:identifier/history", + (request, response) -> { + logger.info("request body: '{}', params: '{}', length={}", request.body(), request.params(), request.contentLength()); + return safeItemRoute(request, response, item -> makeHistory(item, response)); + }); + }); }); + //--- POST /system/exit --- Spark.post("/system/exit", (request, response) -> { try { lock.lock(); diff --git a/integration_test/.gitignore b/integration_test/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..54c5c0b024dab04da2a43d4135ed67ed2a670b46 --- /dev/null +++ b/integration_test/.gitignore @@ -0,0 +1,2 @@ +/build/ +logs/ diff --git a/integration_test/build.gradle b/integration_test/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..2d905f3f6d29a82267538d94376175d6556787d7 --- /dev/null +++ b/integration_test/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'application' + +dependencies { + testCompile project(':eraser-base') + testCompile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.8' + testCompile group: 'org.apache.httpcomponents', name: 'fluent-hc', version: '4.5.8' + testCompile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.8' +} + +run { + mainClassName = 'de.tudresden.inf.st.eraser.integration_test.Main' + standardInput = System.in + if (project.hasProperty("appArgs")) { + args Eval.me(appArgs) + } +} + +sourceSets { + main { + java { + srcDir 'src/main/java' + } + } +} diff --git a/integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java b/integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..c362161e99ef67c70a79278d93263cce1a91eb2f --- /dev/null +++ b/integration_test/src/main/java/de/tudresden/inf/st/eraser/integration_test/Main.java @@ -0,0 +1,8 @@ +package de.tudresden.inf.st.eraser.integration_test; + +public class Main { + + public static void main(String[] args) { + System.out.println("Hello World!"); + } +} diff --git a/integration_test/src/main/resources/log4j2.xml b/integration_test/src/main/resources/log4j2.xml new file mode 100644 index 0000000000000000000000000000000000000000..867ec439d0a32dcb5f8b3e2d0c7485d7d8da418c --- /dev/null +++ b/integration_test/src/main/resources/log4j2.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Configuration> + <Appenders> + <Console name="Console"> + <PatternLayout pattern="%highlight{%d{HH:mm:ss.SSS} %-5level} %c{1.} - %msg%n"/> + </Console> + <RollingFile name="RollingFile" fileName="logs/eraser.log" + filePattern="logs/eraser-%i.log"> + <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/> + <Policies> + <OnStartupTriggeringPolicy/> + </Policies> + <DefaultRolloverStrategy max="20"/> + </RollingFile> + </Appenders> + <Loggers> + <Root level="debug"> + <AppenderRef ref="Console"/> + <AppenderRef ref="RollingFile"/> + </Root> + </Loggers> +</Configuration> diff --git a/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java b/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a1aff8c0ae416aba4f0f1c96801e12445c666076 --- /dev/null +++ b/integration_test/src/test/java/de/tudresden/inf/st/eraser/integration_test/ItemTest.java @@ -0,0 +1,148 @@ +package de.tudresden.inf.st.eraser.integration_test; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.tudresden.inf.st.eraser.openhab2.data.ItemData; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.fluent.Form; +import org.apache.http.client.fluent.Request; +import org.apache.http.entity.ContentType; +import org.apache.http.util.EntityUtils; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.Arrays; +import java.util.function.Function; + +import static org.hamcrest.Matchers.either; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Integration test to check openHAB-binding for eraser works well together with eraser itself. + * + * @author rschoene - Initial contribution + */ +@RunWith(Parameterized.class) +public class ItemTest { + + private static final String OPENHAB_ITEM_URI = "http://localhost:8080/rest/items/"; + private static final String ERASER_ITEM_URI = "http://localhost:4567/model/items/"; + + private static final String DIMMER_ITEM = "dimmer_item"; + private static final String NUMBER_ITEM = "number_item"; + private static final String SWITCH_ITEM = "switch_item"; + private static final String COLOR_ITEM = "color_item"; + + @BeforeClass + public static void ensureItemsAreCreated() throws IOException { + ensureItemCreated(DIMMER_ITEM, "Dimmer"); + ensureItemCreated(NUMBER_ITEM, "Number"); + ensureItemCreated(SWITCH_ITEM, "Switch"); + ensureItemCreated(COLOR_ITEM, "Color"); + } + + private static void ensureItemCreated(String name, String type) throws IOException { + // Create at openHAB + HttpResponse responseOpenHAB = Request.Put(OPENHAB_ITEM_URI + name) + .bodyForm(Form.form().add("type", type).add("name", name).build()) + .execute().returnResponse(); + assertThat( + responseOpenHAB.getStatusLine().getStatusCode(), + either(equalTo(HttpStatus.SC_CREATED)) + .or(equalTo(HttpStatus.SC_OK))); + + // Create at eraser + HttpResponse responseEraser = Request.Put(ERASER_ITEM_URI + name) + .bodyString(type + " Item: id=\"" + name + "\"", ContentType.TEXT_PLAIN) + .execute().returnResponse(); + assertThat( + responseEraser.getStatusLine().getStatusCode(), + equalTo(HttpStatus.SC_CREATED)); + } + + @Test + public void itemAvailable() throws IOException { + String name = "Tradfri_2_small_tv"; + ItemData itemData = Request.Get( OPENHAB_ITEM_URI + name ) + .execute().handleResponse( + response -> retrieveResourceFromResponse(response, ItemData.class)); + assertThat(itemData.type, equalTo("Dimmer")); + } + + @Test + public void dimmerSetStateAtOpenHAB() throws IOException { + String name = "Tradfri_2_small_tv"; + double newValue = 3.0; + // set item state with openHAB REST API + HttpResponse httpResponse = Request.Put(OPENHAB_ITEM_URI + name + "/state") + .bodyString(Double.toString(newValue), ContentType.TEXT_PLAIN) + .execute().returnResponse(); + assertThat( + httpResponse.getStatusLine().getStatusCode(), + equalTo(HttpStatus.SC_ACCEPTED)); + + // check whether state was set correctly + String responseOpenHAB = Request.Get(OPENHAB_ITEM_URI + name + "/state") + .execute().returnContent().asString(); + assertThat(Double.parseDouble(responseOpenHAB), equalTo(newValue)); + + // check whether state was updated on eraser side + String responseEraser = Request.Get(ERASER_ITEM_URI + name + "/state") + .execute().returnContent().asString(); + assertThat(Double.parseDouble(responseEraser), equalTo(newValue)); + } + + @Test + public void dimmerSetStateAtEraser() throws IOException { + String name = "Tradfri_2_small_tv"; + double newValue = 25.0; + // set item state with eraser REST API + String uri = ERASER_ITEM_URI + name + "/state"; + System.out.println(uri); + HttpResponse httpResponse = Request.Put(uri) + .bodyString(Double.toString(newValue), ContentType.TEXT_PLAIN) + .execute().returnResponse(); + assertThat( + httpResponse.getStatusLine().getStatusCode(), + equalTo(HttpStatus.SC_OK)); + + // check whether state was set correctly + String responseEraser = Request.Get(ERASER_ITEM_URI + name + "/state") + .execute().returnContent().asString(); + assertThat(Double.parseDouble(responseEraser), equalTo(newValue)); + + // check whether state was updated on openHAB side + String responseOpenHAB = Request.Get(OPENHAB_ITEM_URI + name + "/state") + .execute().returnContent().asString(); + assertThat(Double.parseDouble(responseOpenHAB), equalTo(newValue)); + } + + private static <T> T retrieveResourceFromResponse(HttpResponse response, Class<T> clazz) + throws IOException { + String jsonFromResponse = EntityUtils.toString(response.getEntity()); + ObjectMapper mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return mapper.readValue(jsonFromResponse, clazz); + } + + private static <T> Object[] d(String name, T initialValue, Function<T, String> toString, + Function<String, T> fromString) { + return new Object[]{name, initialValue, toString, fromString}; + } + + @Parameterized.Parameters(name= "{index}: {0}") + public static Iterable<Object[]> data() { + return Arrays.asList( + d(DIMMER_ITEM, 25.0, d -> Double.toString(d), Double::parseDouble), + d(NUMBER_ITEM, 4, i -> Integer.toString(i), Integer::parseInt), + d(SWITCH_ITEM, "ON", Function.identity(), Function.identity()), + d(COLOR_ITEM, "1,2,3", Function.identity(), Function.identity()) + ); + } + +} diff --git a/settings.gradle b/settings.gradle index 44b66ee077cceb9a26f2cfed97b319d4a30741a3..4492d856ad2d58bf8a3fe0eedca93b30b9c05bb2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,3 +19,4 @@ include ':feedbackloop.learner' include ':influx_test' include ':eraser.spark' include ':eraser.starter' +include ':integration_test'