]> git.basschouten.com Git - openhab-addons.git/commitdiff
[dbquery] Initial contribution (#8780)
authorJoan Pujol <joanpujol@gmail.com>
Sun, 17 Oct 2021 12:33:18 +0000 (14:33 +0200)
committerGitHub <noreply@github.com>
Sun, 17 Oct 2021 12:33:18 +0000 (14:33 +0200)
* Initial commit

Intial work history lost due to the repository shrunk done at c53e4aed2627ec899c083170430399f8925e3345 (intially started from old unshrunked repo)

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Implement reconnect attempts

If database can be connected at bridge initialization schedule retry attempts.
Prevent  query execution scheduling if bridge isn't online

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Minor documentation changes and fix trigger channel name

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Fix NPE bug initializing ThingActions

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Implement query actions and another fixes

Implement actions to execute query and get last query result
Correctly serialize as JSON non scalar results to result channels

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update parameters and correct channel

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Fix formatting and forgot part on previous commit

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Improve documentation

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Add javadoc comment and license to all classes

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Code cleanup

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Untrack unused i18n file

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Fix log level for query actions trace information

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Add dbquery addon to bundles pom

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Temporary remove mqtt bindings that make travis build to fail

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Fix formatting issue

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Revert "Temporary remove mqtt bindings that make travis build to fail"

This reverts commit 21c09957b5850230e1cf1bd2a83a42a088641a45.

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Code clean up from static analysis

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update code to be compatible with 3.1.0

Update dependencies version
Update Copyright
Other minor changes for new static analysis validations.
Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Requested PR changes

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Apply suggestions from code review

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Suggestions from code review

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update parent version

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/jdbc-bridge.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* Changes asked in PR review

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>
* README documentation imporovements

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
* Fix format issue

Signed-off-by: Joan Pujol <joanpujol@gmail.com>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
49 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.dbquery/README.md [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java [new file with mode: 0644]
bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java [new file with mode: 0644]
bundles/pom.xml

index 7a0fd608b6919f94a7653fc2d2543a07f0b6861a..eac0069afb549b80dd2a5843ad75a313d062e92d 100644 (file)
@@ -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
index 4353c4027c1396930859b51a8fd771c4510f6f27..85e2220f5551fd6140e8291242e6a1538202d32c 100644 (file)
       <artifactId>org.openhab.binding.darksky</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.dbquery</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.deconz</artifactId>
diff --git a/bundles/org.openhab.binding.dbquery/README.md b/bundles/org.openhab.binding.dbquery/README.md
new file mode 100644 (file)
index 0000000..787bf06
--- /dev/null
@@ -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<String,Object>: 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<Map<String,Object>>): 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 (file)
index 0000000..0ac0787
--- /dev/null
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.2.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.dbquery</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: DBQuery Binding</name>
+
+  <properties>
+    <bnd.importpackage>
+      !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.*
+    </bnd.importpackage>
+  </properties>
+
+  <dependencies>
+    <!-- influxdb-client-java -->
+    <dependency>
+      <groupId>com.influxdb</groupId>
+      <artifactId>influxdb-client-java</artifactId>
+      <version>1.6.0</version>
+    </dependency>
+    <dependency>
+      <artifactId>influxdb-client-core</artifactId>
+      <groupId>com.influxdb</groupId>
+      <version>1.6.0</version>
+    </dependency>
+    <dependency>
+      <artifactId>converter-gson</artifactId>
+      <groupId>com.squareup.retrofit2</groupId>
+      <version>2.5.0</version>
+    </dependency>
+    <dependency>
+      <artifactId>converter-scalars</artifactId>
+      <groupId>com.squareup.retrofit2</groupId>
+      <version>2.5.0</version>
+    </dependency>
+    <dependency> <!-- also used for querydb library -->
+      <artifactId>gson</artifactId>
+      <groupId>com.google.code.gson</groupId>
+      <version>2.8.5</version>
+    </dependency>
+    <dependency>
+      <artifactId>gson-fire</artifactId>
+      <groupId>io.gsonfire</groupId>
+      <version>1.8.0</version>
+    </dependency>
+    <dependency>
+      <artifactId>okio</artifactId>
+      <groupId>com.squareup.okio</groupId>
+      <version>1.17.3</version>
+    </dependency>
+    <dependency>
+      <artifactId>commons-csv</artifactId>
+      <groupId>org.apache.commons</groupId>
+      <version>1.6</version>
+    </dependency>
+    <dependency>
+      <artifactId>json</artifactId>
+      <groupId>org.json</groupId>
+      <version>20180813</version>
+    </dependency>
+    <dependency>
+      <artifactId>okhttp</artifactId>
+      <groupId>com.squareup.okhttp3</groupId>
+      <version>3.14.4</version>
+    </dependency>
+    <dependency>
+      <artifactId>retrofit</artifactId>
+      <groupId>com.squareup.retrofit2</groupId>
+      <version>2.6.2</version>
+    </dependency>
+    <dependency>
+      <artifactId>jsr305</artifactId>
+      <groupId>com.google.code.findbugs</groupId>
+      <version>3.0.2</version>
+    </dependency>
+    <dependency>
+      <artifactId>logging-interceptor</artifactId>
+      <groupId>com.squareup.okhttp3</groupId>
+      <version>3.14.4</version>
+    </dependency>
+    <dependency>
+      <artifactId>rxjava</artifactId>
+      <groupId>io.reactivex.rxjava2</groupId>
+      <version>2.2.17</version>
+    </dependency>
+    <dependency>
+      <artifactId>reactive-streams</artifactId>
+      <groupId>org.reactivestreams</groupId>
+      <version>1.0.3</version>
+    </dependency>
+    <dependency>
+      <artifactId>swagger-annotations</artifactId>
+      <groupId>io.swagger</groupId>
+      <version>1.5.22</version>
+    </dependency>
+    <!-- end influxdb-client-java -->
+  </dependencies>
+</project>
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 (file)
index 0000000..23cd424
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.dbquery-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-dbquery" description="DBQuery Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dbquery/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..0973e6b
--- /dev/null
@@ -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<Map<String, @Nullable Object>> data = Collections.emptyList();
+
+    public ActionQueryResult(boolean correct, @Nullable List<Map<String, @Nullable Object>> data) {
+        this.correct = correct;
+        if (data != null) {
+            this.data = data;
+        }
+    }
+
+    public boolean isCorrect() {
+        return correct;
+    }
+
+    public List<Map<String, @Nullable Object>> 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 (file)
index 0000000..958c4d2
--- /dev/null
@@ -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<String, @Nullable Object> 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<String, @Nullable Object> resultRow2Map(ResultRow resultRow) {
+        Map<String, @Nullable Object> 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<String, @Nullable Object> 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 (file)
index 0000000..4a2fa70
--- /dev/null
@@ -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<String, @Nullable Object> parameters, int timeoutInSeconds);
+
+    ActionQueryResult getLastQueryResult();
+
+    void setQueryParameters(Map<String, @Nullable Object> 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 (file)
index 0000000..cbdb14e
--- /dev/null
@@ -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 (file)
index 0000000..68037ab
--- /dev/null
@@ -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<Channel> 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 (file)
index 0000000..a497b47
--- /dev/null
@@ -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 (file)
index 0000000..5495dd3
--- /dev/null
@@ -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<ThingTypeUID> 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 (file)
index 0000000..c2ffba2
--- /dev/null
@@ -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<Class<? extends ThingHandlerService>> 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 (file)
index 0000000..984293c
--- /dev/null
@@ -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 (file)
index 0000000..58a741a
--- /dev/null
@@ -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 (file)
index 0000000..63fcde4
--- /dev/null
@@ -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 (file)
index 0000000..652f25d
--- /dev/null
@@ -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<String, @Nullable Object> 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<Class<? extends ThingHandlerService>> getServices() {
+        return List.of(DBQueryActions.class);
+    }
+
+    public QueryResult getLastQueryResult() {
+        return lastQueryResult;
+    }
+
+    private List<Channel> 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 (file)
index 0000000..fe88f08
--- /dev/null
@@ -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<? extends State> targetType = calculateItemType(channel);
+            State state = value2StateConverter.convertValue(extractedResult, targetType);
+            channelStateUpdater.updateChannelState(channel, state);
+        }
+    }
+
+    private Class<? extends State> 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 (file)
index 0000000..6226ee2
--- /dev/null
@@ -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<? extends State> 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 (file)
index 0000000..c9a1fb6
--- /dev/null
@@ -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 (file)
index 0000000..48cff74
--- /dev/null
@@ -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 (file)
index 0000000..0cd636c
--- /dev/null
@@ -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 (file)
index 0000000..cdbf1d9
--- /dev/null
@@ -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<Boolean> connect() {
+        return CompletableFuture.supplyAsync(() -> {
+            synchronized (Influx2Database.this) {
+                return client.connect();
+            }
+        }, executors);
+    }
+
+    @Override
+    public CompletableFuture<Boolean> disconnect() {
+        return CompletableFuture.supplyAsync(() -> {
+            synchronized (Influx2Database.this) {
+                return client.disconnect();
+            }
+        }, executors);
+    }
+
+    @Override
+    public QueryFactory queryFactory() throws DatabaseException {
+        return queryFactory;
+    }
+
+    @Override
+    public CompletableFuture<QueryResult> executeQuery(Query query) {
+        try {
+            if (query instanceof Influx2QueryFactory.Influx2Query) {
+                Influx2QueryFactory.Influx2Query influxQuery = (Influx2QueryFactory.Influx2Query) query;
+
+                CompletableFuture<QueryResult> asyncResult = new CompletableFuture<>();
+                List<FluxRecord> 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 (file)
index 0000000..790a545
--- /dev/null
@@ -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 (file)
index 0000000..2e16e00
--- /dev/null
@@ -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<List<FluxRecord>, QueryResult> {
+
+    @Override
+    public QueryResult apply(List<FluxRecord> records) {
+        var rows = records.stream().map(Influx2QueryResultExtractor::mapRecord2Row).collect(Collectors.toList());
+        return QueryResult.of(rows);
+    }
+
+    private static ResultRow mapRecord2Row(FluxRecord record) {
+        Map<String, @Nullable Object> 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 (file)
index 0000000..5dd9424
--- /dev/null
@@ -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<Cancellable, FluxRecord> onNext, Consumer<? super Throwable> 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 (file)
index 0000000..1bf86d8
--- /dev/null
@@ -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<Cancellable, FluxRecord> onNext, Consumer<? super Throwable> 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 (file)
index 0000000..c5bfb70
--- /dev/null
@@ -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<QueryResult> {
+        @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<ResultRow> {
+        @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<QueryParameters> {
+        @Override
+        public JsonElement serialize(QueryParameters src, Type typeOfSrc, JsonSerializationContext context) {
+            JsonObject jsonObject = new JsonObject();
+            for (Map.Entry<String, @Nullable Object> 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 (file)
index 0000000..58a1709
--- /dev/null
@@ -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<Boolean> connect();
+
+    CompletableFuture<Boolean> disconnect();
+
+    QueryFactory queryFactory() throws DatabaseException;
+
+    CompletableFuture<QueryResult> executeQuery(Query query);
+
+    Database EMPTY = new Database() {
+        @Override
+        public boolean isConnected() {
+            return false;
+        }
+
+        @Override
+        public CompletableFuture<Boolean> connect() {
+            return CompletableFuture.completedFuture(false);
+        }
+
+        @Override
+        public CompletableFuture<Boolean> disconnect() {
+            return CompletableFuture.completedFuture(false);
+        }
+
+        @Override
+        public QueryFactory queryFactory() {
+            return QueryFactory.EMPTY;
+        }
+
+        @Override
+        public CompletableFuture<QueryResult> 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 (file)
index 0000000..a635cff
--- /dev/null
@@ -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<QueryResult> execute(String queryString, Map<String, @Nullable Object> 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<String, @Nullable Object> 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 (file)
index 0000000..cad7bc8
--- /dev/null
@@ -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 (file)
index 0000000..a89ab67
--- /dev/null
@@ -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 (file)
index 0000000..617e12d
--- /dev/null
@@ -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<String, @Nullable Object> params;
+
+    private QueryParameters() {
+        this.params = new HashMap<>();
+    }
+
+    public QueryParameters(Map<String, @Nullable Object> 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<String, @Nullable Object> 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 (file)
index 0000000..c3c4465
--- /dev/null
@@ -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<ResultRow> data;
+
+    private QueryResult(boolean correct, String errorMessage) {
+        this.correct = correct;
+        this.errorMessage = errorMessage;
+        this.data = Collections.emptyList();
+    }
+
+    private QueryResult(List<ResultRow> 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<ResultRow> 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<ResultRow> 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 (file)
index 0000000..3ed8e1f
--- /dev/null
@@ -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 (file)
index 0000000..e404a27
--- /dev/null
@@ -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<String, @Nullable Object> values;
+
+    public ResultRow(String columnName, @Nullable Object value) {
+        this.values = new LinkedHashMap<>();
+        put(columnName, value);
+    }
+
+    public ResultRow(Map<String, @Nullable Object> values) {
+        this.values = new LinkedHashMap<>();
+        values.forEach(this::put);
+    }
+
+    public Set<String> 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 (file)
index 0000000..cc44f67
--- /dev/null
@@ -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 (file)
index 0000000..148853e
--- /dev/null
@@ -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 (file)
index 0000000..b558099
--- /dev/null
@@ -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 (file)
index 0000000..89873ef
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="dbquery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>DBQuery Binding</name>
+       <description>This is the binding for DBQuery that allows to execute native database queries and bind their results to
+               items.</description>
+
+</binding:binding>
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 (file)
index 0000000..7ffc937
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="dbquery"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+       <bridge-type id="influxdb2">
+               <label>InfluxDB2 Bridge</label>
+               <description>The InfluxDB 2.0 represents a connection to a InfluxDB 2.0 server</description>
+
+               <config-description>
+                       <parameter name="url" type="text" required="true">
+                               <context>url</context>
+                               <label>Url</label>
+                               <description>Database url</description>
+                               <default>http://localhost:9999</default>
+                       </parameter>
+                       <parameter name="user" type="text" required="true">
+                               <label>Username</label>
+                               <description>Name of the database user</description>
+                       </parameter>
+                       <parameter name="token" type="text" required="true">
+                               <label>Token</label>
+                               <context>password</context>
+                               <description>Token to authenticate to the database</description>
+                       </parameter>
+                       <parameter name="organization" type="text" required="true">
+                               <label>Organization</label>
+                               <description>Name of the database organization </description>
+                       </parameter>
+                       <parameter name="bucket" type="text" required="true">
+                               <label>Bucket</label>
+                               <description>Name of the database bucket </description>
+                       </parameter>
+               </config-description>
+       </bridge-type>
+</thing:thing-descriptions>
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 (file)
index 0000000..b4a7114
--- /dev/null
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="dbquery"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <thing-type id="query">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="influxdb2"/>
+               </supported-bridge-type-refs>
+               <label>Query Thing</label>
+               <description>Thing that represents a native query</description>
+
+               <channels>
+                       <channel id="execute" typeId="execute-channel"/>
+                       <channel id="resultString" typeId="result-channel-string"/>
+                       <channel id="resultNumber" typeId="result-channel-number"/>
+                       <channel id="resultDateTime" typeId="result-channel-datetime"/>
+                       <channel id="resultContact" typeId="result-channel-contact"/>
+                       <channel id="resultSwitch" typeId="result-channel-switch"/>
+
+                       <channel id="parameters" typeId="parameters-channel"/>
+                       <channel id="correct" typeId="correct-channel"/>
+                       <channel id="calculateParameters" typeId="calculate-parameters-channel"/>
+               </channels>
+
+
+
+               <config-description>
+                       <parameter name="query" type="text" required="true">
+                               <label>Query Definition</label>
+                               <description>Query definition using native query language</description>
+                               <context>script</context>
+                       </parameter>
+                       <parameter name="hasParameters" type="boolean">
+                               <label>Query has Parameters</label>
+                               <description>True if the query has parameters, otherwise false</description>
+                               <default>false</default>
+                       </parameter>
+                       <parameter name="scalarResult" type="boolean">
+                               <label>Scalar Result</label>
+                               <description>True if the query always return only one single scalar value (only one row and one value-column in this
+                                       row), otherwise false</description>
+                               <default>true</default>
+                       </parameter>
+                       <parameter name="scalarColumn" type="text" required="false">
+                               <label>Scalar Column Name</label>
+                               <description>The column's name that is used to extract scalarResult. If only one column is returned this
+                                       parameter
+                                       can be blank</description>
+                       </parameter>
+                       <parameter name="interval" type="integer" min="0">
+                               <label>Interval</label>
+                               <description>
+                                       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.
+                               </description>
+                               <default>0</default>
+                       </parameter>
+                       <parameter name="timeout" type="integer" min="0">
+                               <label>Timeout Query</label>
+                               <description>
+                                       A time-out in seconds to wait for the query result, if it's exceeded result will be discarded.
+                                       Use 0 for
+                                       no timeout
+                               </description>
+                               <default>0</default>
+                       </parameter>
+
+
+               </config-description>
+
+       </thing-type>
+
+       <channel-type id="execute-channel">
+               <item-type>Switch</item-type>
+               <label>Execute Query</label>
+               <description>Send ON to execute the query, the current state tells if the query is running</description>
+       </channel-type>
+       <channel-type id="result-channel-string">
+               <item-type>String</item-type>
+               <label>String Result</label>
+               <description>Execute query and binds result value to channel as a String</description>
+       </channel-type>
+       <channel-type id="result-channel-number">
+               <item-type>Number</item-type>
+               <label>Number Result</label>
+               <description>Execute query and binds result value to channel as a Number</description>
+       </channel-type>
+       <channel-type id="result-channel-datetime">
+               <item-type>DateTime</item-type>
+               <label>DateTime Result</label>
+               <description>Execute query and binds result value to channel as a DateTime</description>
+       </channel-type>
+       <channel-type id="result-channel-contact">
+               <item-type>DateTime</item-type>
+               <label>Contact Result</label>
+               <description>Execute query and binds result value to channel as a Contact</description>
+       </channel-type>
+       <channel-type id="result-channel-switch">
+               <item-type>Switch</item-type>
+               <label>Switch Result</label>
+               <description>Execute query and binds result value to channel as a Switch</description>
+       </channel-type>
+       <channel-type id="parameters-channel">
+               <item-type>String</item-type>
+               <label>JSON Result</label>
+       </channel-type>
+       <channel-type id="correct-channel">
+               <item-type>Switch</item-type>
+               <label>Last Query Worked</label>
+               <description>True if last query executed correctly</description>
+       </channel-type>
+       <channel-type id="calculate-parameters-channel">
+               <kind>trigger</kind>
+               <label>Calculate Parameters</label>
+               <description>Event to calculate query parameters</description>
+               <event>
+                       <options>
+                               <option value="START">START</option>
+                       </options>
+               </event>
+       </channel-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..b1f653a
--- /dev/null
@@ -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 (file)
index 0000000..badca06
--- /dev/null
@@ -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<State> 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<Object> trueValues() {
+        return Stream.of("true", "True", 1, 2, "On", "on", -1, 0.3);
+    }
+
+    private static Stream<Object> falseValues() {
+        return Stream.of("false", "False", 0, 0.0d, "off", "Off", "", "a value");
+    }
+
+    private static Stream<Number> provideNumericTypes() {
+        return Stream.of(1L, 1.2, 1.2f, -1, 0, BIG_DECIMAL_NUMBER);
+    }
+
+    private static Stream<Object> 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<Object> 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 (file)
index 0000000..817d6d2
--- /dev/null
@@ -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<String, @Nullable Object>();
+        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 (file)
index 0000000..17595fb
--- /dev/null
@@ -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 (file)
index 0000000..3e7cb25
--- /dev/null
@@ -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<Cancellable, FluxRecord> onNext,
+            Consumer<? super Throwable> 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 (file)
index 0000000..c99f59c
--- /dev/null
@@ -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<String, Object> map = gson.fromJson(json, Map.class);
+        assertThat(map, Matchers.hasEntry("correct", Boolean.TRUE));
+        assertThat(map, Matchers.hasKey("data"));
+        List<Map> data = (List<Map>) 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<String, Object> 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<String, @Nullable Object> givenRowValues() {
+        Map<String, @Nullable Object> 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;
+    }
+}
index 83ed47c63665b71857a56f68700663eb3ee400ea..c9fa247654a58c051dc441b61d1c2dd689b3ba54 100644 (file)
@@ -92,6 +92,7 @@
     <module>org.openhab.binding.dali</module>
     <module>org.openhab.binding.danfossairunit</module>
     <module>org.openhab.binding.darksky</module>
+    <module>org.openhab.binding.dbquery</module>
     <module>org.openhab.binding.deconz</module>
     <module>org.openhab.binding.denonmarantz</module>
     <module>org.openhab.binding.digiplex</module>