diff --git a/build.gradle.kts b/build.gradle.kts index 5adbee0962..af3cffd25f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { edgeModule("com.hivemq:hivemq-edge-module-plc4x") edgeModule("com.hivemq:hivemq-edge-module-opcua") edgeModule("com.hivemq:hivemq-edge-module-modbus") + edgeModule("com.hivemq:hivemq-edge-module-postgresql") } val hivemqEdgeZip by tasks.registering(Zip::class) { @@ -109,7 +110,8 @@ val edgeProjectsToUpdate = setOf( "hivemq-edge-module-http", "hivemq-edge-module-modbus", "hivemq-edge-module-opcua", - "hivemq-edge-module-plc4x" + "hivemq-edge-module-plc4x", + "hivemq-edge-module-postgresql" ) tasks.register("updateDependantVersions") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 922f0401fe..1e3df8a9d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ dropwizard-metrics = "4.2.30" equalsVerifier = "3.18.2" future-converter = "1.2.0" guava = "32.1.3-jre" +hikari = "6.2.1" hivemq-edge-adapterSdk = "2024.5" hivemq-edge-extensionSdk = "2024.5" hivemq-extensionSdk = "4.30.0" @@ -39,6 +40,9 @@ mqtt-sn-codec = "838f51d691" netty = "4.1.117.Final" org_json = "20250107" pmd = "6.55.0" +postgresql = "42.7.3" +mysql= "8.0.33" +mssql="12.8.1.jre11" shrinkwrap = "1.2.6" slf4j = "2.0.16" spotBugs = "4.8.5" @@ -75,6 +79,7 @@ jersey-hk2 = { module = "org.glassfish.jersey.inject:jersey-hk2", version.ref = jersey-media-json-jackson = { module = "org.glassfish.jersey.media:jersey-media-json-jackson", version.ref = "jersey" } jersey-media-multipart = { module = "org.glassfish.jersey.media:jersey-media-multipart", version.ref = "jersey" } guava = { module = "com.google.guava:guava", version.ref = "guava" } +hikari = { module = "com.zaxxer:HikariCP", version.ref = "hikari" } hivemq-edge-adapterSdk = { module = "com.hivemq:hivemq-edge-adapter-sdk", version.ref = "hivemq-edge-adapterSdk" } hivemq-edge-extensionSdk = { module = "com.hivemq:hivemq-edge-extension-sdk", version.ref = "hivemq-edge-extensionSdk" } hivemq-extensionSdk = { module = "com.hivemq:hivemq-extension-sdk", version.ref = "hivemq-extensionSdk" } @@ -111,6 +116,9 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" netty-commons = { module = "io.netty:netty-common", version.ref = "netty" } netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport = { module = "io.netty:netty-transport", version.ref = "netty" } +postgresql= { module = "org.postgresql:postgresql", version.ref = "postgresql" } +mysql= { module = "mysql:mysql-connector-java", version.ref = "mysql" } +mssql= { module = "com.microsoft.sqlserver:mssql-jdbc", version.ref = "mssql" } org_json = { module = "org.json:json", version.ref = "org_json" } plc4j-api = { module = "org.apache.plc4x:plc4j-api", version.ref = "apache-plc4x" } plc4j-s7 = { module = "org.apache.plc4x:plc4j-driver-s7", version.ref = "apache-plc4x" } diff --git a/modules/hivemq-edge-module-databases/.idea/.gitignore b/modules/hivemq-edge-module-databases/.idea/.gitignore new file mode 100644 index 0000000000..26d33521af --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/modules/hivemq-edge-module-databases/.idea/.name b/modules/hivemq-edge-module-databases/.idea/.name new file mode 100644 index 0000000000..0316ac6373 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/.name @@ -0,0 +1 @@ +hivemq-postgresql-protocol-adapter \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/compiler.xml b/modules/hivemq-edge-module-databases/.idea/compiler.xml new file mode 100644 index 0000000000..fb7f4a8a46 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/gradle.xml b/modules/hivemq-edge-module-databases/.idea/gradle.xml new file mode 100644 index 0000000000..f9163b40e6 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/gradle.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/jarRepositories.xml b/modules/hivemq-edge-module-databases/.idea/jarRepositories.xml new file mode 100644 index 0000000000..a529ef2a03 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/misc.xml b/modules/hivemq-edge-module-databases/.idea/misc.xml new file mode 100644 index 0000000000..25d34a4744 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/misc.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/modules.xml b/modules/hivemq-edge-module-databases/.idea/modules.xml new file mode 100644 index 0000000000..8afaf2bcf5 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/.idea/vcs.xml b/modules/hivemq-edge-module-databases/.idea/vcs.xml new file mode 100644 index 0000000000..b2bdec2d71 --- /dev/null +++ b/modules/hivemq-edge-module-databases/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/HEADER b/modules/hivemq-edge-module-databases/HEADER new file mode 100644 index 0000000000..6e731e9277 --- /dev/null +++ b/modules/hivemq-edge-module-databases/HEADER @@ -0,0 +1,13 @@ +Copyright 2023-present HiveMQ GmbH + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/modules/hivemq-edge-module-databases/build.gradle.kts b/modules/hivemq-edge-module-databases/build.gradle.kts new file mode 100644 index 0000000000..101c419f88 --- /dev/null +++ b/modules/hivemq-edge-module-databases/build.gradle.kts @@ -0,0 +1,216 @@ +import nl.javadude.gradle.plugins.license.DownloadLicensesExtension.license +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent.* + +plugins { + java + alias(libs.plugins.defaults) + alias(libs.plugins.shadow) + alias(libs.plugins.license) + id("com.hivemq.edge-version-updater") + id("com.hivemq.third-party-license-generator") +} + + +group = "com.hivemq" + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +repositories { + mavenCentral() + maven { url = uri("https://jitpack.io") } + exclusiveContent { + forRepository { + maven { + url = uri("https://jitpack.io") + } + } + filter { + includeGroup("com.github.simon622.mqtt-sn") + includeGroup("com.github.simon622") + } + } +} + + +dependencies { + implementation(libs.hikari) + compileOnly(libs.hivemq.edge.adapterSdk) + compileOnly(libs.apache.commonsIO) + compileOnly(libs.jackson.databind) + compileOnly(libs.slf4j.api) + implementation(libs.postgresql) + implementation(libs.mysql) + implementation(libs.mssql) +} + +dependencies { + testImplementation("com.hivemq:hivemq-edge") + testImplementation(libs.jackson.databind) + testImplementation(libs.hivemq.edge.adapterSdk) + testImplementation(libs.apache.commonsIO) + testImplementation(libs.mockito.junitJupiter) + testImplementation(libs.junit.jupiter) + testImplementation(libs.milo.server) + testImplementation(libs.assertj) + testImplementation(libs.awaitility) +} + +tasks.test { + useJUnitPlatform() + testLogging { + events = setOf(STARTED, PASSED, FAILED, SKIPPED, STANDARD_ERROR) + exceptionFormat = TestExceptionFormat.FULL + } +} + +tasks.register("copyAllDependencies") { + shouldRunAfter("assemble") + from(configurations.runtimeClasspath) + into("${buildDir}/deps/libs") +} + +tasks.named("assemble") { finalizedBy("copyAllDependencies") } + +/* ******************** artifacts ******************** */ + +val releaseBinary: Configuration by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named("binary")) + attribute(Usage.USAGE_ATTRIBUTE, objects.named("release")) + } +} + +val thirdPartyLicenses: Configuration by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named("third-party-licenses")) + } +} + +artifacts { + add(releaseBinary.name, tasks.shadowJar) + add(thirdPartyLicenses.name, tasks.updateThirdPartyLicenses.flatMap { it.outputDirectory }) +} +/* ******************** compliance ******************** */ + +license { + header = file("HEADER") + mapping("java", "SLASHSTAR_STYLE") +} + +downloadLicenses { + aliases = mapOf( + license("Apache License, Version 2.0", "https://opensource.org/licenses/Apache-2.0") to listOf( + "Apache 2", + "Apache 2.0", + "Apache-2.0", + "Apache License 2.0", + "Apache License, 2.0", + "Apache License v2.0", + "Apache License, Version 2", + "Apache License Version 2.0", + "Apache License, Version 2.0", + "Apache License, version 2.0", + "The Apache License, Version 2.0", + "Apache Software License - Version 2.0", + "Apache Software License, version 2.0", + "The Apache Software License, Version 2.0" + ), + license("MIT License", "https://opensource.org/licenses/MIT") to listOf( + "MIT License", + "MIT license", + "The MIT License", + "The MIT License (MIT)" + ), + license("CDDL, Version 1.0", "https://opensource.org/licenses/CDDL-1.0") to listOf( + "CDDL, Version 1.0", + "Common Development and Distribution License 1.0", + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0", + license("CDDL", "https://glassfish.dev.java.net/public/CDDLv1.0.html") + ), + license("CDDL, Version 1.1", "https://oss.oracle.com/licenses/CDDL+GPL-1.1") to listOf( + "CDDL 1.1", + "CDDL, Version 1.1", + "Common Development And Distribution License 1.1", + "CDDL+GPL License", + "CDDL + GPLv2 with classpath exception", + "Dual license consisting of the CDDL v1.1 and GPL v2", + "CDDL or GPLv2 with exceptions", + "CDDL/GPLv2+CE" + ), + license("LGPL, Version 2.0", "https://opensource.org/licenses/LGPL-2.0") to listOf( + "LGPL, Version 2.0", + "GNU General Public License, version 2" + ), + license("LGPL, Version 2.1", "https://opensource.org/licenses/LGPL-2.1") to listOf( + "LGPL, Version 2.1", + "LGPL, version 2.1", + "GNU Lesser General Public License version 2.1 (LGPLv2.1)", + license("GNU Lesser General Public License", "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html") + ), + license("LGPL, Version 3.0", "https://opensource.org/licenses/LGPL-3.0") to listOf( + "LGPL, Version 3.0", + "Lesser General Public License, version 3 or greater" + ), + license("EPL, Version 1.0", "https://opensource.org/licenses/EPL-1.0") to listOf( + "EPL, Version 1.0", + "Eclipse Public License - v 1.0", + "Eclipse Public License - Version 1.0", + license("Eclipse Public License", "http://www.eclipse.org/legal/epl-v10.html") + ), + license("EPL, Version 2.0", "https://opensource.org/licenses/EPL-2.0") to listOf( + "EPL 2.0", + "EPL, Version 2.0" + ), + license("EDL, Version 1.0", "https://www.eclipse.org/org/documents/edl-v10.php") to listOf( + "EDL 1.0", + "EDL, Version 1.0", + "Eclipse Distribution License - v 1.0" + ), + license("BSD 3-Clause License", "https://opensource.org/licenses/BSD-3-Clause") to listOf( + "BSD 3-clause", + "BSD-3-Clause", + "BSD 3-Clause License", + "3-Clause BSD License", + "New BSD License", + license("BSD", "http://asm.ow2.org/license.html"), + license("BSD", "http://asm.objectweb.org/license.html"), + license("BSD", "LICENSE.txt") + ), + license("Bouncy Castle License", "https://www.bouncycastle.org/licence.html") to listOf( + "Bouncy Castle Licence" + ), + license("W3C License", "https://opensource.org/licenses/W3C") to listOf( + "W3C License", + "W3C Software Copyright Notice and License", + "The W3C Software License" + ), + license("CC0", "https://creativecommons.org/publicdomain/zero/1.0/") to listOf( + "CC0", + "Public Domain" + ) + ) + + dependencyConfiguration = "runtimeClasspath" +} + +tasks.updateThirdPartyLicenses { + dependsOn(tasks.downloadLicenses) + projectName.set(project.name) + group = "license" + dependencyLicense.set(tasks.downloadLicenses.get().xmlDestination.resolve("dependency-license.xml")) + outputDirectory.set(layout.buildDirectory.dir("distribution/third-party-licenses")) +} + +val javaComponent = components["java"] as AdhocComponentWithVariants +javaComponent.withVariantsFromConfiguration(configurations.shadowRuntimeElements.get()) { + skip() +} diff --git a/modules/hivemq-edge-module-databases/gradle.properties b/modules/hivemq-edge-module-databases/gradle.properties new file mode 100644 index 0000000000..04a0629878 --- /dev/null +++ b/modules/hivemq-edge-module-databases/gradle.properties @@ -0,0 +1 @@ +version=2025.1-SNAPSHOT diff --git a/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.jar b/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.jar differ diff --git a/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.properties b/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..91e0fa781f --- /dev/null +++ b/modules/hivemq-edge-module-databases/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 24 08:26:06 CEST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/modules/hivemq-edge-module-databases/gradlew b/modules/hivemq-edge-module-databases/gradlew new file mode 100755 index 0000000000..1b6c787337 --- /dev/null +++ b/modules/hivemq-edge-module-databases/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/modules/hivemq-edge-module-databases/gradlew.bat b/modules/hivemq-edge-module-databases/gradlew.bat new file mode 100644 index 0000000000..107acd32c4 --- /dev/null +++ b/modules/hivemq-edge-module-databases/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/modules/hivemq-edge-module-databases/settings.gradle.kts b/modules/hivemq-edge-module-databases/settings.gradle.kts new file mode 100644 index 0000000000..88f39448ff --- /dev/null +++ b/modules/hivemq-edge-module-databases/settings.gradle.kts @@ -0,0 +1,13 @@ +rootProject.name = "hivemq-edge-module-databases" + +pluginManagement { + includeBuild("../../edge-plugins") +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../../gradle/libs.versions.toml")) + } + } +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabaseConnection.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabaseConnection.java new file mode 100644 index 0000000000..afa3be3362 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabaseConnection.java @@ -0,0 +1,59 @@ +package com.hivemq.edge.adapters.databases; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Properties; + +public class DatabaseConnection { + private final @NotNull HikariConfig config; + private @Nullable HikariDataSource ds; + + public DatabaseConnection(final @NotNull String jdbcUrl, final @NotNull String username, final @NotNull String password, final int connectionTimeout, final boolean encrypt) { + config = new HikariConfig(); + config.setJdbcUrl(jdbcUrl); + config.setUsername(username); + config.setPassword(password); + config.setConnectionTimeout(connectionTimeout * 1000L); + + + String[] dataSource = config.getJdbcUrl().split(":"); + switch (dataSource[1]){ + case "mysql", "postgresql" -> { + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + } + case "sqlserver" -> { + config.setDataSourceClassName("com.microsoft.sqlserver.jdbc.SQLServerDataSource"); + Properties properties = new Properties(); + if (encrypt) { + properties.setProperty("encrypt", "true"); + properties.setProperty("trustServerCertificate", "true"); // Trust the server certificate implicitly + } else properties.setProperty("encrypt", "false"); + config.setDataSourceProperties(properties); + } + } + } + + public void connect() { + this.ds = new HikariDataSource(config); + } + + public @NotNull Connection getConnection() throws SQLException { + if (ds == null) { + throw new IllegalStateException("Hikari Connection Pool must be started before usage."); + } + return ds.getConnection(); + } + + public void close() { + if (ds != null) { + ds.close(); + } + } +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesPollingProtocolAdapter.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesPollingProtocolAdapter.java new file mode 100644 index 0000000000..a83423c6f7 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesPollingProtocolAdapter.java @@ -0,0 +1,277 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; +import com.hivemq.adapter.sdk.api.config.PollingContext; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStartOutput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStopInput; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterStopOutput; +import com.hivemq.adapter.sdk.api.polling.PollingInput; +import com.hivemq.adapter.sdk.api.polling.PollingOutput; +import com.hivemq.adapter.sdk.api.polling.PollingProtocolAdapter; +import com.hivemq.adapter.sdk.api.state.ProtocolAdapterState; +import com.hivemq.adapter.sdk.api.tag.Tag; +import com.hivemq.edge.adapters.databases.config.DatabaseType; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterConfig; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterTag; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterTagDefinition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + + +public class DatabasesPollingProtocolAdapter implements PollingProtocolAdapter { + + private static final @NotNull Logger log = LoggerFactory.getLogger(DatabasesPollingProtocolAdapter.class); + private static final @NotNull ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final int TIMEOUT = 30; + + private final @NotNull DatabasesAdapterConfig adapterConfig; + private final @NotNull ProtocolAdapterInformation adapterInformation; + private final @NotNull ProtocolAdapterState protocolAdapterState; + private final @NotNull String adapterId; + private final @NotNull List tags; + private final @NotNull DatabaseConnection databaseConnection; + + public DatabasesPollingProtocolAdapter( + final @NotNull ProtocolAdapterInformation adapterInformation, + final @NotNull ProtocolAdapterInput input) { + this.adapterId = input.getAdapterId(); + this.adapterInformation = adapterInformation; + this.adapterConfig = input.getConfig(); + this.protocolAdapterState = input.getProtocolAdapterState(); + this.tags = input.getTags(); + + log.debug("Building connection string"); + String compiledUri = getConnectionString(adapterConfig.getType()); + assert compiledUri != null; + log.debug(compiledUri); + this.databaseConnection = new DatabaseConnection(compiledUri, + adapterConfig.getUsername(), + adapterConfig.getPassword(), + adapterConfig.getConnectionTimeout(), + adapterConfig.getEncrypt()); + } + + private @Nullable String getConnectionString(DatabaseType inputType) { + switch (inputType){ + case POSTGRESQL -> { + return String.format("jdbc:postgresql://%s:%s/%s?ssl=%s", + adapterConfig.getServer(), + adapterConfig.getPort(), + adapterConfig.getDatabase(), + adapterConfig.getEncrypt()); + } + case MYSQL -> { + return String.format("jdbc:mysql://%s:%s/%s?allowPublicKeyRetrieval=true&useSSL=%s", + adapterConfig.getServer(), + adapterConfig.getPort(), + adapterConfig.getDatabase(), + adapterConfig.getEncrypt()); + } + case MSSQL -> { + return String.format("jdbc:sqlserver://%s:%s;databaseName=%s;", + adapterConfig.getServer(), + adapterConfig.getPort(), + adapterConfig.getDatabase()); + } + default -> { + return null; + } + } + } + + @Override + public @NotNull String getId() { + return adapterId; + } + + @Override + public void start( + final @NotNull ProtocolAdapterStartInput input, final @NotNull ProtocolAdapterStartOutput output) { + log.debug("Loading PostgreSQL Driver"); + try { + Class.forName("org.postgresql.Driver"); + } catch (final ClassNotFoundException e) { + output.failStart(e, null); + return; + } + + log.debug("Loading MySQL Driver"); + try { + Class.forName("com.mysql.jdbc.Driver"); + } catch (final ClassNotFoundException e) { + output.failStart(e, null); + return; + } + + log.debug("Loading MS SQL Driver"); + try { + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDataSource"); + } catch (final ClassNotFoundException e) { + output.failStart(e, null); + return; + } + + + + databaseConnection.connect(); + + try { + log.debug("Starting connection to the database instance"); + if (databaseConnection.getConnection().isValid(TIMEOUT)) { + output.startedSuccessfully(); + protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.CONNECTED); + } else { + output.failStart(new Throwable("Error connecting database, please check the configuration"), + "Error connecting database, please check the configuration"); + protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.DISCONNECTED); + } + } catch (final Exception e) { + output.failStart(e, null); + protocolAdapterState.setConnectionStatus(ProtocolAdapterState.ConnectionStatus.DISCONNECTED); + } + } + + @Override + public void stop( + final @NotNull ProtocolAdapterStopInput protocolAdapterStopInput, + final @NotNull ProtocolAdapterStopOutput protocolAdapterStopOutput) { + databaseConnection.close(); + protocolAdapterStopOutput.stoppedSuccessfully(); + } + + + @Override + public @NotNull ProtocolAdapterInformation getProtocolAdapterInformation() { + return adapterInformation; + } + + @Override + public void poll(final @NotNull PollingInput pollingInput, final @NotNull PollingOutput pollingOutput) { + log.debug("Getting polling context"); + final PollingContext pollingContext = pollingInput.getPollingContext(); + + /* Connect to the database and execute the query */ + log.debug("Checking database connection state"); + log.debug("Handling tags for the adapter"); + tags.stream() + .filter(tag -> tag.getName().equals(pollingContext.getTagName())) + .findFirst() + .ifPresentOrElse(tag -> loadDataFromDB(pollingOutput, (DatabasesAdapterTag) tag), + () -> pollingOutput.fail("Polling for Database protocol adapter failed because the used tag '" + + pollingInput.getPollingContext().getTagName() + + "' was not found. For the polling to work the tag must be created via REST API or the UI.")); + pollingOutput.finish(); + } + + private void loadDataFromDB(final @NotNull PollingOutput output, final @NotNull DatabasesAdapterTag tag) { + // ARM to ensure the connection is closed afterward + try (final Connection connection = databaseConnection.getConnection()) { + log.debug("Getting tag definition"); + /* Get the tag definition (Query, RowLimit and Split Lines)*/ + final DatabasesAdapterTagDefinition definition = tag.getDefinition(); + + /* Execute query and handle result */ + final PreparedStatement preparedStatement = connection.prepareStatement(tag.getDefinition().getQuery()); + final ResultSet result = preparedStatement.executeQuery(); + assert result != null; + final ArrayList resultObject = new ArrayList<>(); + final ResultSetMetaData resultSetMD = result.getMetaData(); + while (result.next()) { + final int numColumns = resultSetMD.getColumnCount(); + final ObjectNode node = OBJECT_MAPPER.createObjectNode(); + for (int i = 1; i <= numColumns; i++) { + parseAndAddValue(i, result, resultSetMD, node); + } + + /* Publish datapoint with a single line if split is required */ + if (definition.getSpiltLinesInIndividualMessages()) { + log.debug("Splitting lines in multiple messages"); + output.addDataPoint("queryResult", node); + } else { + resultObject.add(node); + } + } + + /* Publish datapoint with all lines if no split is required */ + if (!definition.getSpiltLinesInIndividualMessages()) { + log.debug("Publishing all lines in a single message"); + output.addDataPoint("queryResult", resultObject); + } + } catch (final Exception e) { + output.fail(e, null); + } + } + + // according to https://www.ibm.com/docs/en/db2/11.1?topic=djr-sql-data-type-representation + private void parseAndAddValue( + final int index, + final @NotNull ResultSet result, + final @NotNull ResultSetMetaData resultSetMD, + final @NotNull ObjectNode node) throws SQLException { + final String columnName = resultSetMD.getColumnName(index); + final int columnType = resultSetMD.getColumnType(index); + switch (columnType) { + case Types.BIT: + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + node.put(columnName, result.getInt(index)); + return; + case Types.BIGINT: + node.put(columnName, result.getLong(index)); + return; + case Types.DECIMAL: + node.put(columnName, result.getBigDecimal(index)); + return; + case Types.REAL: + case Types.FLOAT: + case Types.DOUBLE: + case Types.NUMERIC: + node.put(columnName, result.getDouble(index)); + return; + default: + node.put(columnName, result.getString(index)); + } + } + + @Override + public int getPollingIntervalMillis() { + return adapterConfig.getPollingIntervalMillis(); + } + + @Override + public int getMaxPollingErrorsBeforeRemoval() { + return adapterConfig.getMaxPollingErrorsBeforeRemoval(); + } + +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterFactory.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterFactory.java new file mode 100644 index 0000000000..66cc790996 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases; + +import com.hivemq.adapter.sdk.api.ProtocolAdapter; +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; +import com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory; +import com.hivemq.adapter.sdk.api.model.ProtocolAdapterInput; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterConfig; +import org.jetbrains.annotations.NotNull; + +public class DatabasesProtocolAdapterFactory implements ProtocolAdapterFactory { + + @Override + public @NotNull ProtocolAdapterInformation getInformation() { + return DatabasesProtocolAdapterInformation.INSTANCE; + } + + @Override + public @NotNull ProtocolAdapter createAdapter( + final @NotNull ProtocolAdapterInformation adapterInformation, + @NotNull final ProtocolAdapterInput input) { + return new DatabasesPollingProtocolAdapter(adapterInformation, input); + } +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformation.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformation.java new file mode 100644 index 0000000000..f8d8d5c008 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformation.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases; + + +import com.hivemq.adapter.sdk.api.ProtocolAdapterCapability; +import com.hivemq.adapter.sdk.api.ProtocolAdapterCategory; +import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation; +import com.hivemq.adapter.sdk.api.ProtocolAdapterTag; +import com.hivemq.adapter.sdk.api.config.ProtocolSpecificAdapterConfig; +import com.hivemq.adapter.sdk.api.tag.Tag; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterConfig; +import com.hivemq.edge.adapters.databases.config.DatabasesAdapterTag; +import org.apache.commons.io.IOUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.List; + +public class DatabasesProtocolAdapterInformation implements ProtocolAdapterInformation { + + public static final @NotNull ProtocolAdapterInformation INSTANCE = new DatabasesProtocolAdapterInformation(); + private static final @NotNull Logger LOG = LoggerFactory.getLogger(DatabasesProtocolAdapterInformation.class); + + protected DatabasesProtocolAdapterInformation() { + } + + @Override + public @NotNull String getProtocolName() { + // the returned string will be used for logging information on the protocol adapter + return "Databases"; + } + + @Override + public @NotNull String getProtocolId() { + // this id is very important as this is how the adapters configurations in the config.xml are linked to the adapter implementations. + // any change here means you will need to edit the config.xml + return "databases"; + } + + @Override + public @NotNull String getDisplayName() { + // the name for this protocol adapter type that will be displayed within edge's ui + return "Databases Protocol Adapter"; + } + + @Override + public @NotNull String getDescription() { + // the description that will be shown for this protocol adapter within edge's ui + return "This protocol adapter allow you to execute database query on a database (PostgreSQL, MySQL, MSSQL), retrieve the result and send it via MQTT."; + } + + @Override + public @NotNull String getUrl() { + // this url will be displayed in the ui as a link to further documentation on this protocol adapter. + // e.g. this could be a link to the source code and a readme + return "TO BE DEFINED"; + } + + @Override + public @NotNull String getVersion() { + // the version of this protocol adapter, the usage of semantic versioning is advised. + return "${edge-version}-ALPHA"; + } + + @Override + public @NotNull EnumSet getCapabilities() { + // this indicates what capabilities this protocol adapter has. E.g. READ/WRITE. See the ProtocolAdapterCapability enum for more information. + return EnumSet.of(ProtocolAdapterCapability.READ); + } + + @Override + public @NotNull String getLogoUrl() { + // this is a default image that is always available. + return "/images/database.png"; + } + + @Override + public @NotNull String getAuthor() { + // your name/nick + return "HiveMQ"; + } + + @Override + public @Nullable ProtocolAdapterCategory getCategory() { + // this indicates for which use cases this protocol adapter is intended. See the ProtocolAdapterConstants.CATEGORY enum for more information. + return ProtocolAdapterCategory.CONNECTIVITY; + } + + @Override + public List getTags() { + // here you can set which Tags should be applied to this protocol adapter + return List.of(ProtocolAdapterTag.INTERNET, ProtocolAdapterTag.TCP, ProtocolAdapterTag.AUTOMATION); + } + + + @Override + public @Nullable String getUiSchema() { + try (final InputStream is = this.getClass() + .getClassLoader() + .getResourceAsStream("databases-adapter-ui-schema.json")) { + if (is == null) { + LOG.warn("The UISchema for the Databases Adapter could not be loaded from resources: Not found."); + return null; + } + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (final Exception e) { + LOG.warn("The UISchema for the Databases Adapter could not be loaded from resources:", e); + return null; + } + } + + @Override + public int getCurrentConfigVersion() { + return 1; + } + + @Override + public @NotNull Class tagConfigurationClass() { + return DatabasesAdapterTag.class; + } + + @Override + public @NotNull Class configurationClassNorthbound() { + return DatabasesAdapterConfig.class; + } + + @Override + public @NotNull Class configurationClassNorthAndSouthbound() { + return DatabasesAdapterConfig.class; + } + + +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabaseType.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabaseType.java new file mode 100644 index 0000000000..dac69f8e4c --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabaseType.java @@ -0,0 +1,7 @@ +package com.hivemq.edge.adapters.databases.config; + +public enum DatabaseType { + POSTGRESQL, + MYSQL, + MSSQL +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterConfig.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterConfig.java new file mode 100644 index 0000000000..f1ba6b2b03 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterConfig.java @@ -0,0 +1,189 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField; +import com.hivemq.adapter.sdk.api.config.ProtocolSpecificAdapterConfig; +import org.jetbrains.annotations.NotNull; + + +@SuppressWarnings({"unused", "FieldCanBeLocal", "FieldMayBeFinal"}) +@JsonPropertyOrder({ + "url", "destination"}) +public class DatabasesAdapterConfig implements ProtocolSpecificAdapterConfig { + + private static final @NotNull String ID_REGEX = "^([a-zA-Z_0-9-_])*$"; + + @JsonProperty(value = "id", required = true) + @ModuleConfigField(title = "Identifier", + description = "Unique identifier for this protocol adapter", + format = ModuleConfigField.FieldType.IDENTIFIER, + required = true, + stringPattern = ID_REGEX, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String id; + + @JsonProperty(value = "type", required = true) + @ModuleConfigField(title = "Type", + description = "Database type", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull DatabaseType type; + + @JsonProperty(value = "server", required = true) + @ModuleConfigField(title = "Server", + description = "Server address", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String server; + + @JsonProperty(value = "port", required = true) + @ModuleConfigField(title = "Port", + description = "Server port (Default --> PostgreSQL: 5432, MySQL: 3306, MS SQL: 1433)", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringPattern = ID_REGEX, + stringMinLength = 1, + stringMaxLength = 6, + defaultValue = "5432") + protected @NotNull Integer port; + + @JsonProperty(value = "database", required = true) + @ModuleConfigField(title = "Database", + description = "Database name", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringPattern = ID_REGEX, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String database; + + @JsonProperty(value = "username", required = true) + @ModuleConfigField(title = "Username", + description = "Username for the connection to the database", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String username; + + @JsonProperty(value = "password", required = true) + @ModuleConfigField(title = "Password", + description = "Password for the connection to the database", + format = ModuleConfigField.FieldType.UNSPECIFIED, + required = true, + stringMinLength = 1, + stringMaxLength = 1024) + protected @NotNull String password; + + @JsonProperty(value = "encrypt") + @ModuleConfigField(title = "Encrypt", + description = "Use TLS to communicate with the remote database", + format = ModuleConfigField.FieldType.BOOLEAN, + required = true) + protected Boolean encrypt; + + @JsonProperty(value = "trustCertificate") + @ModuleConfigField(title = "Trust Certificate", + description = "Do you want to trust remote certificate", + format = ModuleConfigField.FieldType.BOOLEAN, + required = true) + protected Boolean trustCertificate; + + @JsonProperty(value = "connectionTimeoutSeconds") + @ModuleConfigField(title = "connectionTimeoutSeconds", + description = "The timeout for connection establishment to the database.", + numberMax = 180, + defaultValue = "30") + protected int connectionTimeoutSeconds = 30; + + @JsonProperty("pollingIntervalMillis") + @ModuleConfigField(title = "Polling Interval [ms]", + description = "Time in millisecond that this endpoint will be polled", + numberMin = 1, + defaultValue = "1000") + private int pollingIntervalMillis = 1000; + + @JsonProperty("maxPollingErrorsBeforeRemoval") + @ModuleConfigField(title = "Max. Polling Errors", + description = "Max. errors polling the endpoint before the polling daemon is stopped", + numberMin = -1, + defaultValue = "10") + private int maxPollingErrorsBeforeRemoval = 10; + + + public DatabasesAdapterConfig() { + id = ""; + password = ""; + username = ""; + database = ""; + port = 0; + server = ""; + type = DatabaseType.POSTGRESQL; + encrypt = false; + trustCertificate = false; + } + + public @NotNull DatabaseType getType() { return type;} + + public @NotNull String getServer() { + return server; + } + + public @NotNull String getDatabase() { + return database; + } + + public @NotNull Integer getPort() { + return port; + } + + public @NotNull String getUsername() { + return username; + } + + public @NotNull String getPassword() { + return password; + } + + public @NotNull Boolean getEncrypt() { + return encrypt; + } + + public @NotNull Boolean getTrustCertificate() { + return encrypt; + } + + + public int getPollingIntervalMillis() { + return pollingIntervalMillis; + } + + public int getMaxPollingErrorsBeforeRemoval() { + return maxPollingErrorsBeforeRemoval; + } + + public int getConnectionTimeout() { + return connectionTimeoutSeconds; + } +} diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTag.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTag.java new file mode 100644 index 0000000000..a8904a03db --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTag.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField; +import com.hivemq.adapter.sdk.api.tag.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class DatabasesAdapterTag implements Tag { + + @JsonProperty(value = "name", required = true) + @ModuleConfigField(title = "name", + description = "Name of the tag to be used in mappings", + format = ModuleConfigField.FieldType.MQTT_TAG, + required = true) + private final @NotNull String name; + + @JsonProperty(value = "description") + @ModuleConfigField(title = "description", description = "Description of the tag") + private final @NotNull String description; + + @JsonProperty(value = "definition", required = true) + @ModuleConfigField(title = "definition", description = "The actual definition of the tag for PostgreSQL Query") + private final @NotNull DatabasesAdapterTagDefinition definition; + + + public DatabasesAdapterTag( + @JsonProperty(value = "name", required = true) final @NotNull String name, + @JsonProperty(value = "description") final @Nullable String description, + @JsonProperty(value = "definition", + required = true) final @NotNull DatabasesAdapterTagDefinition definition) { + this.name = name; + this.description = Objects.requireNonNullElse(description, "no description present."); + this.definition = definition; + } + + @Override + public @NotNull DatabasesAdapterTagDefinition getDefinition() { + return definition; + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public @NotNull String getDescription() { + return description; + } + + @Override + public boolean equals(final @Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final DatabasesAdapterTag databasesAdapterTag = (DatabasesAdapterTag) o; + return Objects.equals(name, databasesAdapterTag.name) && + Objects.equals(description, databasesAdapterTag.description) && + Objects.equals(definition, databasesAdapterTag.definition); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, definition); + } +} + + diff --git a/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTagDefinition.java b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTagDefinition.java new file mode 100644 index 0000000000..2079272dbf --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/java/com/hivemq/edge/adapters/databases/config/DatabasesAdapterTagDefinition.java @@ -0,0 +1,71 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases.config; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField; +import com.hivemq.adapter.sdk.api.tag.TagDefinition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class DatabasesAdapterTagDefinition implements TagDefinition { + @JsonProperty(value = "query", required = true) + @ModuleConfigField(title = "Query", + description = "Query to execute on the database", + required = true, + format = ModuleConfigField.FieldType.UNSPECIFIED) + private final @Nullable String query; + + @JsonProperty(value = "rowLimit", required = true) + @ModuleConfigField(title = "Row Limit", + description = "Number of row to retrieve (default 10, maximum 99)", + required = true, + numberMin = 1, + numberMax = 99) + private final int rowLimit; + + @JsonProperty(value = "spiltLinesInIndividualMessages") + @ModuleConfigField(title = "Split lines into individual messages ?", + description = "Select this option to create a single message per line returned by the query (by default all lines are sent in a single message as an array).", + defaultValue = "false") + protected @NotNull Boolean spiltLinesInIndividualMessages; + + public DatabasesAdapterTagDefinition( + @JsonProperty(value = "query") final @Nullable String query, + @JsonProperty(value = "rowLimit") final @Nullable Integer rowLimit, + @JsonProperty(value = "spiltLinesInIndividualMessages") final @Nullable Boolean spiltLinesInIndividualMessages) { + this.query = query; + assert rowLimit != null; + this.rowLimit = rowLimit; + this.spiltLinesInIndividualMessages = Objects.requireNonNullElse(spiltLinesInIndividualMessages, false); + } + + public @Nullable String getQuery() { + return query; + } + + public int getRowLimit() { + return rowLimit; + } + + public @NotNull Boolean getSpiltLinesInIndividualMessages() { + return spiltLinesInIndividualMessages; + } + + +} diff --git a/modules/hivemq-edge-module-databases/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory b/modules/hivemq-edge-module-databases/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory new file mode 100644 index 0000000000..8349c7a3f7 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/resources/META-INF/services/com.hivemq.adapter.sdk.api.factories.ProtocolAdapterFactory @@ -0,0 +1 @@ +com.hivemq.edge.adapters.databases.DatabasesProtocolAdapterFactory diff --git a/modules/hivemq-edge-module-databases/src/main/resources/databases-adapter-ui-schema.json b/modules/hivemq-edge-module-databases/src/main/resources/databases-adapter-ui-schema.json new file mode 100644 index 0000000000..58ddd0d182 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/main/resources/databases-adapter-ui-schema.json @@ -0,0 +1,38 @@ +{ + "ui:tabs": [ + { + "id": "coreFields", + "title": "Settings", + "properties": [ + "id", + "type", + "server", + "port", + "database", + "username", + "password" + ] + }, + { + "id": "publishing", + "title": "Publishing", + "properties": [ + "maxPollingErrorsBeforeRemoval", + "pollingIntervalMillis" + ] + } + ], + "port": { + "type": "integer" + }, + "password": { + "ui:widget": "password" + }, + "ui:order": [ + "id", + "type", + "server", + "port", + "*" + ] +} diff --git a/modules/hivemq-edge-module-databases/src/main/resources/httpd/images/database.png b/modules/hivemq-edge-module-databases/src/main/resources/httpd/images/database.png new file mode 100644 index 0000000000..497b172bb4 Binary files /dev/null and b/modules/hivemq-edge-module-databases/src/main/resources/httpd/images/database.png differ diff --git a/modules/hivemq-edge-module-databases/src/test/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformationTest.java b/modules/hivemq-edge-module-databases/src/test/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformationTest.java new file mode 100644 index 0000000000..9a5a138ca5 --- /dev/null +++ b/modules/hivemq-edge-module-databases/src/test/java/com/hivemq/edge/adapters/databases/DatabasesProtocolAdapterInformationTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024-present HiveMQ GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hivemq.edge.adapters.databases; + +import org.junit.jupiter.api.Test; + +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; + +class DatabasesProtocolAdapterInformationTest { + + @Test + void getProtocolId_MustNotContainWhiteSpaces() { + final DatabasesProtocolAdapterInformation information = new DatabasesProtocolAdapterInformation(); + assertFalse(information.getProtocolId().contains(" ")); + } + + + @Test + void getProtocolId_MustBeAlphaNummercialOrUnderscore() { + final String ALPHA_NUM = "[A-Za-z0-9_]*"; + final Pattern alphaNumPattern = Pattern.compile(ALPHA_NUM); + final DatabasesProtocolAdapterInformation information = new DatabasesProtocolAdapterInformation(); + assertTrue(alphaNumPattern.matcher(information.getProtocolId()).matches()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 882715f3df..c856c82b3f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,3 +17,4 @@ includeBuild("./modules/hivemq-edge-module-http") includeBuild("./modules/hivemq-edge-module-modbus") includeBuild("./modules/hivemq-edge-module-opcua") includeBuild("./modules/hivemq-edge-module-file") +includeBuild("./modules/hivemq-edge-module-databases")