diff --git a/CODEOWNERS b/CODEOWNERS index 6589d06defe52..e45bb93ced9df 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,6 +60,7 @@ /bundles/org.openhab.binding.dali/ @rs22 /bundles/org.openhab.binding.danfossairunit/ @pravussum /bundles/org.openhab.binding.darksky/ @cweitkamp +/bundles/org.openhab.binding.dbquery/ @lujop /bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.denonmarantz/ @jwveldhuis /bundles/org.openhab.binding.digiplex/ @rmichalak diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 54c6f8c3dcdb3..bb8833638bc2a 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -286,6 +286,11 @@ org.openhab.binding.darksky ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.dbquery + ${project.version} + org.openhab.addons.bundles org.openhab.binding.deconz diff --git a/bundles/org.openhab.binding.dbquery/README.md b/bundles/org.openhab.binding.dbquery/README.md new file mode 100644 index 0000000000000..787bf06ac752f --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/README.md @@ -0,0 +1,210 @@ +# DBQuery Binding + +This binding allows creating items from the result of native database queries. +It currently only supports InfluxDB 2.X. + +You can use the addon in any situation where you want to create an item from a native query. +The source of the query can be any supported database, and doesn't need to be the one you use as the persistence service in openHAB. +Some use cases can be: + +- Integrate a device that stores its data in a database +- Query derived data from you openHAB persistence, for example with Influx2 tasks you can process your data to create a new one +- Bypass limitations of current openHAB persistence queries + +## Supported Things + +There are two types of supported things: `influxdb2` and a `query`. +For each different database you want to connect to, you must define a `Bridge` thing for that database. +Then each `Bridge` can define as many `Query` things that you want to execute. + +## Thing Configuration + +### Bridges + +#### influxdb2 + +Defines a connection to an Influx2 database and allows creating queries on it. + +| Parameter | Required | Description | +|--------------|----------|----------------------------------------- | +| url | Yes | database url | +| user | Yes | name of the database user | +| token | Yes | token to authenticate to the database ([Intructions about how to create one](https://v2.docs.influxdata.com/v2.0/security/tokens/create-token/)) | +| organization | Yes | database organization name | +| bucket | Yes | database bucket name | + +### query + +The `Query` thing defines a native query that provides several channels that you can bind to items. + +#### Query parameters + +The query items support the following parameters: + +| Parameter | Required | Default | Description | +|--------------|----------|----------|-----------------------------------------------------------------------| +| query | true | | Query string in native syntax | +| interval | false | 0 | Interval in seconds in which the query is automatically executed | +| hasParameters| false | false | True if the query has parameters, false otherwise | +| timeout | false | 0 | Query execution timeout in seconds | +| scalarResult | false | true | If query always returns a single value or not | +| scalarColumn | false | | In case of multiple columns, it indicates which to use for scalarResult| + +These are described further in the following subsections. + +##### query + +The query the items represents in the native language of your database: + + - Flux for `influxdb2` + +#### hasParameters + +If `hasParameters=true` you can use parameters in the query string that can be dynamically set with the `setQueryParameters` action. + + For InfluxDB use the `${paramName}` syntax for each parameter, and keep in mind that the values from that parameters must be from a trusted source as current + parameter substitution is subject to query injection attacks. + +#### timeout + +A time-out in seconds to wait for the query result, if it's exceeded, the result will be discarded and the addon will do its best to cancel the query. +Currently it's ignored and it will be implemented in a future version. + +#### scalarResult + +If `true` the query is expected to return a single scalar value that will be available to `result` channels as string, number, boolean,... +If the query can return several rows and/or several columns per row then it needs to be set to `false` and the result can be retrieved in `resultString` +channel as JSON or using the `getLastQueryResult` action. + +#### scalarColumn + +In case `scalarResult` is `true` and the select returns multiple columns you can use that parameter to choose which column to use to extract the result. + +## Channels + +Query items offer the following channels to be able to query / bind them to items: + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|------------------------------------------------------------------------------------------------------------------------------------| +| execute | Switch | Send `ON` to execute the query manually. It also indicates if query is currently running (`ON`) or not running (`OFF`) | +| resultString | String | Result of last executed query as a String | +| resultNumber | Number | Result of last executed query as a Number, query must have `scalarResult=true` | +| resultDateTime | DateTime | Result of last executed query as a DateTime, query must have `scalarResult=true` | +| resultContact | Contact | Result of last executed query as Contact, query must have `scalarResult=true` | +| resultSwitch | Switch | Result of last executed query as Switch, query must have `scalarResult=true` | +| parameters | String | Contains parameters of last executed query as JSON| +| correct | Switch | `ON` if the last executed query completed successfully, `OFF` if the query failed.| + +All the channels, except `execute`, are updated when the query execution finishes, and while there is a query in execution they have the values from +last previous executed query. + +The `resultString` channel is the only valid one if `scalarResult=false`, and in that case it contains the query result serialized to JSON in that format: + + { + correct : true, + data : [ + { + column1 : value, + column2 : value + }, + { ... }, //row2 + { ... } //row3 + ] + } + +### Channel Triggers + +#### calculateParameters + +Triggers when there's a need to calculate parameters before query execution. +When a query has `hasParameters=true` it fires the `calculateParameters` channel trigger and pauses the execution until `setQueryParameters` action is call in + that query. + +In the case a query has parameters, it's expected that there is a rule that catches the `calculateParameters` trigger, calculate the parameters with the corresponding logic and then calls the `setQueryParameters` action, after that the query will be executed. + +## Actions + +### For DatabaseBridge + +#### executeQuery + +It allows executing a query synchronously from a script/rule without defining it in a Thing. + +To execute the action you need to pass the following parameters: + +- String query: The query to execute +- Map: Query parameters (empty map if not needed) +- int timeout: Query timeout in seconds + +And it returns an `ActionQueryResult` that has the following properties: + +- correct (boolean) : True if the query was executed correctly, false otherwise +- data (List>): A list where each element is a row that is stored in a map with (columnName,value) entries +- isScalarResult: It returns if the result is scalar one (only one row with one column) +- resultAsScalar: It returns the result as a scalar if possible, if not returns null + + +Example (using Jython script): + + from core.log import logging, LOG_PREFIX + log = logging.getLogger("{}.action_example".format(LOG_PREFIX)) + map = {"time" : "-2h"} + influxdb = actions.get("dbquery","dbquery:influxdb2:sampleQuery") //Get bridge thing + result = influxdb.executeQuery("from(bucket: \"default\") |> range(start:-2h) |> filter(fn: (r) => r[\"_measurement\"] == \"go_memstats_frees_total\") |> filter(fn: (r) => r[\"_field\"] == \"counter\") |> mean()",{},5) + log.info("execute query result is "+str(result.data)) + + +Use this action with care, because as the query is executed synchronously, it is not good to execute long-running queries that can block script execution. + +### For Queries + +#### setQueryParameters + +It's used for queries with parameters to set them. +To execute the action you need to pass the parameters as a Map. + +Example (using Jython script): + + params = {"time" : "-2h"} + dbquery = actions.get("dbquery","dbquery:query:queryWithParams") //Get query thing + dbquery.setQueryParameters(params) + +#### getLastQueryResult + +It can be used in scripts to get the last query result. +It doesn't have any parameters and returns an `ActionQueryResult` as defined in `executeQuery` action. + +Example (using Jython script): + + dbquery = actions.get("dbquery","dbquery:query:queryWithParams") //Get query thing + result = dbquery.getLastQueryResult() + + +## Examples + +### The Simplest case + +Define a InfluxDB2 database thing and a query with an interval execution. +That executes the query every 15 seconds and punts the result in `myItem`. + + # Bridge Thing definition + Bridge dbquery:influxdb2:mydatabase "InfluxDB2 Bridge" [ bucket="default", user="admin", url="http://localhost:8086", organization="openhab", token="*******" ] + + # Query Thing definition + Thing dbquery:query:myquery "My Query" [ interval=15, hasParameters=false, scalarResult=true, timeout=0, query="from(bucket: \"default\") |> range(start:-1h) |> filter(fn: (r) => r[\"_measurement\"] == \"go_memstats_frees_total\") |> filter(fn: (r) => r[\"_field\"] == \"counter\") |> mean()", scalarColumn="_value" ] + + # Item definition + Number myItem "QueryResult" {channel="dbquery:query:myquery:resultNumber"} + +### A query with parameters + +Using the previous example you change the `range(start:-1h)` for `range(start:${time})` + +Create a rule that is fired + + - **When** `calculateParameters` is triggered in `myquery` + - **Then** executes the following script action (in that example Jython): + + map = {"time" : "-2h"} + dbquery = actions.get("dbquery","dbquery:query:myquery") + dbquery.setQueryParameters(map) diff --git a/bundles/org.openhab.binding.dbquery/pom.xml b/bundles/org.openhab.binding.dbquery/pom.xml new file mode 100644 index 0000000000000..0ac0787c13860 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/pom.xml @@ -0,0 +1,107 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.binding.dbquery + + openHAB Add-ons :: Bundles :: DBQuery Binding + + + + !javax.annotation;!android.*,!com.android.*,!com.google.appengine.*,!dalvik.system,!kotlin.*,!kotlinx.*,!org.conscrypt,!sun.security.ssl,!org.apache.harmony.*,!org.apache.http.*,!rx.*,!org.msgpack.* + + + + + + + com.influxdb + influxdb-client-java + 1.6.0 + + + influxdb-client-core + com.influxdb + 1.6.0 + + + converter-gson + com.squareup.retrofit2 + 2.5.0 + + + converter-scalars + com.squareup.retrofit2 + 2.5.0 + + + gson + com.google.code.gson + 2.8.5 + + + gson-fire + io.gsonfire + 1.8.0 + + + okio + com.squareup.okio + 1.17.3 + + + commons-csv + org.apache.commons + 1.6 + + + json + org.json + 20180813 + + + okhttp + com.squareup.okhttp3 + 3.14.4 + + + retrofit + com.squareup.retrofit2 + 2.6.2 + + + jsr305 + com.google.code.findbugs + 3.0.2 + + + logging-interceptor + com.squareup.okhttp3 + 3.14.4 + + + rxjava + io.reactivex.rxjava2 + 2.2.17 + + + reactive-streams + org.reactivestreams + 1.0.3 + + + swagger-annotations + io.swagger + 1.5.22 + + + + diff --git a/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml b/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml new file mode 100644 index 0000000000000..23cd424abe4be --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.dbquery/${project.version} + + diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java new file mode 100644 index 0000000000000..0973e6ba41dda --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.action; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Query result as it's exposed to users in thing actions + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class ActionQueryResult { + private final boolean correct; + private List> data = Collections.emptyList(); + + public ActionQueryResult(boolean correct, @Nullable List> data) { + this.correct = correct; + if (data != null) { + this.data = data; + } + } + + public boolean isCorrect() { + return correct; + } + + public List> getData() { + return data; + } + + public @Nullable Object getResultAsScalar() { + var firstResult = data.get(0); + return isScalarResult() ? firstResult.get(firstResult.keySet().iterator().next()) : null; + } + + public boolean isScalarResult() { + return data.size() == 1 && data.get(0).keySet().size() == 1; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java new file mode 100644 index 0000000000000..958c4d27eceed --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.action; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.DatabaseBridgeHandler; +import org.openhab.binding.dbquery.internal.QueryHandler; +import org.openhab.binding.dbquery.internal.domain.ExecuteNonConfiguredQuery; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.domain.ResultRow; +import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition; +import org.openhab.core.automation.annotation.ActionInput; +import org.openhab.core.automation.annotation.RuleAction; +import org.openhab.core.thing.binding.ThingActions; +import org.openhab.core.thing.binding.ThingActionsScope; +import org.openhab.core.thing.binding.ThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Joan Pujol - Initial contribution + */ +@ThingActionsScope(name = "dbquery") +@NonNullByDefault +public class DBQueryActions implements IDBQueryActions, ThingActions { + private final Logger logger = LoggerFactory.getLogger(DBQueryActions.class); + + private @Nullable QueryHandler queryHandler; + private @Nullable DatabaseBridgeHandler databaseBridgeHandler; + + @Override + @RuleAction(label = "Execute query", description = "Execute query synchronously (use with care)") + public ActionQueryResult executeQuery(String query, Map parameters, + int timeoutInSeconds) { + logger.debug("executeQuery from action {} params={}", query, parameters); + var currentDatabaseBridgeHandler = databaseBridgeHandler; + if (currentDatabaseBridgeHandler != null) { + QueryResult queryResult = new ExecuteNonConfiguredQuery(currentDatabaseBridgeHandler.getDatabase()) + .executeSynchronously(query, parameters, Duration.ofSeconds(timeoutInSeconds)); + logger.debug("executeQuery from action result {}", queryResult); + return queryResult2ActionQueryResult(queryResult); + } else { + logger.warn("Execute queried ignored as databaseBridgeHandler is null"); + return new ActionQueryResult(false, null); + } + } + + private ActionQueryResult queryResult2ActionQueryResult(QueryResult queryResult) { + return new ActionQueryResult(queryResult.isCorrect(), + queryResult.getData().stream().map(DBQueryActions::resultRow2Map).collect(Collectors.toList())); + } + + private static Map resultRow2Map(ResultRow resultRow) { + Map map = new HashMap<>(); + for (String column : resultRow.getColumnNames()) { + map.put(column, resultRow.getValue(column)); + } + return map; + } + + @Override + @RuleAction(label = "Set query parameters", description = "Set query parameters for a query") + public void setQueryParameters(@ActionInput(name = "parameters") Map parameters) { + logger.debug("setQueryParameters {}", parameters); + var queryHandler = getThingHandler(); + if (queryHandler instanceof QueryHandler) { + ((QueryHandler) queryHandler).setParameters(parameters); + } else { + logger.warn("setQueryParameters called on wrong Thing, it must be a Query Thing"); + } + } + + @Override + @RuleAction(label = "Get last query result", description = "Get last result from a query") + public ActionQueryResult getLastQueryResult() { + var currentQueryHandler = queryHandler; + if (currentQueryHandler != null) { + return queryResult2ActionQueryResult(queryHandler.getLastQueryResult()); + } else { + logger.warn("getLastQueryResult ignored as queryHandler is null"); + return new ActionQueryResult(false, null); + } + } + + @Override + public void setThingHandler(ThingHandler thingHandler) { + if (thingHandler instanceof QueryHandler) { + this.queryHandler = ((QueryHandler) thingHandler); + } else if (thingHandler instanceof DatabaseBridgeHandler) { + this.databaseBridgeHandler = ((DatabaseBridgeHandler) thingHandler); + } else { + throw new UnnexpectedCondition("Not expected thing handler " + thingHandler); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return queryHandler != null ? queryHandler : databaseBridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java new file mode 100644 index 0000000000000..4a2fa70cc55cf --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.action; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Defines rule actions for interacting with DBQuery addon Things. + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface IDBQueryActions { + ActionQueryResult executeQuery(String query, Map parameters, int timeoutInSeconds); + + ActionQueryResult getLastQueryResult(); + + void setQueryParameters(Map parameters); +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java new file mode 100644 index 0000000000000..cbdb14efb3758 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Channel; +import org.openhab.core.types.State; + +/** + * Abstract the operation to update a channel + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface ChannelStateUpdater { + void updateChannelState(Channel channelUID, State value); +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java new file mode 100644 index 0000000000000..68037abe42d2a --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Channel; + +/** + * Abstract the action to get channels that need to be updated + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface ChannelsToUpdateQueryResult { + List getChannels(); +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java new file mode 100644 index 0000000000000..a497b477d7997 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Common constants, which are used across the whole binding. + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class DBQueryBindingConstants { + + private static final String BINDING_ID = "dbquery"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_INFLUXDB2_BRIDGE = new ThingTypeUID(BINDING_ID, "influxdb2"); + public static final ThingTypeUID THING_TYPE_QUERY = new ThingTypeUID(BINDING_ID, "query"); + + // List of all Channel ids + public static final String CHANNEL_EXECUTE = "execute"; + + public static final String CHANNEL_PARAMETERS = "parameters"; + public static final String CHANNEL_CORRECT = "correct"; + public static final String TRIGGER_CHANNEL_CALCULATE_PARAMETERS = "calculateParameters"; + + public static final String RESULT_STRING_CHANNEL_TYPE = "result-channel-string"; + public static final String RESULT_NUMBER_CHANNEL_TYPE = "result-channel-number"; + public static final String RESULT_DATETIME_CHANNEL_TYPE = "result-channel-datetime"; + public static final String RESULT_CONTACT_CHANNEL_TYPE = "result-channel-contact"; + public static final String RESULT_SWITCH_CHANNEL_TYPE = "result-channel-switch"; + + private DBQueryBindingConstants() { + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java new file mode 100644 index 0000000000000..5495dd34b4cf2 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.THING_TYPE_INFLUXDB2_BRIDGE; +import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.THING_TYPE_QUERY; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * DBQuery binding factory that is responsible for creating things and thing handlers. + * + * @author Joan Pujol Espinar - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.dbquery", service = ThingHandlerFactory.class) +public class DBQueryHandlerFactory extends BaseThingHandlerFactory { + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INFLUXDB2_BRIDGE, + THING_TYPE_QUERY); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_QUERY.equals(thingTypeUID)) { + return new QueryHandler(thing); + } else if (THING_TYPE_INFLUXDB2_BRIDGE.equals(thingTypeUID)) { + return new InfluxDB2BridgeHandler((Bridge) thing); + } else { + return null; + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java new file mode 100644 index 0000000000000..c2ffba22ee5ae --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.action.DBQueryActions; +import org.openhab.binding.dbquery.internal.domain.Database; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Base implementation common to all implementation of database bridge + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public abstract class DatabaseBridgeHandler extends BaseBridgeHandler { + private static final long RETRY_CONNECTION_ATTEMPT_TIME_SECONDS = 60; + private final Logger logger = LoggerFactory.getLogger(DatabaseBridgeHandler.class); + private Database database = Database.EMPTY; + private @Nullable ScheduledFuture retryConnectionAttemptFuture; + + public DatabaseBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void initialize() { + initConfig(); + + database = createDatabase(); + + connectDatabase(); + } + + private void connectDatabase() { + logger.debug("connectDatabase {}", database); + var completable = database.connect(); + updateStatus(ThingStatus.UNKNOWN); + completable.thenAccept(result -> { + if (result) { + logger.trace("Succesfully connected to database {}", getThing().getUID()); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connect to database failed"); + if (retryConnectionAttemptFuture == null) { + scheduleRetryConnectionAttempt(); + } + } + }); + } + + protected void scheduleRetryConnectionAttempt() { + logger.trace("Scheduled retry connection attempt every {}", RETRY_CONNECTION_ATTEMPT_TIME_SECONDS); + retryConnectionAttemptFuture = scheduler.scheduleWithFixedDelay(this::connectDatabase, + RETRY_CONNECTION_ATTEMPT_TIME_SECONDS, RETRY_CONNECTION_ATTEMPT_TIME_SECONDS, TimeUnit.SECONDS); + } + + protected abstract void initConfig(); + + @Override + public void dispose() { + cancelRetryConnectionAttemptIfPresent(); + disconnectDatabase(); + } + + protected void cancelRetryConnectionAttemptIfPresent() { + ScheduledFuture currentFuture = retryConnectionAttemptFuture; + if (currentFuture != null) { + currentFuture.cancel(true); + } + } + + private void disconnectDatabase() { + var completable = database.disconnect(); + updateStatus(ThingStatus.UNKNOWN); + completable.thenAccept(result -> { + if (result) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Successfully disconnected to database"); + } else { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR, + "Disconnect to database failed"); + } + }); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // No commands supported + } + + abstract Database createDatabase(); + + public Database getDatabase() { + return database; + } + + @Override + public Collection> getServices() { + return List.of(DBQueryActions.class); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java new file mode 100644 index 0000000000000..984293ca0d073 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration; +import org.openhab.binding.dbquery.internal.dbimpl.influx2.Influx2Database; +import org.openhab.binding.dbquery.internal.dbimpl.influx2.InfluxDBClientFacadeImpl; +import org.openhab.binding.dbquery.internal.domain.Database; +import org.openhab.core.thing.Bridge; + +/** + * Concrete implementation of {@link DatabaseBridgeHandler} for Influx2 + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class InfluxDB2BridgeHandler extends DatabaseBridgeHandler { + private InfluxDB2BridgeConfiguration config = new InfluxDB2BridgeConfiguration(); + + public InfluxDB2BridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + Database createDatabase() { + return new Influx2Database(config, new InfluxDBClientFacadeImpl(config)); + } + + @Override + protected void initConfig() { + config = getConfig().as(InfluxDB2BridgeConfiguration.class); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java new file mode 100644 index 0000000000000..58a741a03e9dd --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; + +/** + * Concrete implementation of {@link DatabaseBridgeHandler} for Influx2 + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class JDBCBridgeHandler extends BaseBridgeHandler { + public JDBCBridgeHandler(Bridge bridge) { + super(bridge); + } + + @Override + public void initialize() { + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java new file mode 100644 index 0000000000000..63fcde42fa1c1 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.openhab.binding.dbquery.internal.domain.Database; +import org.openhab.binding.dbquery.internal.domain.Query; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Mantains information of a query that is currently executing + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryExecution { + private final Logger logger = LoggerFactory.getLogger(QueryExecution.class); + private final Database database; + private final String queryString; + private final QueryConfiguration queryConfiguration; + + private QueryParameters queryParameters; + private @Nullable QueryResultListener queryResultListener; + + public QueryExecution(Database database, QueryConfiguration queryConfiguration, + QueryResultListener queryResultListener) { + this.database = database; + this.queryString = queryConfiguration.getQuery(); + this.queryConfiguration = queryConfiguration; + this.queryResultListener = queryResultListener; + this.queryParameters = QueryParameters.EMPTY; + } + + public void setQueryParameters(QueryParameters queryParameters) { + this.queryParameters = queryParameters; + } + + public void execute() { + Query query; + if (queryConfiguration.isHasParameters()) { + query = database.queryFactory().createQuery(queryString, queryParameters, queryConfiguration); + } else { + query = database.queryFactory().createQuery(queryString, queryConfiguration); + } + + logger.trace("Execute query {}", query); + database.executeQuery(query).thenAccept(this::notifyQueryResult).exceptionally(error -> { + logger.warn("Error executing query", error); + notifyQueryResult(QueryResult.ofIncorrectResult("Error executing query")); + return null; + }); + } + + private void notifyQueryResult(QueryResult queryResult) { + var currentQueryResultListener = queryResultListener; + if (currentQueryResultListener != null) { + currentQueryResultListener.queryResultReceived(queryResult); + } + } + + public void cancel() { + queryResultListener = null; + } + + public QueryParameters getQueryParameters() { + return queryParameters; + } + + public interface QueryResultListener { + void queryResultReceived(QueryResult queryResult); + } + + @Override + public String toString() { + return "QueryExecution{" + "queryString='" + queryString + '\'' + ", queryParameters=" + queryParameters + '}'; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java new file mode 100644 index 0000000000000..652f25dd95a51 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.CHANNEL_EXECUTE; +import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.TRIGGER_CHANNEL_CALCULATE_PARAMETERS; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.action.DBQueryActions; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.openhab.binding.dbquery.internal.domain.DBQueryJSONEncoder; +import org.openhab.binding.dbquery.internal.domain.Database; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.domain.QueryResultExtractor; +import org.openhab.binding.dbquery.internal.domain.ResultValue; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BridgeHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages query thing, handling it's commands and updating it's channels + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QueryHandler.class); + // Relax nullable rules as config can be only null when not initialized + private @NonNullByDefault({}) QueryConfiguration config; + private @NonNullByDefault({}) QueryResultExtractor queryResultExtractor; + + private @Nullable ScheduledFuture scheduledQueryExecutionInterval; + private @Nullable QueryResultChannelUpdater queryResultChannelUpdater; + private Database database = Database.EMPTY; + private final DBQueryJSONEncoder jsonEncoder = new DBQueryJSONEncoder(); + + private @Nullable QueryExecution currentQueryExecution; + private QueryResult lastQueryResult = QueryResult.NO_RESULT; + + public QueryHandler(Thing thing) { + super(thing); + } + + @Override + public void initialize() { + config = getConfigAs(QueryConfiguration.class); + queryResultExtractor = new QueryResultExtractor(config); + + initQueryResultChannelUpdater(); + updateStateWithParentBridgeStatus(); + } + + private void initQueryResultChannelUpdater() { + ChannelStateUpdater channelStateUpdater = (channel, state) -> updateState(channel.getUID(), state); + queryResultChannelUpdater = new QueryResultChannelUpdater(channelStateUpdater, this::getResultChannels2Update); + } + + private void scheduleQueryExecutionIntervalIfNeeded() { + int interval = config.getInterval(); + if (interval != QueryConfiguration.NO_INTERVAL && scheduledQueryExecutionInterval == null) { + logger.trace("Scheduling query execution every {} seconds for {}", interval, getQueryIdentifier()); + scheduledQueryExecutionInterval = scheduler.scheduleWithFixedDelay(this::executeQuery, 0, interval, + TimeUnit.SECONDS); + } + } + + private ThingUID getQueryIdentifier() { + return getThing().getUID(); + } + + private void cancelQueryExecutionIntervalIfNeeded() { + ScheduledFuture currentFuture = scheduledQueryExecutionInterval; + if (currentFuture != null) { + currentFuture.cancel(true); + scheduledQueryExecutionInterval = null; + } + } + + @Override + public void dispose() { + cancelQueryExecutionIntervalIfNeeded(); + cancelCurrentQueryExecution(); + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("handleCommand for channel {} with command {}", channelUID, command); + + if (command instanceof RefreshType) { + if (CHANNEL_EXECUTE.equals(channelUID.getId())) { + executeQuery(); + } + } else { + logger.warn("Query Thing can only handle RefreshType commands as the thing is read-only"); + } + } + + private synchronized void executeQuery() { + if (getThing().getStatus() == ThingStatus.ONLINE) { + QueryExecution queryExecution = currentQueryExecution; + if (queryExecution != null) { + logger.debug("Previous query execution for {} discarded as a new one is requested", + getQueryIdentifier()); + cancelCurrentQueryExecution(); + } + + queryExecution = new QueryExecution(database, config, queryResultReceived); + this.currentQueryExecution = queryExecution; + + if (config.isHasParameters()) { + logger.trace("{} triggered to set parameters for {}", TRIGGER_CHANNEL_CALCULATE_PARAMETERS, + queryExecution); + updateParametersChannel(QueryParameters.EMPTY); + triggerChannel(TRIGGER_CHANNEL_CALCULATE_PARAMETERS); + } else { + queryExecution.execute(); + } + } else { + logger.debug("Execute query ignored because thing status is {}", getThing().getStatus()); + } + } + + private synchronized void cancelCurrentQueryExecution() { + QueryExecution current = currentQueryExecution; + if (current != null) { + current.cancel(); + currentQueryExecution = null; + } + } + + private void updateStateWithQueryResult(QueryResult queryResult) { + var currentQueryResultChannelUpdater = queryResultChannelUpdater; + var localCurrentQueryExecution = this.currentQueryExecution; + lastQueryResult = queryResult; + if (currentQueryResultChannelUpdater != null && localCurrentQueryExecution != null) { + ResultValue resultValue = queryResultExtractor.extractResult(queryResult); + updateCorrectChannel(resultValue.isCorrect()); + updateParametersChannel(localCurrentQueryExecution.getQueryParameters()); + if (resultValue.isCorrect()) { + currentQueryResultChannelUpdater.updateChannelResults(resultValue.getResult()); + } else { + currentQueryResultChannelUpdater.clearChannelResults(); + } + } else { + logger.warn( + "QueryResult discarded as queryResultChannelUpdater nor currentQueryExecution are not expected to be null"); + } + } + + private void updateCorrectChannel(boolean correct) { + updateState(DBQueryBindingConstants.CHANNEL_CORRECT, OnOffType.from(correct)); + } + + private void updateParametersChannel(QueryParameters queryParameters) { + updateState(DBQueryBindingConstants.CHANNEL_PARAMETERS, new StringType(jsonEncoder.encode(queryParameters))); + } + + private void updateStateWithParentBridgeStatus() { + final @Nullable Bridge bridge = getBridge(); + DatabaseBridgeHandler databaseBridgeHandler; + + if (bridge != null) { + @Nullable + BridgeHandler bridgeHandler = bridge.getHandler(); + if (bridgeHandler instanceof DatabaseBridgeHandler) { + databaseBridgeHandler = (DatabaseBridgeHandler) bridgeHandler; + database = databaseBridgeHandler.getDatabase(); + if (bridge.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED); + } + } + + @Override + protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) { + super.updateStatus(status, statusDetail, description); + if (status == ThingStatus.ONLINE) { + scheduleQueryExecutionIntervalIfNeeded(); + } + } + + @Override + public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { + cancelCurrentQueryExecution(); + updateStateWithParentBridgeStatus(); + } + + public void setParameters(Map parameters) { + final @Nullable QueryExecution queryExecution = currentQueryExecution; + if (queryExecution != null) { + QueryParameters queryParameters = new QueryParameters(parameters); + queryExecution.setQueryParameters(queryParameters); + queryExecution.execute(); + } else { + logger.trace("setParameters ignored as there is any executing query for {}", getQueryIdentifier()); + } + } + + private final QueryExecution.QueryResultListener queryResultReceived = (QueryResult queryResult) -> { + synchronized (QueryHandler.this) { + logger.trace("queryResultReceived for {} : {}", getQueryIdentifier(), queryResult); + updateStateWithQueryResult(queryResult); + + currentQueryExecution = null; + } + }; + + @Override + public Collection> getServices() { + return List.of(DBQueryActions.class); + } + + public QueryResult getLastQueryResult() { + return lastQueryResult; + } + + private List getResultChannels2Update() { + return getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID())) + .filter(this::isResultChannel).collect(Collectors.toList()); + } + + private boolean isResultChannel(Channel channel) { + @Nullable + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + return channelTypeUID != null && channelTypeUID.getId().startsWith("result"); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java new file mode 100644 index 0000000000000..fe88f08aee4c6 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Updates a query result to needed channels doing needed conversions + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryResultChannelUpdater { + private final ChannelStateUpdater channelStateUpdater; + private final ChannelsToUpdateQueryResult channels2Update; + private final Value2StateConverter value2StateConverter; + + public QueryResultChannelUpdater(ChannelStateUpdater channelStateUpdater, + ChannelsToUpdateQueryResult channelsToUpdate) { + this.channelStateUpdater = channelStateUpdater; + this.channels2Update = channelsToUpdate; + this.value2StateConverter = new Value2StateConverter(); + } + + public void clearChannelResults() { + for (Channel channel : channels2Update.getChannels()) { + channelStateUpdater.updateChannelState(channel, UnDefType.NULL); + } + } + + public void updateChannelResults(@Nullable Object extractedResult) { + for (Channel channel : channels2Update.getChannels()) { + Class targetType = calculateItemType(channel); + State state = value2StateConverter.convertValue(extractedResult, targetType); + channelStateUpdater.updateChannelState(channel, state); + } + } + + private Class calculateItemType(Channel channel) { + ChannelTypeUID channelTypeUID = channel.getChannelTypeUID(); + String channelID = channelTypeUID != null ? channelTypeUID.getId() + : DBQueryBindingConstants.RESULT_STRING_CHANNEL_TYPE; + switch (channelID) { + case DBQueryBindingConstants.RESULT_STRING_CHANNEL_TYPE: + return StringType.class; + case DBQueryBindingConstants.RESULT_NUMBER_CHANNEL_TYPE: + return DecimalType.class; + case DBQueryBindingConstants.RESULT_DATETIME_CHANNEL_TYPE: + return DateTimeType.class; + case DBQueryBindingConstants.RESULT_SWITCH_CHANNEL_TYPE: + return OnOffType.class; + case DBQueryBindingConstants.RESULT_CONTACT_CHANNEL_TYPE: + return OpenClosedType.class; + default: + throw new UnnexpectedCondition("Unexpected channel type " + channelTypeUID); + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java new file mode 100644 index 0000000000000..6226ee2a4ae30 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.Date; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.domain.DBQueryJSONEncoder; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manage conversion from a value to needed State target type + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class Value2StateConverter { + private final Logger logger = LoggerFactory.getLogger(Value2StateConverter.class); + private final DBQueryJSONEncoder jsonEncoder = new DBQueryJSONEncoder(); + + public State convertValue(@Nullable Object value, Class targetType) { + if (value == null) { + return UnDefType.NULL; + } else { + if (targetType == StringType.class) { + return convert2String(value); + } else if (targetType == DecimalType.class) { + return convert2Decimal(value); + } else if (targetType == DateTimeType.class) { + return convert2DateTime(value); + } else if (targetType == OnOffType.class) { + @Nullable + Boolean bool = convert2Boolean(value); + return bool != null ? OnOffType.from(bool) : UnDefType.NULL; + } else if (targetType == OpenClosedType.class) { + @Nullable + Boolean bool = convert2Boolean(value); + if (bool != null) { + return bool ? OpenClosedType.OPEN : OpenClosedType.CLOSED; + } else { + return UnDefType.NULL; + } + } else { + throw new UnnexpectedCondition("Not expected targetType " + targetType); + } + } + } + + private State convert2DateTime(Object value) { + if (value instanceof Instant) { + return new DateTimeType(ZonedDateTime.ofInstant((Instant) value, ZoneId.systemDefault())); + } else if (value instanceof Date) { + return new DateTimeType(ZonedDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault())); + } else if (value instanceof String) { + return new DateTimeType((String) value); + } else { + logger.warn("Can't convert {} to DateTimeType", value); + return UnDefType.NULL; + } + } + + private State convert2Decimal(Object value) { + if (value instanceof Integer) { + return new DecimalType((Integer) value); + } else if (value instanceof Long) { + return new DecimalType((Long) value); + } else if (value instanceof Float) { + return new DecimalType((Float) value); + } else if (value instanceof Double) { + return new DecimalType((Double) value); + } else if (value instanceof BigDecimal) { + return new DecimalType((BigDecimal) value); + } else if (value instanceof BigInteger) { + return new DecimalType(new BigDecimal((BigInteger) value)); + } else if (value instanceof Number) { + return new DecimalType(((Number) value).longValue()); + } else if (value instanceof String) { + return DecimalType.valueOf((String) value); + } else if (value instanceof Duration) { + return new DecimalType(((Duration) value).toMillis()); + } else { + logger.warn("Can't convert {} to DecimalType", value); + return UnDefType.NULL; + } + } + + private State convert2String(Object value) { + if (value instanceof String) { + return new StringType((String) value); + } else if (value instanceof byte[]) { + return new StringType(Base64.getEncoder().encodeToString((byte[]) value)); + } else if (value instanceof QueryResult) { + return new StringType(jsonEncoder.encode((QueryResult) value)); + } else { + return new StringType(String.valueOf(value)); + } + } + + private @Nullable Boolean convert2Boolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } else if (value instanceof Number) { + return ((Number) value).doubleValue() != 0d; + } else if (value instanceof String) { + var svalue = (String) value; + return Boolean.parseBoolean(svalue) || (svalue.equalsIgnoreCase("on")) || svalue.equals("1"); + } else { + logger.warn("Can't convert {} to OnOffType or OpenClosedType", value); + return null; + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java new file mode 100644 index 0000000000000..c9a1fb6acc934 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.config; + +import java.util.StringJoiner; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Contains fields mapping InfluxDB2 bridge configuration parameters. + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class InfluxDB2BridgeConfiguration { + private String url; + private String user; + private String token; + private String bucket; + private String organization; + + public InfluxDB2BridgeConfiguration(String url, String user, String token, String organization, String bucket) { + this.url = url; + this.user = user; + this.token = token; + this.organization = organization; + this.bucket = bucket; + } + + public InfluxDB2BridgeConfiguration() { + // Used only when configuration is created by reflection using ConfigMapper + url = user = token = organization = bucket = ""; + } + + public String getUrl() { + return url; + } + + public String getUser() { + return user; + } + + public String getToken() { + return token; + } + + public String getOrganization() { + return organization; + } + + public String getBucket() { + return bucket; + } + + @Override + public String toString() { + return new StringJoiner(", ", InfluxDB2BridgeConfiguration.class.getSimpleName() + "[", "]") + .add("url='" + url + "'").add("user='" + user + "'").add("token='" + "*".repeat(token.length()) + "'") + .add("organization='" + organization + "'").add("bucket='" + bucket + "'").toString(); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java new file mode 100644 index 0000000000000..48cff74033623 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.config; + +import java.util.StringJoiner; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Contains fields mapping query things parameters. + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryConfiguration { + public static final int NO_INTERVAL = 0; + + private String query = ""; + private int interval; + private int timeout; + private boolean scalarResult; + private boolean hasParameters; + private @Nullable String scalarColumn = ""; + + public QueryConfiguration() { + // Used only when configuration is created by reflection using ConfigMapper + } + + public QueryConfiguration(String query, int interval, int timeout, boolean scalarResult, + @Nullable String scalarColumn, boolean hasParameters) { + this.query = query; + this.interval = interval; + this.timeout = timeout; + this.scalarResult = scalarResult; + this.scalarColumn = scalarColumn; + this.hasParameters = hasParameters; + } + + public String getQuery() { + return query; + } + + public int getInterval() { + return interval; + } + + public int getTimeout() { + return timeout; + } + + public boolean isScalarResult() { + return scalarResult; + } + + public @Nullable String getScalarColumn() { + var currentScalarColumn = scalarColumn; + return currentScalarColumn != null ? currentScalarColumn : ""; + } + + public boolean isScalarColumnDefined() { + return !getScalarColumn().isBlank(); + } + + public boolean isHasParameters() { + return hasParameters; + } + + @Override + public String toString() { + return new StringJoiner(", ", QueryConfiguration.class.getSimpleName() + "[", "]").add("query='" + query + "'") + .add("interval=" + interval).add("timeout=" + timeout).add("scalarResult=" + scalarResult) + .add("hasParameters=" + hasParameters).add("scalarColumn='" + scalarColumn + "'").toString(); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java new file mode 100644 index 0000000000000..0cd636c6bc4c5 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl; + +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; + +/** + * Provides a parser to substitute query parameters for database like InfluxDB that doesn't support that in it's client. + * It's not ideal because it's subject to query injection attacks but it does the work if params are from trusted + * sources. + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class StringSubstitutionParamsParser { + private final Pattern paramPattern = Pattern.compile("\\$\\{([\\w_]*?)}"); + private final String query; + + public StringSubstitutionParamsParser(String query) { + this.query = query; + } + + public String getQueryWithParametersReplaced(QueryParameters queryParameters) { + var matcher = paramPattern.matcher(query); + int idx = 0; + StringBuilder substitutedQuery = new StringBuilder(); + while (matcher.find()) { + String nonParametersPart = query.substring(idx, matcher.start()); + String parameterName = matcher.group(1); + substitutedQuery.append(nonParametersPart); + substitutedQuery.append(parameterValue(parameterName, queryParameters)); + idx = matcher.end(); + } + if (idx < query.length()) { + substitutedQuery.append(query.substring(idx)); + } + + return substitutedQuery.toString(); + } + + private String parameterValue(String parameterName, QueryParameters queryParameters) { + var parameter = queryParameters.getParameter(parameterName); + if (parameter != null) { + return parameter.toString(); + } else { + return ""; + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java new file mode 100644 index 0000000000000..cdbf1d99ad8c8 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration; +import org.openhab.binding.dbquery.internal.domain.Database; +import org.openhab.binding.dbquery.internal.domain.Query; +import org.openhab.binding.dbquery.internal.domain.QueryFactory; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.error.DatabaseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.influxdb.query.FluxRecord; + +/** + * Influx2 implementation of {@link Database} + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class Influx2Database implements Database { + private final Logger logger = LoggerFactory.getLogger(Influx2Database.class); + private final ExecutorService executors; + private final InfluxDB2BridgeConfiguration config; + private final InfluxDBClientFacade client; + private final QueryFactory queryFactory; + + public Influx2Database(InfluxDB2BridgeConfiguration config, InfluxDBClientFacade influxDBClientFacade) { + this.config = config; + this.client = influxDBClientFacade; + executors = Executors.newSingleThreadScheduledExecutor(); + queryFactory = new Influx2QueryFactory(); + } + + @Override + public boolean isConnected() { + return client.isConnected(); + } + + @Override + public CompletableFuture connect() { + return CompletableFuture.supplyAsync(() -> { + synchronized (Influx2Database.this) { + return client.connect(); + } + }, executors); + } + + @Override + public CompletableFuture disconnect() { + return CompletableFuture.supplyAsync(() -> { + synchronized (Influx2Database.this) { + return client.disconnect(); + } + }, executors); + } + + @Override + public QueryFactory queryFactory() throws DatabaseException { + return queryFactory; + } + + @Override + public CompletableFuture executeQuery(Query query) { + try { + if (query instanceof Influx2QueryFactory.Influx2Query) { + Influx2QueryFactory.Influx2Query influxQuery = (Influx2QueryFactory.Influx2Query) query; + + CompletableFuture asyncResult = new CompletableFuture<>(); + List records = new ArrayList<>(); + client.query(influxQuery.getQuery(), (cancellable, record) -> { // onNext + records.add(record); + }, error -> { // onError + logger.warn("Error executing query {}", query, error); + asyncResult.complete(QueryResult.ofIncorrectResult("Error executing query")); + }, () -> { // onComplete + asyncResult.complete(new Influx2QueryResultExtractor().apply(records)); + }); + return asyncResult; + } else { + return CompletableFuture + .completedFuture(QueryResult.ofIncorrectResult("Unnexpected query type " + query)); + } + } catch (RuntimeException e) { + return CompletableFuture.failedFuture(e); + } + } + + @Override + public String toString() { + return "Influx2Database{config=" + config + '}'; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java new file mode 100644 index 0000000000000..790a54582ea7d --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.openhab.binding.dbquery.internal.dbimpl.StringSubstitutionParamsParser; +import org.openhab.binding.dbquery.internal.domain.Query; +import org.openhab.binding.dbquery.internal.domain.QueryFactory; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; + +/** + * Influx2 implementation of {@link QueryFactory} + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class Influx2QueryFactory implements QueryFactory { + + @Override + public Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration) { + return new Influx2Query(query); + } + + @Override + public Query createQuery(String query, QueryParameters parameters, + @Nullable QueryConfiguration queryConfiguration) { + return new Influx2Query(substituteParameters(query, parameters)); + } + + private String substituteParameters(String query, QueryParameters parameters) { + return new StringSubstitutionParamsParser(query).getQueryWithParametersReplaced(parameters); + } + + static class Influx2Query implements Query { + private final String query; + + public Influx2Query(String query) { + this.query = query; + } + + String getQuery() { + return query; + } + + @Override + public String toString() { + return query; + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java new file mode 100644 index 0000000000000..2e16e00ae7eb7 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.domain.ResultRow; + +import com.influxdb.query.FluxRecord; + +/** + * Extracts results from Influx2 client query result to a {@link QueryResult} + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class Influx2QueryResultExtractor implements Function, QueryResult> { + + @Override + public QueryResult apply(List records) { + var rows = records.stream().map(Influx2QueryResultExtractor::mapRecord2Row).collect(Collectors.toList()); + return QueryResult.of(rows); + } + + private static ResultRow mapRecord2Row(FluxRecord record) { + Map values = record.getValues().entrySet().stream() + .filter(entry -> !Set.of("result", "table").contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + return new ResultRow(values); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java new file mode 100644 index 0000000000000..5dd9424f30472 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.influxdb.Cancellable; +import com.influxdb.query.FluxRecord; + +/** + * Facade to Influx2 client to facilitate testing + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface InfluxDBClientFacade { + boolean connect(); + + boolean isConnected(); + + boolean disconnect(); + + void query(String query, BiConsumer onNext, Consumer onError, + Runnable onComplete); +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java new file mode 100644 index 0000000000000..1bf86d8094ada --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.influxdb.Cancellable; +import com.influxdb.client.InfluxDBClient; +import com.influxdb.client.InfluxDBClientFactory; +import com.influxdb.client.InfluxDBClientOptions; +import com.influxdb.client.QueryApi; +import com.influxdb.client.domain.Ready; +import com.influxdb.query.FluxRecord; + +/** + * Real implementation of {@link InfluxDBClientFacade} + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class InfluxDBClientFacadeImpl implements InfluxDBClientFacade { + private final Logger logger = LoggerFactory.getLogger(InfluxDBClientFacadeImpl.class); + + private final InfluxDB2BridgeConfiguration config; + + private @Nullable InfluxDBClient client; + private @Nullable QueryApi queryAPI; + + public InfluxDBClientFacadeImpl(InfluxDB2BridgeConfiguration config) { + this.config = config; + } + + @Override + public boolean connect() { + var clientOptions = InfluxDBClientOptions.builder().url(config.getUrl()).org(config.getOrganization()) + .bucket(config.getBucket()).authenticateToken(config.getToken().toCharArray()).build(); + + final InfluxDBClient createdClient = InfluxDBClientFactory.create(clientOptions); + this.client = createdClient; + var currentQueryAPI = createdClient.getQueryApi(); + this.queryAPI = currentQueryAPI; + + boolean connected = checkConnectionStatus(); + if (connected) { + logger.debug("Successfully connected to InfluxDB. Instance ready={}", createdClient.ready()); + } else { + logger.warn("Not able to connect to InfluxDB with config {}", config); + } + + return connected; + } + + private boolean checkConnectionStatus() { + final InfluxDBClient currentClient = client; + if (currentClient != null) { + Ready ready = currentClient.ready(); + boolean isUp = ready != null && ready.getStatus() == Ready.StatusEnum.READY; + if (isUp) { + logger.debug("database status is OK"); + } else { + logger.warn("database not ready"); + } + return isUp; + } else { + logger.warn("checkConnection: database is not connected"); + return false; + } + } + + @Override + public boolean isConnected() { + return checkConnectionStatus(); + } + + @Override + public boolean disconnect() { + final InfluxDBClient currentClient = client; + if (currentClient != null) { + currentClient.close(); + client = null; + queryAPI = null; + logger.debug("Succesfully disconnected from InfluxDB"); + } else { + logger.debug("Already disconnected"); + } + return true; + } + + @Override + public void query(String query, BiConsumer onNext, Consumer onError, + Runnable onComplete) { + var currentQueryAPI = queryAPI; + if (currentQueryAPI != null) { + currentQueryAPI.query(query, onNext, onError, onComplete); + } else { + logger.warn("Query ignored as current queryAPI is null"); + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java new file mode 100644 index 0000000000000..c5bfb701317b4 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * Encodes domain objects to JSON + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class DBQueryJSONEncoder { + private final Gson gson; + + public DBQueryJSONEncoder() { + gson = new GsonBuilder().registerTypeAdapter(QueryResult.class, new QueryResultGSONSerializer()) + .registerTypeAdapter(ResultRow.class, new ResultRowGSONSerializer()) + .registerTypeAdapter(QueryParameters.class, new QueryParametersGSONSerializer()).create(); + } + + public String encode(QueryResult queryResult) { + return gson.toJson(queryResult); + } + + public String encode(QueryParameters parameters) { + return gson.toJson(parameters); + } + + @NonNullByDefault({}) + private static class QueryResultGSONSerializer implements JsonSerializer { + @Override + public JsonElement serialize(QueryResult src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("correct", src.isCorrect()); + if (src.getErrorMessage() != null) { + jsonObject.addProperty("errorMessage", src.getErrorMessage()); + } + jsonObject.add("data", context.serialize(src.getData())); + return jsonObject; + } + } + + private static class ResultRowGSONSerializer implements JsonSerializer { + @Override + public JsonElement serialize(ResultRow src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + for (String columnName : src.getColumnNames()) { + jsonObject.add(columnName, convertValueToJsonPrimitive(src.getValue(columnName))); + } + return jsonObject; + } + } + + private static class QueryParametersGSONSerializer implements JsonSerializer { + @Override + public JsonElement serialize(QueryParameters src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject jsonObject = new JsonObject(); + for (Map.Entry param : src.getAll().entrySet()) { + jsonObject.add(param.getKey(), convertValueToJsonPrimitive(param.getValue())); + } + return jsonObject; + } + } + + private static JsonElement convertValueToJsonPrimitive(@Nullable Object value) { + if (value instanceof Number) { + return new JsonPrimitive((Number) value); + } else if (value instanceof Boolean) { + return new JsonPrimitive((Boolean) value); + } else if (value instanceof Character) { + return new JsonPrimitive((Character) value); + } else if (value instanceof Date) { + return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format(((Date) value).toInstant())); + } else if (value instanceof Instant) { + return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format((Instant) value)); + } else if (value != null) { + return new JsonPrimitive(value.toString()); + } else { + return JsonNull.INSTANCE; + } + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java new file mode 100644 index 0000000000000..58a170970ee32 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dbquery.internal.error.DatabaseException; + +/** + * Abstracts database operations needed for query execution + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface Database { + boolean isConnected(); + + CompletableFuture connect(); + + CompletableFuture disconnect(); + + QueryFactory queryFactory() throws DatabaseException; + + CompletableFuture executeQuery(Query query); + + Database EMPTY = new Database() { + @Override + public boolean isConnected() { + return false; + } + + @Override + public CompletableFuture connect() { + return CompletableFuture.completedFuture(false); + } + + @Override + public CompletableFuture disconnect() { + return CompletableFuture.completedFuture(false); + } + + @Override + public QueryFactory queryFactory() { + return QueryFactory.EMPTY; + } + + @Override + public CompletableFuture executeQuery(Query query) { + return CompletableFuture.completedFuture(QueryResult.ofIncorrectResult("Empty database")); + } + }; +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java new file mode 100644 index 0000000000000..a635cffcca8d5 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Executes a non defined query in given database + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class ExecuteNonConfiguredQuery { + private final Logger logger = LoggerFactory.getLogger(ExecuteNonConfiguredQuery.class); + private final Database database; + + public ExecuteNonConfiguredQuery(Database database) { + this.database = database; + } + + public CompletableFuture execute(String queryString, Map parameters, + Duration timeout) { + if (!database.isConnected()) { + return CompletableFuture.completedFuture(QueryResult.ofIncorrectResult("Database not connected")); + } + + Query query = database.queryFactory().createQuery(queryString, new QueryParameters(parameters), + createConfiguration(queryString, timeout)); + return database.executeQuery(query); + } + + public QueryResult executeSynchronously(String queryString, Map parameters, + Duration timeout) { + var completableFuture = execute(queryString, parameters, timeout); + try { + if (timeout.isZero()) { + return completableFuture.get(); + } else { + return completableFuture.get(timeout.getSeconds(), TimeUnit.SECONDS); + } + } catch (InterruptedException e) { + logger.debug("Query was interrupted", e); + Thread.currentThread().interrupt(); + return QueryResult.ofIncorrectResult("Query execution was interrupted"); + } catch (ExecutionException e) { + logger.warn("Error executing query", e); + return QueryResult.ofIncorrectResult("Error executing query " + e.getMessage()); + } catch (TimeoutException e) { + logger.debug("Timeout executing query", e); + return QueryResult.ofIncorrectResult("Timeout"); + } + } + + private QueryConfiguration createConfiguration(String query, Duration timeout) { + return new QueryConfiguration(query, QueryConfiguration.NO_INTERVAL, (int) timeout.getSeconds(), false, null, + true); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java new file mode 100644 index 0000000000000..cad7bc8263246 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Marker interface for queries + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface Query { + Query EMPTY = new Query() { + }; +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java new file mode 100644 index 0000000000000..a89ab676d41c7 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; + +/** + * Abstracts operations needed to create a query from its thing configuration + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public interface QueryFactory { + Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration); + + Query createQuery(String query, QueryParameters parameters, @Nullable QueryConfiguration queryConfiguration); + + QueryFactory EMPTY = new QueryFactory() { + @Override + public Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration) { + return Query.EMPTY; + } + + @Override + public Query createQuery(String query, QueryParameters parameters, + @Nullable QueryConfiguration queryConfiguration) { + return Query.EMPTY; + } + }; +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java new file mode 100644 index 0000000000000..617e12d95b079 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Query parameters + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryParameters { + public static final QueryParameters EMPTY = new QueryParameters(Collections.emptyMap()); + private final Map params; + + private QueryParameters() { + this.params = new HashMap<>(); + } + + public QueryParameters(Map params) { + this.params = params; + } + + public void setParameter(String name, @Nullable Object value) { + params.put(name, value); + } + + public @Nullable Object getParameter(String paramName) { + return params.get(paramName); + } + + public Map getAll() { + return Collections.unmodifiableMap(params); + } + + public int size() { + return params.size(); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java new file mode 100644 index 0000000000000..c3c4465372fcd --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.util.Collections; +import java.util.List; +import java.util.StringJoiner; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Query result + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryResult { + public static final QueryResult NO_RESULT = QueryResult.ofIncorrectResult("No result"); + + private final boolean correct; + private final @Nullable String errorMessage; + private final List data; + + private QueryResult(boolean correct, String errorMessage) { + this.correct = correct; + this.errorMessage = errorMessage; + this.data = Collections.emptyList(); + } + + private QueryResult(List data) { + this.correct = true; + this.errorMessage = null; + this.data = data; + } + + public static QueryResult ofIncorrectResult(String errorMessage) { + return new QueryResult(false, errorMessage); + } + + public static QueryResult of(ResultRow... rows) { + return new QueryResult(List.of(rows)); + } + + public static QueryResult of(List rows) { + return new QueryResult(rows); + } + + public static QueryResult ofSingleValue(String columnName, Object value) { + return new QueryResult(List.of(new ResultRow(columnName, value))); + } + + public boolean isCorrect() { + return correct; + } + + public @Nullable String getErrorMessage() { + return errorMessage; + } + + public List getData() { + return data; + } + + @Override + public String toString() { + return new StringJoiner(", ", QueryResult.class.getSimpleName() + "[", "]").add("correct=" + correct) + .add("errorMessage='" + errorMessage + "'").add("data=" + data).toString(); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java new file mode 100644 index 0000000000000..3ed8e1fa28c0d --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extracts a result from {@link QueryResult} to a single value to be used in channels + * (after being converted that it's not responsability of this class) + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class QueryResultExtractor { + private final Logger logger = LoggerFactory.getLogger(QueryResultExtractor.class); + private final QueryConfiguration config; + + public QueryResultExtractor(QueryConfiguration config) { + this.config = config; + } + + public ResultValue extractResult(QueryResult queryResult) { + if (queryResult.isCorrect()) { + if (config.isScalarResult()) { + return getScalarValue(queryResult); + } else { + return ResultValue.of(queryResult); + } + } else { + return ResultValue.incorrect(); + } + } + + private ResultValue getScalarValue(QueryResult queryResult) { + if (validateHasScalarValue(queryResult)) { + var row = queryResult.getData().get(0); + @Nullable + Object value; + if (config.isScalarColumnDefined()) { + value = row.getValue(Objects.requireNonNull(config.getScalarColumn())); + } else { + value = row.getValue(row.getColumnNames().iterator().next()); + } + return ResultValue.of(value); + } else { + return ResultValue.incorrect(); + } + } + + private boolean validateHasScalarValue(QueryResult queryResult) { + boolean valid = false; + String baseErrorMessage = "Can't get scalar value for result: "; + if (queryResult.isCorrect()) { + if (queryResult.getData().size() == 1) { + boolean oneColumn = queryResult.getData().get(0).getColumnsSize() == 1; + if (oneColumn || config.isScalarColumnDefined()) { + valid = true; + } else { + logger.warn("{} Columns size is {} and scalarColumn isn't defined", baseErrorMessage, + queryResult.getData().get(0).getColumnNames().size()); + } + } else { + logger.warn("{} Rows size is {}", baseErrorMessage, queryResult.getData().size()); + } + } else { + logger.debug("{} Incorrect result", baseErrorMessage); + } + return valid; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java new file mode 100644 index 0000000000000..e404a2761381f --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Query result row + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class ResultRow { + private final Logger logger = LoggerFactory.getLogger(ResultRow.class); + + private final LinkedHashMap values; + + public ResultRow(String columnName, @Nullable Object value) { + this.values = new LinkedHashMap<>(); + put(columnName, value); + } + + public ResultRow(Map values) { + this.values = new LinkedHashMap<>(); + values.forEach(this::put); + } + + public Set getColumnNames() { + return values.keySet(); + } + + public int getColumnsSize() { + return values.size(); + } + + public @Nullable Object getValue(String column) { + return values.get(column); + } + + public static boolean isValidResultRowType(@Nullable Object object) { + return object == null || object instanceof String || object instanceof Boolean || object instanceof Number + || object instanceof byte[] || object instanceof Instant || object instanceof Date + || object instanceof Duration; + } + + private void put(String columnName, @Nullable Object value) { + if (!isValidResultRowType(value)) { + logger.trace("Value {} of type {} converted to String as not supported internal type in dbquery", value, + value.getClass()); + value = value.toString(); + } + values.put(columnName, value); + } + + @Override + public String toString() { + return values.toString(); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java new file mode 100644 index 0000000000000..cc44f67863d80 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * A query result value as is extracted by {@link QueryResultExtractor} from a {@link QueryResult} + * to be set in channels + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class ResultValue { + private final boolean correct; + private final @Nullable Object result; + + private ResultValue(boolean correct, @Nullable Object result) { + this.correct = correct; + this.result = result; + } + + public static ResultValue of(@Nullable Object result) { + return new ResultValue(true, result); + } + + public static ResultValue incorrect() { + return new ResultValue(false, null); + } + + public boolean isCorrect() { + return correct; + } + + public @Nullable Object getResult() { + return result; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java new file mode 100644 index 0000000000000..148853ee28b91 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.error; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception from a database operation + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class DatabaseException extends RuntimeException { + + private static final long serialVersionUID = 5181127643040903150L; + + public DatabaseException(String message) { + super(message); + } + + public DatabaseException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java new file mode 100644 index 0000000000000..b558099777ccc --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.error; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * An unexpected error, aka bug + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class UnnexpectedCondition extends RuntimeException { + private static final long serialVersionUID = -7785815761302340174L; + + public UnnexpectedCondition(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..89873ef13c70d --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + DBQuery Binding + This is the binding for DBQuery that allows to execute native database queries and bind their results to + items. + + diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml new file mode 100644 index 0000000000000..7ffc937f0d9d3 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml @@ -0,0 +1,36 @@ + + + + + The InfluxDB 2.0 represents a connection to a InfluxDB 2.0 server + + + + url + + Database url + http://localhost:9999 + + + + Name of the database user + + + + password + Token to authenticate to the database + + + + Name of the database organization + + + + Name of the database bucket + + + + diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..b4a71149e2570 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,125 @@ + + + + + + + + + Thing that represents a native query + + + + + + + + + + + + + + + + + + + + Query definition using native query language + script + + + + True if the query has parameters, otherwise false + false + + + + True if the query always return only one single scalar value (only one row and one value-column in this + row), otherwise false + true + + + + The column's name that is used to extract scalarResult. If only one column is returned this + parameter + can be blank + + + + + An interval, in seconds, the query will be repeatedly executed. Default values is 0, which means that + query is never executed automatically. You need to send the ON command each time you wish to execute. + + 0 + + + + + A time-out in seconds to wait for the query result, if it's exceeded result will be discarded. + Use 0 for + no timeout + + 0 + + + + + + + + + Switch + + Send ON to execute the query, the current state tells if the query is running + + + String + + Execute query and binds result value to channel as a String + + + Number + + Execute query and binds result value to channel as a Number + + + DateTime + + Execute query and binds result value to channel as a DateTime + + + DateTime + + Execute query and binds result value to channel as a Contact + + + Switch + + Execute query and binds result value to channel as a Switch + + + String + + + + Switch + + True if last query executed correctly + + + trigger + + Event to calculate query parameters + + + + + + + + diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java new file mode 100644 index 0000000000000..b1f653aedbc5a --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dbquery.internal.config.QueryConfiguration; +import org.openhab.binding.dbquery.internal.domain.QueryResult; +import org.openhab.binding.dbquery.internal.domain.QueryResultExtractor; +import org.openhab.binding.dbquery.internal.domain.ResultRow; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +class Influx2QueryResultExtractorTest { + public static final QueryResult ONE_ROW_ONE_COLUMN_RESULT = QueryResult.ofSingleValue("AnyValueName", "value"); + public static final QueryResult SEVERAL_ROWS_COLUMNS_RESULT = QueryResult.of( + new ResultRow(Map.of("valueName", "value1", "column2", "value2")), + new ResultRow(Map.of("valueName", "value1", "column2", "value2"))); + public static final QueryResult ONE_ROW_SEVERAL_COLUMNS_RESULT = QueryResult + .of(new ResultRow(Map.of("valueName", "value1", "column2", "value2"))); + public static final QueryResult INCORRECT_RESULT = QueryResult.ofIncorrectResult("Incorrect result"); + + private static final QueryConfiguration SCALAR_VALUE_CONFIG = new QueryConfiguration("query", 10, 10, true, null, + false); + private static final QueryConfiguration NON_SCALAR_VALUE_CONFIG = new QueryConfiguration("query", 10, 10, false, + null, false); + private static final QueryConfiguration SCALAR_VALUE_CONFIG_WITH_SCALAR_COLUMN = new QueryConfiguration("query", 10, + 10, true, "valueName", false); + + @Test + void givenAResultWithOneRowAndOneColumnAndScalarConfigurationScalarValueIsReturned() { + var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(ONE_ROW_ONE_COLUMN_RESULT); + + assertThat(extracted.isCorrect(), is(Boolean.TRUE)); + assertThat(extracted.getResult(), is("value")); + } + + @Test + void givenAResultWithSeveralRowsAndScalarConfigurationIncorrectValueIsReturned() { + var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(SEVERAL_ROWS_COLUMNS_RESULT); + + assertThat(extracted.isCorrect(), is(false)); + assertThat(extracted.getResult(), nullValue()); + } + + @Test + void givenAResultWithSeveralColumnsAndScalarConfigurationIncorrectValueIsReturned() { + var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(ONE_ROW_SEVERAL_COLUMNS_RESULT); + + assertThat(extracted.isCorrect(), is(false)); + assertThat(extracted.getResult(), nullValue()); + } + + @Test + void givenAResultWithSeveralColumnsAndScalarConfigurationAndScalarColumnDefinedValueIsReturned() { + var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG_WITH_SCALAR_COLUMN) + .extractResult(ONE_ROW_SEVERAL_COLUMNS_RESULT); + + assertThat(extracted.isCorrect(), is(true)); + assertThat(extracted.getResult(), is("value1")); + } + + @Test + void givenAResultWithSeveralRowsAndNonScalarConfigQueryResultIsReturned() { + var extracted = new QueryResultExtractor(NON_SCALAR_VALUE_CONFIG).extractResult(SEVERAL_ROWS_COLUMNS_RESULT); + + assertThat(extracted.isCorrect(), is(true)); + assertThat(extracted.getResult(), is(SEVERAL_ROWS_COLUMNS_RESULT)); + } + + @Test + void givenAIncorrectResultIncorrectValueIsReturned() { + var extracted = new QueryResultExtractor(NON_SCALAR_VALUE_CONFIG).extractResult(INCORRECT_RESULT); + + assertThat(extracted.isCorrect(), is(false)); + assertThat(extracted.getResult(), nullValue()); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java new file mode 100644 index 0000000000000..badca06273162 --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal; + +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.number.IsCloseTo.closeTo; + +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Base64; +import java.util.Date; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault({}) +class Value2StateConverterTest { + public static final BigDecimal BIG_DECIMAL_NUMBER = new BigDecimal("212321213123123123123123"); + private Value2StateConverter instance; + + @BeforeEach + void setUp() { + instance = new Value2StateConverter(); + } + + @AfterEach + void tearDown() { + instance = null; + } + + @ParameterizedTest + @ValueSource(classes = { StringType.class, DecimalType.class, DateTimeType.class, OpenClosedType.class, + OnOffType.class }) + void givenNullValueReturnUndef(Class classe) { + assertThat(instance.convertValue(null, classe), is(UnDefType.NULL)); + } + + @ParameterizedTest + @ValueSource(strings = { "", "stringValue" }) + void givenStringValueAndStringTargetReturnStringtype(String value) { + var converted = instance.convertValue(value, StringType.class); + assertThat(converted.toFullString(), is(value)); + } + + @ParameterizedTest + @MethodSource("provideValuesOfAllSupportedResultRowTypesExceptBytes") + void givenValidObjectTypesAndStringTargetReturnStringtypeWithString(Object value) { + var converted = instance.convertValue(value, StringType.class); + assertThat(converted.toFullString(), is(value.toString())); + } + + @Test + void givenByteArrayAndStringTargetReturnEncodedBase64() { + var someBytes = "Hello world".getBytes(Charset.defaultCharset()); + var someBytesB64 = Base64.getEncoder().encodeToString(someBytes); + var converted = instance.convertValue(someBytes, StringType.class); + assertThat(converted.toFullString(), is(someBytesB64)); + } + + @ParameterizedTest + @MethodSource("provideNumericTypes") + void givenNumericTypeAndDecimalTargetReturnDecimaltype(Number value) { + var converted = instance.convertValue(value, DecimalType.class); + assertThat(converted, instanceOf(DecimalType.class)); + assertThat(((DecimalType) converted).doubleValue(), closeTo(value.doubleValue(), 0.01d)); + } + + @ParameterizedTest + @MethodSource("provideNumericTypes") + void givenNumericStringAndDecimalTargetReturnDecimaltype(Number value) { + var numberString = value.toString(); + var converted = instance.convertValue(numberString, DecimalType.class); + assertThat(converted, instanceOf(DecimalType.class)); + assertThat(((DecimalType) converted).doubleValue(), closeTo(value.doubleValue(), 0.01d)); + } + + @Test + void givenDurationAndDecimalTargetReturnDecimaltypeWithMilliseconds() { + var duration = Duration.ofDays(1); + var converted = instance.convertValue(duration, DecimalType.class); + assertThat(converted, instanceOf(DecimalType.class)); + assertThat(((DecimalType) converted).longValue(), is(duration.toMillis())); + } + + @Test + void givenInstantAndDatetimeTargetReturnDatetype() { + var instant = Instant.now(); + var converted = instance.convertValue(instant, DateTimeType.class); + assertThat(converted, instanceOf(DateTimeType.class)); + assertThat(((DateTimeType) converted).getZonedDateTime(), + is(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()).withFixedOffsetZone())); + } + + @Test + void givenDateAndDatetimeTargetReturnDatetype() { + var date = new Date(); + var converted = instance.convertValue(date, DateTimeType.class); + assertThat(converted, instanceOf(DateTimeType.class)); + assertThat(((DateTimeType) converted).getZonedDateTime(), + is(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).withFixedOffsetZone())); + } + + @ParameterizedTest + @ValueSource(strings = { "2019-10-12T07:20:50.52Z", "2019-10-12" }) + void givenValidStringDateAndDatetimeTargetReturnDatetype(String date) { + var converted = instance.convertValue(date, DateTimeType.class); + assertThat(converted, instanceOf(DateTimeType.class)); + var convertedDateTime = ((DateTimeType) converted).getZonedDateTime(); + assertThat(convertedDateTime.getYear(), is(2019)); + assertThat(convertedDateTime.getMonthValue(), is(10)); + assertThat(convertedDateTime.getDayOfMonth(), is(12)); + assertThat(convertedDateTime.getHour(), anyOf(is(7), is(0))); + } + + @ParameterizedTest + @MethodSource("trueValues") + void givenValuesConsideratedTrueAndOnOffTargetReturnOn(Object value) { + var converted = instance.convertValue(value, OnOffType.class); + assertThat(converted, instanceOf(OnOffType.class)); + assertThat(converted, is(OnOffType.ON)); + } + + @ParameterizedTest + @MethodSource("falseValues") + void givenValuesConsideratedFalseAndOnOffTargetReturnOff(Object value) { + var converted = instance.convertValue(value, OnOffType.class); + assertThat(converted, instanceOf(OnOffType.class)); + assertThat(converted, is(OnOffType.OFF)); + } + + @ParameterizedTest + @MethodSource("trueValues") + void givenValuesConsideratedTrueAndOpenClosedTargetReturnOpen(Object value) { + var converted = instance.convertValue(value, OpenClosedType.class); + assertThat(converted, instanceOf(OpenClosedType.class)); + assertThat(converted, is(OpenClosedType.OPEN)); + } + + @ParameterizedTest + @MethodSource("falseValues") + void givenValuesConsideratedFalseAndOpenClosedTargetReturnClosed(Object value) { + var converted = instance.convertValue(value, OpenClosedType.class); + assertThat(converted, instanceOf(OpenClosedType.class)); + assertThat(converted, is(OpenClosedType.CLOSED)); + } + + private static Stream trueValues() { + return Stream.of("true", "True", 1, 2, "On", "on", -1, 0.3); + } + + private static Stream falseValues() { + return Stream.of("false", "False", 0, 0.0d, "off", "Off", "", "a value"); + } + + private static Stream provideNumericTypes() { + return Stream.of(1L, 1.2, 1.2f, -1, 0, BIG_DECIMAL_NUMBER); + } + + private static Stream provideValuesOfAllSupportedResultRowTypes() { + return Stream.of("", "String", Boolean.TRUE, 1L, 1.2, 1.2f, BIG_DECIMAL_NUMBER, + "bytes".getBytes(Charset.defaultCharset()), Instant.now(), new Date(), Duration.ofDays(1)); + } + + private static Stream provideValuesOfAllSupportedResultRowTypesExceptBytes() { + return provideValuesOfAllSupportedResultRowTypes().filter(o -> !(o instanceof byte[])); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java new file mode 100644 index 0000000000000..817d6d240ab7a --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class StringSubstitutionParamsParserTest { + + @Test + public void testMultipleParameters() { + String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> fill( value: ${fillValue})"; + var parser = new StringSubstitutionParamsParser(query); + QueryParameters parameters = new QueryParameters(Map.of("start", "0", "fillValue", "1")); + + var result = parser.getQueryWithParametersReplaced(parameters); + + assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: 0) |> fill( value: 1)")); + } + + @Test + public void testRepeatedParameter() { + String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> limit(n:${start})"; + var parser = new StringSubstitutionParamsParser(query); + QueryParameters parameters = new QueryParameters(Map.of("start", "0")); + + var result = parser.getQueryWithParametersReplaced(parameters); + + assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: 0) |> limit(n:0)")); + } + + @Test + public void testNullAndNotDefinedParametersAreSubstitutedByEmptyString() { + String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> limit(n:${start})"; + var parser = new StringSubstitutionParamsParser(query); + var paramMap = new HashMap(); + paramMap.put("start", null); + QueryParameters parameters = new QueryParameters(paramMap); + + var result = parser.getQueryWithParametersReplaced(parameters); + + assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: ) |> limit(n:)")); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java new file mode 100644 index 0000000000000..17595fbded9ba --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.Is.is; + +import org.eclipse.jdt.annotation.DefaultLocation; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration; +import org.openhab.binding.dbquery.internal.domain.Query; +import org.openhab.binding.dbquery.internal.domain.QueryParameters; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault(value = { DefaultLocation.PARAMETER }) +class Influx2DatabaseTest { + private Influx2Database instance; + + @BeforeEach + public void setup() { + instance = new Influx2Database(new InfluxDB2BridgeConfiguration(), new InfluxDBClientFacadeMock()); + } + + @AfterEach + public void clearDown() { + instance = null; + } + + @Test + public void givenQueryThatReturnsScalarResultGetValidScalarResult() throws Exception { + instance.connect().get(); + Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.SCALAR_QUERY, QueryParameters.EMPTY, + null); + var future = instance.executeQuery(query); + var queryResult = future.get(); + + assertThat(queryResult, notNullValue()); + assertThat(queryResult.isCorrect(), is(true)); + assertThat(queryResult.getData(), hasSize(1)); + assertThat(queryResult.getData().get(0).getColumnsSize(), is(1)); + } + + @Test + public void givenQueryThatReturnsMultipleRowsGetValidQueryResult() throws Exception { + instance.connect().get(); + Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.MULTIPLE_ROWS_QUERY, + QueryParameters.EMPTY, null); + var future = instance.executeQuery(query); + var queryResult = future.get(); + + assertThat(queryResult, notNullValue()); + assertThat(queryResult.isCorrect(), is(true)); + assertThat(queryResult.getData(), hasSize(InfluxDBClientFacadeMock.MULTIPLE_ROWS_SIZE)); + assertThat("contains expected result data", queryResult.getData().stream().allMatch(row -> { + var value = (String) row.getValue(InfluxDBClientFacadeMock.VALUE_COLUMN); + var time = row.getValue(InfluxDBClientFacadeMock.TIME_COLUMN); + return value != null && value.contains(InfluxDBClientFacadeMock.MULTIPLE_ROWS_VALUE_PREFIX) && time != null; + })); + } + + @Test + public void givenQueryThatReturnsErrorGetErroneusResult() throws Exception { + instance.connect().get(); + Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.INVALID_QUERY, QueryParameters.EMPTY, + null); + var future = instance.executeQuery(query); + var queryResult = future.get(); + + assertThat(queryResult, notNullValue()); + assertThat(queryResult.isCorrect(), equalTo(false)); + assertThat(queryResult.getData(), is(empty())); + } + + @Test + public void givenQueryThatReturnsNoRowsGetEmptyResult() throws Exception { + instance.connect().get(); + Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.EMPTY_QUERY, QueryParameters.EMPTY, + null); + var future = instance.executeQuery(query); + var queryResult = future.get(); + + assertThat(queryResult, notNullValue()); + assertThat(queryResult.isCorrect(), equalTo(true)); + assertThat(queryResult.getData(), is(empty())); + } + + @Test + public void givenNotConnectedClientShouldGetIncorrectQuery() { + Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.SCALAR_QUERY, QueryParameters.EMPTY, + null); + var future = instance.executeQuery(query); + assertThat(future.isCompletedExceptionally(), is(Boolean.TRUE)); + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java new file mode 100644 index 0000000000000..3e7cb255c744d --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.dbimpl.influx2; + +import static org.mockito.Mockito.mock; + +import java.time.Instant; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.dbquery.internal.error.DatabaseException; + +import com.influxdb.Cancellable; +import com.influxdb.query.FluxRecord; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +public class InfluxDBClientFacadeMock implements InfluxDBClientFacade { + public static final String INVALID_QUERY = "invalid"; + public static final String EMPTY_QUERY = "empty"; + public static final String SCALAR_QUERY = "scalar"; + public static final String MULTIPLE_ROWS_QUERY = "multiple"; + + public static final String SCALAR_RESULT = "scalarResult"; + public static final int MULTIPLE_ROWS_SIZE = 3; + public static final String VALUE_COLUMN = "_value"; + public static final String TIME_COLUMN = "_time"; + public static final String MULTIPLE_ROWS_VALUE_PREFIX = "value"; + + boolean connected; + + @Override + public boolean connect() { + connected = true; + return true; + } + + @Override + public boolean isConnected() { + return connected; + } + + @Override + public boolean disconnect() { + connected = false; + return true; + } + + @Override + public void query(String queryString, BiConsumer onNext, + Consumer onError, Runnable onComplete) { + if (!connected) { + throw new DatabaseException("Client not connected"); + } + + if (INVALID_QUERY.equals(queryString)) { + onError.accept(new RuntimeException("Invalid query")); + } else if (EMPTY_QUERY.equals(queryString)) { + onComplete.run(); + } else if (SCALAR_QUERY.equals(queryString)) { + FluxRecord scalar = new FluxRecord(0); + scalar.getValues().put("result", "_result"); + scalar.getValues().put("table", 0); + scalar.getValues().put(VALUE_COLUMN, SCALAR_RESULT); + onNext.accept(mock(Cancellable.class), scalar); + onComplete.run(); + } else if (MULTIPLE_ROWS_QUERY.equals(queryString)) { + onNext.accept(mock(Cancellable.class), createRowRecord(0, MULTIPLE_ROWS_VALUE_PREFIX + 1)); + onNext.accept(mock(Cancellable.class), createRowRecord(0, MULTIPLE_ROWS_VALUE_PREFIX + 2)); + onNext.accept(mock(Cancellable.class), createRowRecord(1, MULTIPLE_ROWS_VALUE_PREFIX + 3)); + onComplete.run(); + } + } + + private static FluxRecord createRowRecord(int table, String value) { + FluxRecord record = new FluxRecord(0); + record.getValues().put("result", "_result"); + record.getValues().put("table", table); + record.getValues().put(VALUE_COLUMN, value); + record.getValues().put(TIME_COLUMN, Instant.now()); + record.getValues().put("_start", Instant.now()); + record.getValues().put("_stop", Instant.now()); + record.getValues().put("_measurement", "measurementName"); + return record; + } +} diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java new file mode 100644 index 0000000000000..c99f59c1e3b9c --- /dev/null +++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.dbquery.internal.domain; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.lessThan; + +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.google.gson.JsonParser; + +/** + * + * @author Joan Pujol - Initial contribution + */ +@NonNullByDefault +class QueryResultJSONEncoderTest { + public static final double TOLERANCE = 0.001d; + private final DBQueryJSONEncoder instance = new DBQueryJSONEncoder(); + private final Gson gson = new Gson(); + private final JsonParser jsonParser = new JsonParser(); + + @Test + void givenQueryResultIsSerializedToJson() { + String json = instance.encode(givenQueryResultWithResults()); + + assertThat(jsonParser.parse(json), notNullValue()); + } + + @Test + @SuppressWarnings({ "rawtypes", "unchecked" }) + void givenQueryResultItsContentIsCorrectlySerializedToJson() { + String json = instance.encode(givenQueryResultWithResults()); + + Map map = gson.fromJson(json, Map.class); + assertThat(map, Matchers.hasEntry("correct", Boolean.TRUE)); + assertThat(map, Matchers.hasKey("data")); + List data = (List) map.get("data"); + assertThat(data, Matchers.hasSize(2)); + Map firstRow = data.get(0); + + assertReadGivenValuesDecodedFromJson(firstRow); + } + + private void assertReadGivenValuesDecodedFromJson(Map firstRow) { + assertThat(firstRow.get("strValue"), is("an string")); + + Object doubleValue = firstRow.get("doubleValue"); + assertThat(doubleValue, instanceOf(Number.class)); + assertThat(((Number) doubleValue).doubleValue(), closeTo(2.3d, TOLERANCE)); + + Object intValue = firstRow.get("intValue"); + assertThat(intValue, instanceOf(Number.class)); + assertThat(((Number) intValue).intValue(), is(3)); + + Object longValue = firstRow.get("longValue"); + assertThat(longValue, instanceOf(Number.class)); + assertThat(((Number) longValue).longValue(), is(Long.MAX_VALUE)); + + Object date = Objects.requireNonNull(firstRow.get("date")); + assertThat(date, instanceOf(String.class)); + var parsedDate = Instant.from(DateTimeFormatter.ISO_INSTANT.parse((String) date)); + assertThat(Duration.between(parsedDate, Instant.now()).getSeconds(), lessThan(10L)); + + Object instant = Objects.requireNonNull(firstRow.get("instant")); + assertThat(instant, instanceOf(String.class)); + var parsedInstant = Instant.from(DateTimeFormatter.ISO_INSTANT.parse((String) instant)); + assertThat(Duration.between(parsedInstant, Instant.now()).getSeconds(), lessThan(10L)); + + assertThat(firstRow.get("booleanValue"), is(Boolean.TRUE)); + assertThat(firstRow.get("object"), is("an object")); + } + + @Test + @SuppressWarnings({ "unchecked" }) + void givenQueryResultWithIncorrectResultItsContentIsCorrectlySerializedToJson() { + String json = instance.encode(QueryResult.ofIncorrectResult("Incorrect")); + + Map map = gson.fromJson(json, Map.class); + assertThat(map, Matchers.hasEntry("correct", Boolean.FALSE)); + assertThat(map.get("errorMessage"), is("Incorrect")); + } + + @Test + void givenQueryParametersAreCorrectlySerializedToJson() { + QueryParameters queryParameters = new QueryParameters(givenRowValues()); + + String json = instance.encode(queryParameters); + + Map map = Objects.requireNonNull(gson.fromJson(json, Map.class)); + assertReadGivenValuesDecodedFromJson(map); + } + + private QueryResult givenQueryResultWithResults() { + return QueryResult.of(new ResultRow(givenRowValues()), new ResultRow(givenRowValues())); + } + + private Map givenRowValues() { + Map values = new HashMap<>(); + values.put("strValue", "an string"); + values.put("doubleValue", 2.3d); + values.put("intValue", 3); + values.put("longValue", Long.MAX_VALUE); + values.put("date", new Date()); + values.put("instant", Instant.now()); + values.put("booleanValue", Boolean.TRUE); + values.put("object", new Object() { + @Override + public String toString() { + return "an object"; + } + }); + return values; + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 24e1feb55f47b..0fb3bffa4b80d 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -92,6 +92,7 @@ org.openhab.binding.dali org.openhab.binding.danfossairunit org.openhab.binding.darksky + org.openhab.binding.dbquery org.openhab.binding.deconz org.openhab.binding.denonmarantz org.openhab.binding.digiplex