From b1f6a16ff580d97026f2947e029923952d82016b Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Fri, 13 Jan 2023 15:25:59 +0100 Subject: [PATCH 01/18] create device communication api Signed-off-by: g.dimitropoulos --- device-communication/.dockerignore | 5 + device-communication/.gitignore | 40 +++ device-communication/.mvn/wrapper/.gitignore | 1 + .../.mvn/wrapper/MavenWrapperDownloader.java | 121 +++++++ .../.mvn/wrapper/maven-wrapper.properties | 18 + device-communication/README.md | 54 +++ device-communication/mvnw | 316 +++++++++++++++++ device-communication/mvnw.cmd | 188 +++++++++++ device-communication/pom.xml | 210 ++++++++++++ .../src/main/docker/Dockerfile.jvm | 93 +++++ .../src/main/docker/Dockerfile.legacy-jar | 89 +++++ .../src/main/docker/Dockerfile.native | 27 ++ .../src/main/docker/Dockerfile.native-micro | 30 ++ .../hono/communication/api/Application.java | 54 +++ .../api/DeviceCommunicationHttpServer.java | 169 ++++++++++ .../api/config/DeviceCommandConstants.java | 26 ++ .../api/config/DeviceConfigsConstants.java | 30 ++ .../api/config/EndpointConstants.java | 25 ++ .../api/entity/DeviceCommandRequest.java | 83 +++++ .../api/entity/DeviceConfig.java | 129 +++++++ .../api/entity/DeviceConfigEntity.java | 136 ++++++++ .../api/entity/DeviceConfigRequest.java | 83 +++++ .../ListDeviceConfigVersionsResponse.java | 73 ++++ .../api/handler/DeviceCommandsHandler.java | 49 +++ .../api/handler/DeviceConfigsHandler.java | 78 +++++ .../api/mapper/DeviceConfigMapper.java | 48 +++ .../repository/DeviceConfigsRepository.java | 34 ++ .../DeviceConfigsRepositoryImpl.java | 202 +++++++++++ .../api/service/DatabaseService.java | 28 ++ .../api/service/DatabaseServiceImpl.java | 66 ++++ .../api/service/DeviceCommandService.java | 26 ++ .../api/service/DeviceCommandServiceImpl.java | 41 +++ .../api/service/DeviceConfigService.java | 33 ++ .../api/service/DeviceConfigServiceImpl.java | 84 +++++ .../VertxHttpHandlerManagerService.java | 44 +++ .../core/app/AbstractServiceApplication.java | 160 +++++++++ .../core/app/ApplicationConfig.java | 62 ++++ .../core/app/DatabaseConfig.java | 84 +++++ .../communication/core/app/ServerConfig.java | 48 +++ .../core/http/AbstractVertxHttpServer.java | 35 ++ .../core/http/HttpEndpointHandler.java | 32 ++ .../communication/core/http/HttpServer.java | 34 ++ .../core/http/HttpServiceBase.java | 34 ++ .../communication/core/utils/DbUtils.java | 50 +++ .../core/utils/ResponseUtils.java | 104 ++++++ .../api/hono-device-communication-v1.yaml | 206 ++++++++++++ .../src/main/resources/application.yaml | 37 ++ .../resources/db/migration/V1.0__create.sql | 11 + .../communication/api/ApplicationTest.java | 56 ++++ .../DeviceCommunicationHttpServerTest.java | 317 ++++++++++++++++++ .../handler/DeviceCommandsHandlerTest.java | 89 +++++ .../api/handler/DeviceConfigsHandlerTest.java | 238 +++++++++++++ .../api/service/DatabaseServiceImplTest.java | 81 +++++ .../service/DeviceConfigServiceImplTest.java | 121 +++++++ .../src/test/resources/application.yaml | 16 + 55 files changed, 4548 insertions(+) create mode 100644 device-communication/.dockerignore create mode 100644 device-communication/.gitignore create mode 100644 device-communication/.mvn/wrapper/.gitignore create mode 100644 device-communication/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 device-communication/.mvn/wrapper/maven-wrapper.properties create mode 100644 device-communication/README.md create mode 100644 device-communication/mvnw create mode 100644 device-communication/mvnw.cmd create mode 100644 device-communication/pom.xml create mode 100644 device-communication/src/main/docker/Dockerfile.jvm create mode 100644 device-communication/src/main/docker/Dockerfile.legacy-jar create mode 100644 device-communication/src/main/docker/Dockerfile.native create mode 100644 device-communication/src/main/docker/Dockerfile.native-micro create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/config/EndpointConstants.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpEndpointHandler.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java create mode 100644 device-communication/src/main/resources/api/hono-device-communication-v1.yaml create mode 100644 device-communication/src/main/resources/application.yaml create mode 100644 device-communication/src/main/resources/db/migration/V1.0__create.sql create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java create mode 100644 device-communication/src/test/resources/application.yaml diff --git a/device-communication/.dockerignore b/device-communication/.dockerignore new file mode 100644 index 00000000..94810d00 --- /dev/null +++ b/device-communication/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/device-communication/.gitignore b/device-communication/.gitignore new file mode 100644 index 00000000..693002a0 --- /dev/null +++ b/device-communication/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/device-communication/.mvn/wrapper/.gitignore b/device-communication/.mvn/wrapper/.gitignore new file mode 100644 index 00000000..e72f5e8b --- /dev/null +++ b/device-communication/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/device-communication/.mvn/wrapper/MavenWrapperDownloader.java b/device-communication/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..05212d56 --- /dev/null +++ b/device-communication/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,121 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + private static final String WRAPPER_VERSION = "3.1.1"; + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION + + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the + * default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println("- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) + throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/device-communication/.mvn/wrapper/maven-wrapper.properties b/device-communication/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..61a2ef15 --- /dev/null +++ b/device-communication/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/device-communication/README.md b/device-communication/README.md new file mode 100644 index 00000000..ce13e7f2 --- /dev/null +++ b/device-communication/README.md @@ -0,0 +1,54 @@ +# device-communication Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: + +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: + +```shell script +./mvnw package +``` + +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: + +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: + +```shell script +./mvnw package -Pnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: + +```shell script +./mvnw package -Pnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/device-communication-1.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. diff --git a/device-communication/mvnw b/device-communication/mvnw new file mode 100644 index 00000000..eaa3d308 --- /dev/null +++ b/device-communication/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/device-communication/mvnw.cmd b/device-communication/mvnw.cmd new file mode 100644 index 00000000..abb7c324 --- /dev/null +++ b/device-communication/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/device-communication/pom.xml b/device-communication/pom.xml new file mode 100644 index 00000000..3bbf3801 --- /dev/null +++ b/device-communication/pom.xml @@ -0,0 +1,210 @@ + + + 4.0.0 + org.eclipse.hono + device-communication + 1.0-SNAPSHOT + + 3.10.1 + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 2.15.3.Final + true + 3.0.0-M7 + 1.5.3.Final + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-vertx + + + io.vertx + vertx-core + + + io.vertx + vertx-web + + + io.vertx + vertx-web-openapi + + + io.vertx + vertx-web-validation + + + com.fasterxml.jackson.core + jackson-annotations + + + io.quarkus + quarkus-config-yaml + + + + + io.quarkus + quarkus-flyway + + + + + io.quarkus + quarkus-jdbc-postgresql + + + + + org.mockito + mockito-inline + test + + + + io.vertx + vertx-pg-client + 4.3.7 + + + jakarta.persistence + jakarta.persistence-api + 2.2.3 + + + com.ongres.scram + client + 2.1 + + + + io.vertx + vertx-sql-client-templates + + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + io.quarkus + quarkus-extension-processor + ${quarkus.platform.version} + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/device-communication/src/main/docker/Dockerfile.jvm b/device-communication/src/main/docker/Dockerfile.jvm new file mode 100644 index 00000000..c5a55161 --- /dev/null +++ b/device-communication/src/main/docker/Dockerfile.jvm @@ -0,0 +1,93 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/device-communication-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/device-communication/src/main/docker/Dockerfile.legacy-jar b/device-communication/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 00000000..e1a0c7e6 --- /dev/null +++ b/device-communication/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,89 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/device-communication-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-17:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/device-communication/src/main/docker/Dockerfile.native b/device-communication/src/main/docker/Dockerfile.native new file mode 100644 index 00000000..9dd59a05 --- /dev/null +++ b/device-communication/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/device-communication . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/device-communication/src/main/docker/Dockerfile.native-micro b/device-communication/src/main/docker/Dockerfile.native-micro new file mode 100644 index 00000000..44595bbb --- /dev/null +++ b/device-communication/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/device-communication . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/device-communication +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java new file mode 100644 index 00000000..6fbbd88f --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java @@ -0,0 +1,54 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api; + +import io.vertx.core.Vertx; +import org.eclipse.hono.communication.core.app.AbstractServiceApplication; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.http.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Device Communication application + */ +public class Application extends AbstractServiceApplication { + + private final Logger log = LoggerFactory.getLogger(AbstractServiceApplication.class); + private final HttpServer server; + + public Application(Vertx vertx, + ApplicationConfig appConfigs, + HttpServer server) { + super(vertx, appConfigs); + this.server = server; + } + + @Override + public void doStart() { + log.info("Starting HTTP server..."); + server.start(); + } + + @Override + public void doStop() { + log.info("Stopping HTTP server..."); + server.stop(); + } + + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java new file mode 100644 index 00000000..95086423 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -0,0 +1,169 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.quarkus.runtime.Quarkus; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.validation.BadRequestException; +import org.eclipse.hono.communication.api.service.DatabaseService; +import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.http.AbstractVertxHttpServer; +import org.eclipse.hono.communication.core.http.HttpEndpointHandler; +import org.eclipse.hono.communication.core.http.HttpServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Singleton; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Vertx HTTP Server for the device communication api + */ +@Singleton +public class DeviceCommunicationHttpServer extends AbstractVertxHttpServer implements HttpServer { + private final Logger log = LoggerFactory.getLogger(DeviceCommunicationHttpServer.class); + private final String serverStartedMsg = "HTTP Server is listening at http://{}:{}"; + private final String serverFailedMsg = "HTTP Server failed to start: {}"; + private final VertxHttpHandlerManagerService httpHandlerManager; + + private final DatabaseService db; + private List httpEndpointHandlers; + + + public DeviceCommunicationHttpServer(ApplicationConfig appConfigs, + Vertx vertx, + VertxHttpHandlerManagerService httpHandlerManager, + DatabaseService databaseService) { + super(appConfigs, vertx); + this.httpHandlerManager = httpHandlerManager; + this.httpEndpointHandlers = new ArrayList<>(); + this.db = databaseService; + } + + + @Override + public void start() { + this.httpEndpointHandlers = httpHandlerManager.getAvailableHandlerServices(); + RouterBuilder.create(this.vertx, appConfigs.getServerConfig().getOpenApiFilePath()) + .onSuccess(routerBuilder -> + { + + Router apiRouter = this.createRouterWithEndpoints(routerBuilder, httpEndpointHandlers); + this.startVertxServer(apiRouter); + }) + .onFailure(error -> { + if (error != null) { + log.error(error.getMessage()); + } else { + log.error("Can not create Router"); + } + + }); + + // Wait until application is stopped + Quarkus.waitForExit(); + + } + + /** + * Creates the Router object and adds endpoints and handlers + * + * @param routerBuilder Vertx RouterBuilder object + * @param httpEndpointHandlers All available http endpoint handlers + * @return The created Router object + */ + Router createRouterWithEndpoints(RouterBuilder routerBuilder, List httpEndpointHandlers) { + for (HttpEndpointHandler handlerService : httpEndpointHandlers) { + handlerService.addRoutes(routerBuilder); + } + + return routerBuilder.createRouter(); + } + + /** + * Starts the server and blocks until application is stopped + * + * @param router The Router object + */ + void startVertxServer(Router router) { + var serverConfigs = appConfigs.getServerConfig(); + var serverOptions = new HttpServerOptions() + .setPort(serverConfigs.getServerPort()) + .setHost(serverConfigs.getServerUrl()); + + var serverCreationFuture = vertx + .createHttpServer(serverOptions) + .requestHandler(router) + .listen(); + + serverCreationFuture + .onSuccess(server -> log.info(this.serverStartedMsg, serverConfigs.getServerUrl() + , serverConfigs.getServerPort())) + .onFailure(error -> log.info(this.serverFailedMsg, error.getMessage())); + } + + /** + * Adds status code 400 and sets the error message for the routingContext response. + * + * @param routingContext the routing context object + * @Throws: NullPointerException – if routingContext is {@code null}. + */ + void addDefault400ExceptionHandler(RoutingContext routingContext) { + Objects.requireNonNull(routingContext); + String errorMsg = ((BadRequestException) routingContext.failure()).toJson().toString(); + log.error(errorMsg); + routingContext.response().setStatusCode(400).end(errorMsg); + } + + /** + * Adds status code 404 and sets the error message for the routingContext response. + * + * @param routingContext the routing context object + * @Throws: NullPointerException – if routingContext is {@code null}. + */ + void addDefault404ExceptionHandler(RoutingContext routingContext) { + Objects.requireNonNull(routingContext); + if (!routingContext.response().ended()) { + routingContext.response().setStatusCode(404); + + if (!routingContext.request().method().equals(HttpMethod.HEAD)) { + routingContext.response() + .putHeader(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8") + .end("

Resource not found

"); + } else { + routingContext.response().end(); + } + } + } + + + @Override + public void stop() { + // stop server custom functionality + db.close(); + + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java new file mode 100644 index 00000000..2e1f3c2f --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java @@ -0,0 +1,26 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.config; + +/** + * Device commands constant values + */ +public class DeviceCommandConstants { + + // Open api operationIds + public final static String POST_DEVICE_COMMAND_OP_ID = "postCommand"; +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java new file mode 100644 index 00000000..b12e3ddf --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java @@ -0,0 +1,30 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.config; + +/** + * Device configs constant values + */ +public class DeviceConfigsConstants { + + // Open api operationIds + public final static String POST_MODIFY_DEVICE_CONFIG_OP_ID = "modifyCloudToDeviceConfig"; + public final static String LIST_CONFIG_VERSIONS_OP_ID = "listConfigVersions"; + public final static String TENANT_PATH_PARAMETER = "tenantid"; + public final static String DEVICE_PATH_PARAMETER = "deviceid"; + public final static String NUM_VERSION_QUERY_PARAMS = "numVersions"; +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/EndpointConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/EndpointConstants.java new file mode 100644 index 00000000..15d04f4c --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/EndpointConstants.java @@ -0,0 +1,25 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.config; + +/** + * Class for sharing api constant values + */ +public class EndpointConstants { + + // Device +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java new file mode 100644 index 00000000..23b12f65 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java @@ -0,0 +1,83 @@ +package org.eclipse.hono.communication.api.entity; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Command json object structure + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceCommandRequest { + + private String binaryData; + private String subfolder; + + public DeviceCommandRequest () { + + } + + public DeviceCommandRequest (String binaryData, String subfolder) { + this.binaryData = binaryData; + this.subfolder = subfolder; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @JsonProperty("subfolder") + public String getSubfolder() { + return subfolder; + } + public void setSubfolder(String subfolder) { + this.subfolder = subfolder; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceCommandRequest deviceCommandRequest = (DeviceCommandRequest) o; + return Objects.equals(binaryData, deviceCommandRequest.binaryData) && + Objects.equals(subfolder, deviceCommandRequest.subfolder); + } + + @Override + public int hashCode() { + return Objects.hash(binaryData, subfolder); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DeviceCommandRequest {\n"); + + sb.append(" binaryData: ").append(toIndentedString(binaryData)).append("\n"); + sb.append(" subfolder: ").append(toIndentedString(subfolder)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java new file mode 100644 index 00000000..fa2590af --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java @@ -0,0 +1,129 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * The device configuration. + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceConfig { + + private String version; + private String cloudUpdateTime; + private String deviceAckTime; + private String binaryData; + + public DeviceConfig() { + + } + + public DeviceConfig(String version, String cloudUpdateTime, String deviceAckTime, String binaryData) { + this.version = version; + this.cloudUpdateTime = cloudUpdateTime; + this.deviceAckTime = deviceAckTime; + this.binaryData = binaryData; + } + + + @JsonProperty("version") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + + @JsonProperty("cloudUpdateTime") + public String getCloudUpdateTime() { + return cloudUpdateTime; + } + + public void setCloudUpdateTime(String cloudUpdateTime) { + this.cloudUpdateTime = cloudUpdateTime; + } + + + @JsonProperty("deviceAckTime") + public String getDeviceAckTime() { + return deviceAckTime; + } + + public void setDeviceAckTime(String deviceAckTime) { + this.deviceAckTime = deviceAckTime; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceConfig deviceConfig = (DeviceConfig) o; + return Objects.equals(version, deviceConfig.version) && + Objects.equals(cloudUpdateTime, deviceConfig.cloudUpdateTime) && + Objects.equals(deviceAckTime, deviceConfig.deviceAckTime) && + Objects.equals(binaryData, deviceConfig.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(version, cloudUpdateTime, deviceAckTime, binaryData); + } + + @Override + public String toString() { + + String sb = "class DeviceConfig {\n" + + " version: " + toIndentedString(version) + "\n" + + " cloudUpdateTime: " + toIndentedString(cloudUpdateTime) + "\n" + + " deviceAckTime: " + toIndentedString(deviceAckTime) + "\n" + + " binaryData: " + toIndentedString(binaryData) + "\n" + + "}"; + return sb; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java new file mode 100644 index 00000000..3008d5e7 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java @@ -0,0 +1,136 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.entity; + +import java.util.Objects; + +/** + * The device configuration entity object. + **/ +public class DeviceConfigEntity { + + + private int version; + private String tenantId; + private String deviceId; + + private String cloudUpdateTime; + private String deviceAckTime; + private String binaryData; + + public DeviceConfigEntity(int version, + String tenantId, + String deviceId, + String cloudUpdateTime, + String deviceAckTime, + String binaryData) { + this.version = version; + this.tenantId = tenantId; + this.deviceId = deviceId; + this.cloudUpdateTime = cloudUpdateTime; + this.deviceAckTime = deviceAckTime; + this.binaryData = binaryData; + } + + public DeviceConfigEntity(String tenantId, String deviceId, DeviceConfig deviceConfig) { + this.version = Integer.parseInt(deviceConfig.getVersion()); + this.cloudUpdateTime = deviceConfig.getCloudUpdateTime(); + this.deviceAckTime = deviceConfig.getDeviceAckTime(); + this.binaryData = deviceConfig.getBinaryData(); + } + + public DeviceConfigEntity() { + } + + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + + public String getCloudUpdateTime() { + return cloudUpdateTime; + } + + public void setCloudUpdateTime(String cloudUpdateTime) { + this.cloudUpdateTime = cloudUpdateTime; + } + + public String getDeviceAckTime() { + return deviceAckTime; + } + + public void setDeviceAckTime(String deviceAckTime) { + this.deviceAckTime = deviceAckTime; + } + + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public String toString() { + return "DeviceConfigEntity{" + + "version=" + version + + ", tenantId='" + tenantId + '\'' + + ", deviceId='" + deviceId + '\'' + + ", cloudUpdateTime='" + cloudUpdateTime + '\'' + + ", deviceAckTime='" + deviceAckTime + '\'' + + ", binaryData='" + binaryData + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceConfigEntity that = (DeviceConfigEntity) o; + return version == that.version && tenantId.equals(that.tenantId) && deviceId.equals(that.deviceId) && cloudUpdateTime.equals(that.cloudUpdateTime) && Objects.equals(deviceAckTime, that.deviceAckTime) && binaryData.equals(that.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(version, tenantId, deviceId, cloudUpdateTime, deviceAckTime, binaryData); + } + + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java new file mode 100644 index 00000000..c1913b0d --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java @@ -0,0 +1,83 @@ +package org.eclipse.hono.communication.api.entity; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request body for modifying device configs + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceConfigRequest { + + private String versionToUpdate; + private String binaryData; + + public DeviceConfigRequest () { + + } + + public DeviceConfigRequest (String versionToUpdate, String binaryData) { + this.versionToUpdate = versionToUpdate; + this.binaryData = binaryData; + } + + + @JsonProperty("versionToUpdate") + public String getVersionToUpdate() { + return versionToUpdate; + } + public void setVersionToUpdate(String versionToUpdate) { + this.versionToUpdate = versionToUpdate; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceConfigRequest deviceConfigRequest = (DeviceConfigRequest) o; + return Objects.equals(versionToUpdate, deviceConfigRequest.versionToUpdate) && + Objects.equals(binaryData, deviceConfigRequest.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(versionToUpdate, binaryData); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DeviceConfigRequest {\n"); + + sb.append(" versionToUpdate: ").append(toIndentedString(versionToUpdate)).append("\n"); + sb.append(" binaryData: ").append(toIndentedString(binaryData)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java new file mode 100644 index 00000000..892d4001 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java @@ -0,0 +1,73 @@ +package org.eclipse.hono.communication.api.entity; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A list of a device config versions + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ListDeviceConfigVersionsResponse { + + private List deviceConfigs = new ArrayList<>(); + + public ListDeviceConfigVersionsResponse() { + + } + + public ListDeviceConfigVersionsResponse(List deviceConfigs) { + this.deviceConfigs = deviceConfigs; + } + + + @JsonProperty("deviceConfigs") + public List getDeviceConfigs() { + return deviceConfigs; + } + + public void setDeviceConfigs(List deviceConfigs) { + this.deviceConfigs = deviceConfigs; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = (ListDeviceConfigVersionsResponse) o; + return Objects.equals(deviceConfigs, listDeviceConfigVersionsResponse.deviceConfigs); + } + + @Override + public int hashCode() { + return Objects.hash(deviceConfigs); + } + + @Override + public String toString() { + + String sb = "class ListDeviceConfigVersionsResponse {\n" + + " deviceConfigs: " + toIndentedString(deviceConfigs) + "\n" + + "}"; + return sb; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java new file mode 100644 index 00000000..ef6e1f44 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java @@ -0,0 +1,49 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.handler; + +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; +import org.eclipse.hono.communication.api.config.DeviceCommandConstants; +import org.eclipse.hono.communication.api.service.DeviceCommandService; +import org.eclipse.hono.communication.core.http.HttpEndpointHandler; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Handler for device command endpoints + */ +@ApplicationScoped +public class DeviceCommandsHandler implements HttpEndpointHandler { + + private final DeviceCommandService commandService; + + public DeviceCommandsHandler(DeviceCommandService commandService) { + this.commandService = commandService; + } + + @Override + public void addRoutes(RouterBuilder routerBuilder) { + routerBuilder.operation(DeviceCommandConstants.POST_DEVICE_COMMAND_OP_ID) + .handler(this::handlePostCommand); + } + + public void handlePostCommand(RoutingContext routingContext) { + commandService.postCommand(routingContext); + // publish command and send response + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java new file mode 100644 index 00000000..b20b1021 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java @@ -0,0 +1,78 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.handler; + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.Future; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; +import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.service.DeviceConfigService; +import org.eclipse.hono.communication.core.http.HttpEndpointHandler; +import org.eclipse.hono.communication.core.utils.ResponseUtils; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Handler for device config endpoints + */ +@ApplicationScoped +public class DeviceConfigsHandler implements HttpEndpointHandler { + + private final String QUERY_PARAMS_NUM_VERSION = "numVersions"; + + private final DeviceConfigService configService; + + public DeviceConfigsHandler(DeviceConfigService configService) { + this.configService = configService; + } + + + @Override + public void addRoutes(RouterBuilder routerBuilder) { + routerBuilder.operation(DeviceConfigsConstants.LIST_CONFIG_VERSIONS_OP_ID) + .handler(this::handleListConfigVersions); + routerBuilder.operation(DeviceConfigsConstants.POST_MODIFY_DEVICE_CONFIG_OP_ID) + .handler(this::handleModifyCloudToDeviceConfig); + } + + public Future<@Nullable DeviceConfigEntity> handleModifyCloudToDeviceConfig(RoutingContext routingContext) { + var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + + final DeviceConfigRequest deviceConfig = routingContext.body() + .asJsonObject() + .mapTo(DeviceConfigRequest.class); + + return configService.modifyCloudToDeviceConfig(deviceConfig, deviceId, tenantId) + .onSuccess(result -> ResponseUtils.successResponse(routingContext, result)) + .onFailure(err -> ResponseUtils.errorResponse(routingContext, err)); + } + + public Future handleListConfigVersions(RoutingContext routingContext) { + var limit = Integer.parseInt(routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS)); + var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + + return configService.listAll(deviceId, tenantId, limit) + .onSuccess(result -> ResponseUtils.successResponse(routingContext, result)) + .onFailure(err -> ResponseUtils.errorResponse(routingContext, err)); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java new file mode 100644 index 00000000..cc009ad5 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java @@ -0,0 +1,48 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.mapper; + +import org.eclipse.hono.communication.api.entity.DeviceConfig; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.NullValuePropertyMappingStrategy; + +import java.time.Instant; + +@Mapper(componentModel = "cdi", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) +public interface DeviceConfigMapper { + + DeviceConfigEntity deviceConfigToEntity(DeviceConfig deviceConfig); + + + DeviceConfig entityToDeviceConfig(DeviceConfigEntity entity); + + @Mapping(target = "version", source = "request.versionToUpdate") + @Mapping(target = "cloudUpdateTime", expression = "java(getDateTime())") + DeviceConfig configRequestToDeviceConfig(DeviceConfigRequest request); + + @Mapping(target = "version", source = "request.versionToUpdate") + @Mapping(target = "cloudUpdateTime", expression = "java(getDateTime())") + DeviceConfigEntity configRequestToDeviceConfigEntity(DeviceConfigRequest request); + + default String getDateTime() { + return Instant.now().toString(); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java new file mode 100644 index 00000000..34c0bec0 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java @@ -0,0 +1,34 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.repository; + +import io.vertx.core.Future; +import io.vertx.sqlclient.SqlConnection; +import org.eclipse.hono.communication.api.entity.DeviceConfig; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; + +import java.util.List; + +/** + * Device config repository interface + */ +public interface DeviceConfigsRepository { + + Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit); + + Future createOrUpdate(SqlConnection sqlConnection, DeviceConfigEntity entity); +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java new file mode 100644 index 00000000..777be10d --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java @@ -0,0 +1,202 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.repository; + +import io.vertx.core.Future; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.SqlConnection; +import io.vertx.sqlclient.templates.RowMapper; +import io.vertx.sqlclient.templates.SqlTemplate; +import org.eclipse.hono.communication.api.entity.DeviceConfig; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.core.app.DatabaseConfig; + +import javax.enterprise.context.ApplicationScoped; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Repository class for making CRUD operations for device config entities + */ +@ApplicationScoped +public class DeviceConfigsRepositoryImpl implements DeviceConfigsRepository { + private final static String SQL_INSERT = "INSERT INTO \"deviceConfig\" (version, \"tenantId\", \"deviceId\", \"cloudUpdateTime\", \"deviceAckTime\", \"binaryData\") " + + "VALUES (#{version}, #{tenantId}, #{deviceId}, #{cloudUpdateTime}, #{deviceAckTime}, #{binaryData}) RETURNING version"; + private final static String SQL_UPDATE = "UPDATE \"deviceConfig\" SET \"cloudUpdateTime\" = #{cloudUpdateTime}, \"deviceAckTime\" = #{deviceAckTime}, " + + "\"binaryData\" = #{binaryData} WHERE version = #{version} and \"tenantId\" = #{tenantId} and \"deviceId\" = #{deviceId}"; + private final static String SQL_LIST = "SELECT version, \"cloudUpdateTime\", COALESCE(\"deviceAckTime\",'') AS \"deviceAckTime\", \"binaryData\" " + + "FROM \"deviceConfig\" WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId} ORDER BY version DESC LIMIT #{limit}"; + private final static String SQL_FIND_MAX_VERSION = "SELECT COALESCE(max(version), 0) AS version from \"deviceConfig\" " + + "WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId}"; + private final Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); + private final DatabaseConfig databaseConfig; + private String SQL_COUNT_DEVICES_WITH_PK_FILTER = "SELECT COUNT(*) as total FROM %s where %s = #{tenantId} and %s = #{deviceId}"; + + public DeviceConfigsRepositoryImpl(DatabaseConfig databaseConfig) { + this.databaseConfig = databaseConfig; + + SQL_COUNT_DEVICES_WITH_PK_FILTER = String.format(SQL_COUNT_DEVICES_WITH_PK_FILTER, + databaseConfig.getDeviceRegistrationTableName(), + databaseConfig.getDeviceRegistrationTenantIdColumn(), + databaseConfig.getDeviceRegistrationDeviceIdColumn()); + } + + + private Future countDevices(SqlConnection sqlConnection, String deviceId, String tenantId) { + final RowMapper ROW_MAPPER = row -> row.getInteger("total"); + return SqlTemplate + .forQuery(sqlConnection, SQL_COUNT_DEVICES_WITH_PK_FILTER) + .mapTo(ROW_MAPPER) + .execute(Map.of("deviceId", deviceId, "tenantId", tenantId)).map(rowSet -> { + final RowIterator iterator = rowSet.iterator(); + return iterator.next(); + }); + + } + + /** + * Lists all config versions for a specific device. Result is order by version desc + * + * @param sqlConnection The sql connection instance + * @param deviceId The device id + * @param tenantId The tenant id + * @param limit The number of config to show + * @return A Future with a List of DeviceConfigs + */ + public Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit) { + return SqlTemplate + .forQuery(sqlConnection, SQL_LIST) + .mapTo(DeviceConfig.class) + .execute(Map.of("limit", limit, "deviceId", deviceId, "tenantId", tenantId)) + .map(rowSet -> { + final List configs = new ArrayList<>(); + rowSet.forEach(configs::add); + return configs; + }) + .onSuccess(success -> log.info( + String.format("Listing all configs for device %s and tenant %s", + deviceId, tenantId))) + .onFailure(throwable -> log.error("Error: {}", throwable)); + } + + /** + * Updates an entity for a given version + * + * @param sqlConnection The sql connection instance + * @param entity The instance to insert + * @return A Future of the created DeviceConfigEntity + */ + private Future update(SqlConnection sqlConnection, DeviceConfigEntity entity) { + + return SqlTemplate + .forQuery(sqlConnection, SQL_UPDATE) + .mapFrom(DeviceConfigEntity.class) + .execute(entity) + .map(rowSet -> { + if (rowSet.rowCount() > 0) { + return entity; + } else { + throw new IllegalStateException(String.format("Device config version doesn't exist: %s", entity)); + } + }) + .onSuccess(success -> log.info(String.format("Device config updated successfully: %s", success.toString()))) + .onFailure(throwable -> log.error("Error: {}", throwable)); + } + + /** + * Increases the version number and creates a new entity. + * + * @param sqlConnection The sql connection instance + * @param entity The instance to insert + * @return A Future of the created DeviceConfigEntity + */ + private Future create(SqlConnection sqlConnection, DeviceConfigEntity entity) { + final RowMapper ROW_MAPPER = row -> row.getInteger("version"); + + return SqlTemplate + .forQuery(sqlConnection, SQL_FIND_MAX_VERSION) + .mapFrom(DeviceConfigEntity.class) + .mapTo(ROW_MAPPER) + .execute(entity) + .map(rowSet -> { + final RowIterator iterator = rowSet.iterator(); + return iterator.hasNext() ? iterator.next() + 1 : 1; + }).compose(maxResults -> { + entity.setVersion(maxResults); + return insertEntity(sqlConnection, entity); + } + ) + .onSuccess(success -> log.info(String.format("Device configs created successfully: %s", success.toString()))) + .onFailure(throwable -> log.error("Error: {}", throwable)); + } + + /** + * Inserts an entity to Database + * + * @param sqlConnection The sql connection instance + * @param entity The instance to insert + * @return A Future of the created DeviceConfigEntity + */ + private Future insertEntity(SqlConnection sqlConnection, DeviceConfigEntity entity) { + return SqlTemplate + .forUpdate(sqlConnection, SQL_INSERT) + .mapFrom(DeviceConfigEntity.class) + .mapTo(DeviceConfigEntity.class) + .execute(entity) + .map(rowSet -> { + final RowIterator iterator = rowSet.iterator(); + if (iterator.hasNext()) { + entity.setVersion(iterator.next().getVersion()); + return entity; + } else { + throw new IllegalStateException(String.format("Can't create device config: %s", entity)); + } + }); + } + + + /** + * Creates a new config if version is 0 else updates the current config + * + * @param sqlConnection The sql connection instance + * @param entity The instance to insert + * @return A Future of the created DeviceConfigEntity + */ + public Future createOrUpdate(SqlConnection sqlConnection, DeviceConfigEntity entity) { + return countDevices(sqlConnection, entity.getDeviceId(), entity.getTenantId()) + .compose( + counter -> { + if (counter < 1) { + throw new IllegalStateException(String.format("Device with id %s and tenant id %s doesn't exist", + entity.getDeviceId(), + entity.getTenantId())); + } else { + + if (entity.getVersion() == 0) { + return create(sqlConnection, entity); + } else if (entity.getVersion() > 0) { + return update(sqlConnection, entity); + } else { + throw new IllegalStateException("Config version must be >= 0"); + } + } + }); + } +} \ No newline at end of file diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java new file mode 100644 index 00000000..09f293d4 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java @@ -0,0 +1,28 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.pgclient.PgPool; + +/** + * Database service interface + */ +public interface DatabaseService { + PgPool getDbClient(); + + void close(); +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java new file mode 100644 index 00000000..a899e8c5 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java @@ -0,0 +1,66 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.pgclient.PgPool; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.utils.DbUtils; + +import javax.enterprise.context.ApplicationScoped; + + +/** + * Service for database + */ +@ApplicationScoped +public class DatabaseServiceImpl implements DatabaseService { + + private final Logger log = LoggerFactory.getLogger(DatabaseServiceImpl.class); + private final ApplicationConfig appConfigs; + private final Vertx vertx; + private final PgPool dbClient; + + public DatabaseServiceImpl(ApplicationConfig appConfigs, Vertx vertx) { + this.appConfigs = appConfigs; + this.vertx = vertx; + this.dbClient = DbUtils.createDbClient(vertx, appConfigs); + log.debug("Database connection is open."); + } + + /** + * Gets the database client instance. + * + * @return The database client + */ + public PgPool getDbClient() { + return dbClient; + } + + /** + * Close database connection + */ + public void close() { + if (this.dbClient != null) { + this.dbClient.close(); + log.debug("Database connection is closed."); + } + } + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java new file mode 100644 index 00000000..4716b0a0 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java @@ -0,0 +1,26 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.ext.web.RoutingContext; + +/** + * Device commands interface + */ +public interface DeviceCommandService { + void postCommand(RoutingContext routingContext); +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java new file mode 100644 index 00000000..23f939fe --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java @@ -0,0 +1,41 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.ext.web.RoutingContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + +/** + * Service for device commands + */ +@ApplicationScoped +public class DeviceCommandServiceImpl implements DeviceCommandService { + private final Logger log = LoggerFactory.getLogger(DeviceCommandServiceImpl.class); + + + /** + * Handles device post commands + * + * @param routingContext The routing context + */ + public void postCommand(RoutingContext routingContext) { + log.info("postCommand received"); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java new file mode 100644 index 00000000..e9072881 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java @@ -0,0 +1,33 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.Future; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; + +/** + * Device config interface + */ +public interface DeviceConfigService { + + Future<@Nullable DeviceConfigEntity> modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId); + + Future listAll(String deviceId, String tenantId, int limit); +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java new file mode 100644 index 00000000..9beeac2e --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java @@ -0,0 +1,84 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + + +import io.vertx.codegen.annotations.Nullable; +import io.vertx.core.Future; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; +import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; + + +/** + * Service for device commands + */ + +@ApplicationScoped +public class DeviceConfigServiceImpl implements DeviceConfigService { + private final Logger log = LoggerFactory.getLogger(DeviceConfigServiceImpl.class); + + private final DeviceConfigsRepository repository; + private final DatabaseService db; + private final DeviceConfigMapper mapper; + + public DeviceConfigServiceImpl(DeviceConfigsRepository repository, + DatabaseService db, + DeviceConfigMapper mapper) { + + this.repository = repository; + this.db = db; + this.mapper = mapper; + } + + public Future<@Nullable DeviceConfigEntity> modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId) { + + var entity = mapper.configRequestToDeviceConfigEntity(deviceConfig); + entity.setDeviceId(deviceId); + entity.setTenantId(tenantId); + + return db.getDbClient().withTransaction( + sqlConnection -> { + return repository.createOrUpdate(sqlConnection, entity); + }) + .onSuccess(success -> log.info(success.toString())) + .onFailure(error -> log.error(error.getMessage())); + } + + public Future listAll(String deviceId, String tenantId, int limit) { + return db.getDbClient().withTransaction( + sqlConnection -> { + return repository.listAll(sqlConnection, deviceId, tenantId, limit) + .map( + result -> { + var listConfig = new ListDeviceConfigVersionsResponse(); + listConfig.setDeviceConfigs(result); + return listConfig; + } + ); + } + ) + .onSuccess(success -> log.info(success.getDeviceConfigs().toString())) + .onFailure(error -> log.error(error.getMessage())); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java new file mode 100644 index 00000000..d062701e --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java @@ -0,0 +1,44 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import org.eclipse.hono.communication.api.handler.DeviceCommandsHandler; +import org.eclipse.hono.communication.api.handler.DeviceConfigsHandler; +import org.eclipse.hono.communication.core.http.HttpEndpointHandler; + +import javax.enterprise.context.ApplicationScoped; +import java.util.List; + +/** + * Provides and Manages available HTTP vertx handlers + */ +@ApplicationScoped +public class VertxHttpHandlerManagerService { + /** + * Available vertx endpoints handler services + */ + private final List availableHandlerServices; + + + public VertxHttpHandlerManagerService(DeviceConfigsHandler configHandler, DeviceCommandsHandler commandHandler) { + this.availableHandlerServices = List.of(configHandler, commandHandler); + } + + public List getAvailableHandlerServices() { + return availableHandlerServices; + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java new file mode 100644 index 00000000..dd894ea0 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java @@ -0,0 +1,160 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.app; + +import io.quarkus.runtime.ShutdownEvent; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.Closeable; +import io.vertx.core.Vertx; +import io.vertx.core.impl.VertxInternal; +import io.vertx.core.impl.cpu.CpuCoreSensor; +import io.vertx.core.json.impl.JsonUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.event.Observes; +import java.util.Arrays; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * Abstract Service application class + */ +public abstract class AbstractServiceApplication { + + private final Logger log = LoggerFactory.getLogger(AbstractServiceApplication.class); + + /** + * The vert.x instance managed by Quarkus. + */ + protected Vertx vertx; + /** + * YAML file application configurations properties + */ + protected ApplicationConfig appConfigs; + private Closeable addedVertxCloseHook; + + + public AbstractServiceApplication(final Vertx vertx, + ApplicationConfig appConfigs) { + this.vertx = vertx; + this.appConfigs = appConfigs; + } + + /** + * Logs information about the JVM. + */ + protected void logJvmDetails() { + if (log.isInfoEnabled()) { + + final String base64Encoder = Base64.getEncoder() == JsonUtil.BASE64_ENCODER ? "legacy" : "URL safe"; + + log.info(""" + running on Java VM [version: {}, name: {}, vendor: {}, max memory: {}MiB, processors: {}] \ + with vert.x using {} Base64 encoder\ + """, + System.getProperty("java.version"), + System.getProperty("java.vm.name"), + System.getProperty("java.vm.vendor"), + Runtime.getRuntime().maxMemory() >> 20, + CpuCoreSensor.availableProcessors(), + base64Encoder); + } + } + + /** + * Registers a close hook that will be notified when the Vertx instance is being closed + */ + private void registerVertxCloseHook() { + if (vertx instanceof VertxInternal vertxInternal) { + final Closeable closeHook = completion -> { + final var stackTrace = Thread.currentThread().getStackTrace(); + final String s = Arrays.stream(stackTrace) + .skip(2) + .map(element -> "\tat %s.%s(%s:%d)".formatted( + element.getClassName(), + element.getMethodName(), + element.getFileName(), + element.getLineNumber())) + .collect(Collectors.joining(System.lineSeparator())); + log.warn("managed vert.x instance has been closed unexpectedly{}{}", System.lineSeparator(), s); + completion.complete(); + }; + vertxInternal.addCloseHook(closeHook); + addedVertxCloseHook = closeHook; + } else { + log.debug("Vertx instance is not a VertxInternal, skipping close hook registration"); + } + } + + /** + * Starts this component. + *

+ * This implementation + *

    + *
  1. logs the VM details,
  2. + *
  3. invokes {@link #doStart()}.
  4. + *
+ * + * @param ev The event indicating shutdown. + */ + public void onStart(final @Observes StartupEvent ev) { + + logJvmDetails(); + registerVertxCloseHook(); + log.info("Starting component {}...", appConfigs.componentName); + doStart(); + } + + /** + * Invoked during start up. + *

+ * Subclasses should override this method in order to initialize + * the component. + */ + protected void doStart() { + // do nothing + } + + protected void doStop() { + // do nothing + } + + + /** + * Stops this component. + * + * @param ev The event indicating shutdown. + */ + public void onStop(final @Observes ShutdownEvent ev) { + doStop(); + log.info("shutting down {}", appConfigs.componentName); + if (addedVertxCloseHook != null && vertx instanceof VertxInternal vertxInternal) { + vertxInternal.removeCloseHook(addedVertxCloseHook); + } + final CompletableFuture shutdown = new CompletableFuture<>(); + vertx.close(attempt -> { + if (attempt.succeeded()) { + shutdown.complete(null); + } else { + shutdown.completeExceptionally(attempt.cause()); + } + }); + shutdown.join(); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java new file mode 100644 index 00000000..5fc2fde0 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java @@ -0,0 +1,62 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.app; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.inject.Singleton; + + +/** + * Application configurations + */ +@Singleton +public class ApplicationConfig { + + private final ServerConfig serverConfig; + private final DatabaseConfig databaseConfig; + + // Application + @ConfigProperty(name = "app.name") + String componentName; + + + @ConfigProperty(name = "app.version") + String version; + + public ApplicationConfig(ServerConfig serverConfig, DatabaseConfig databaseConfig) { + this.serverConfig = serverConfig; + this.databaseConfig = databaseConfig; + } + + public String getVersion() { + return version; + } + + public String getComponentName() { + return componentName; + } + + + public ServerConfig getServerConfig() { + return serverConfig; + } + + public DatabaseConfig getDatabaseConfig() { + return databaseConfig; + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java new file mode 100644 index 00000000..97e7756a --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java @@ -0,0 +1,84 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.app; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.inject.Singleton; + +/** + * Database configurations + */ +@Singleton +public class DatabaseConfig { + + // Datasource properties + @ConfigProperty(name = "quarkus.datasource.port") + int port; + @ConfigProperty(name = "quarkus.datasource.host") + String host; + @ConfigProperty(name = "quarkus.datasource.username") + String userName; + @ConfigProperty(name = "quarkus.datasource.password") + String password; + @ConfigProperty(name = "quarkus.datasource.name") + String name; + @ConfigProperty(name = "quarkus.datasource.pool-max-size") + int poolMaxSize; + @ConfigProperty(name = "quarkus.device-registration.table") + String deviceRegistrationTableName; + @ConfigProperty(name = "quarkus.device-registration.tenant-id-column") + String deviceRegistrationTenantIdColumn; + @ConfigProperty(name = "quarkus.device-registration.device-id-column") + String deviceRegistrationDeviceIdColumn; + + public String getDeviceRegistrationTableName() { + return deviceRegistrationTableName; + } + + public String getDeviceRegistrationTenantIdColumn() { + return deviceRegistrationTenantIdColumn; + } + + public String getDeviceRegistrationDeviceIdColumn() { + return deviceRegistrationDeviceIdColumn; + } + + public int getPoolMaxSize() { + return poolMaxSize; + } + + public int getPort() { + return port; + } + + public String getHost() { + return host; + } + + public String getUserName() { + return userName; + } + + public String getPassword() { + return password; + } + + public String getName() { + return name; + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java new file mode 100644 index 00000000..54903a13 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java @@ -0,0 +1,48 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.app; + +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.inject.Singleton; + +/** + * Server configurations + */ +@Singleton +public class ServerConfig { + + // Vertx server properties + @ConfigProperty(name = "vertx.openapi.file") + String openApiFilePath; + @ConfigProperty(name = "vertx.server.url") + String serverUrl; + @ConfigProperty(name = "vertx.server.port", defaultValue = "8080") + int serverPort; + + public String getOpenApiFilePath() { + return openApiFilePath; + } + + public String getServerUrl() { + return serverUrl; + } + + public int getServerPort() { + return serverPort; + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java new file mode 100644 index 00000000..a0441fef --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java @@ -0,0 +1,35 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.http; + +import io.vertx.core.Vertx; +import org.eclipse.hono.communication.core.app.ApplicationConfig; + +/** + * Abstract class for creating HTTP server in quarkus + * using the managed vertx api + */ +public abstract class AbstractVertxHttpServer { + protected final ApplicationConfig appConfigs; + protected final Vertx vertx; + + + public AbstractVertxHttpServer(ApplicationConfig appConfigs, Vertx vertx) { + this.appConfigs = appConfigs; + this.vertx = vertx; + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpEndpointHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpEndpointHandler.java new file mode 100644 index 00000000..8eba4df6 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpEndpointHandler.java @@ -0,0 +1,32 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.http; + +import io.vertx.ext.web.openapi.RouterBuilder; + +/** + * An vertx endpoint that handles HTTP requests. + */ +public interface HttpEndpointHandler { + /** + * Adds custom routes for handling requests that this endpoint can handle. + * + * @param router The router to add the routes to. + */ + void addRoutes(RouterBuilder router); + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java new file mode 100644 index 00000000..cbfce4c4 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java @@ -0,0 +1,34 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.http; + +/** + * HTTP server service + */ +public interface HttpServer { + + /** + * Starts the http server + */ + void start(); + + /** + * Stops the http server + */ + void stop(); + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java new file mode 100644 index 00000000..8202b152 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java @@ -0,0 +1,34 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base HTTP service class + */ +public class HttpServiceBase { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + private final Map endpoints = new HashMap<>(); + + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java new file mode 100644 index 00000000..c3eb514f --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java @@ -0,0 +1,50 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.utils; + +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgConnectOptions; +import io.vertx.pgclient.PgPool; +import io.vertx.sqlclient.PoolOptions; +import org.eclipse.hono.communication.core.app.ApplicationConfig; + +/** + * Database utilities class + */ +public class DbUtils { + + /** + * Build DB client that is used to manage a pool of connections + * + * @param vertx Vertx context + * @return PostgreSQL pool + */ + public static PgPool createDbClient(Vertx vertx, ApplicationConfig appConfigs) { + var dbConfigs = appConfigs.getDatabaseConfig(); + final PgConnectOptions connectOptions = new PgConnectOptions() + .setHost(dbConfigs.getHost()) + .setPort(dbConfigs.getPort()) + .setDatabase(dbConfigs.getName()) + .setUser(dbConfigs.getUserName()) + .setPassword(dbConfigs.getPassword()); + + final PoolOptions poolOptions = new PoolOptions().setMaxSize(dbConfigs.getPoolMaxSize()); + + return PgPool.pool(vertx, connectOptions, poolOptions); + } + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java new file mode 100644 index 00000000..2af8a2a8 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java @@ -0,0 +1,104 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.core.utils; + +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.validation.BadRequestException; + +import java.util.NoSuchElementException; + +/** + * HTTP Response utilities class + */ +public abstract class ResponseUtils { + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + private static final String APPLICATION_JSON_TYPE = "application/json"; + + + public static void successResponse(RoutingContext rc, + Object response) { + rc.response() + .setStatusCode(200) + .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON_TYPE) + .end(Json.encodePrettily(response)); + } + + /** + * Build success response using 201 Created as its status code and response as its body + * + * @param rc Routing context + * @param response Response body + */ + public static void createdResponse(RoutingContext rc, + Object response) { + rc.response() + .setStatusCode(201) + .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON_TYPE) + .end(Json.encodePrettily(response)); + } + + /** + * Build success response using 204 No Content as its status code and no body + * + * @param rc Routing context + */ + public static void noContentResponse(RoutingContext rc) { + rc.response() + .setStatusCode(204) + .end(); + } + + /** + * Build error response using 400 Bad Request, 404 Not Found or 500 Internal Server Error + * as its status code and throwable as its body + * + * @param rc Routing context + * @param error Throwable exception + */ + public static void errorResponse(RoutingContext rc, Throwable error) { + final int status; + final String message; + + + if (error instanceof IllegalArgumentException + || error instanceof IllegalStateException + || error instanceof NullPointerException + || error instanceof BadRequestException) { + + // Bad Request + status = 400; + message = error.getMessage(); + } else if (error instanceof NoSuchElementException) { + // Not Found + status = 404; + message = error.getMessage(); + } else { + // Internal Server Error + status = 500; + message = "Internal Server Error"; + } + + rc.response() + .setStatusCode(status) + .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON_TYPE) + .end(new JsonObject().put("error", message).encodePrettily()); + } + + +} diff --git a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml new file mode 100644 index 00000000..5b94daa4 --- /dev/null +++ b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml @@ -0,0 +1,206 @@ +--- +openapi: 3.0.2 +info: + title: Hono Device Communication + version: 1.0.0 + description: Device commands and configs API. +paths: + /commands/{tenantid}/{deviceid}: + summary: Device commands + description: Commands for a specific device + post: + requestBody: + description: CommandRequest object as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceCommandRequest' + required: true + tags: + - COMMANDS + responses: + "200": + description: Command was sent successfully + "400": + description: Command can not be send to device + operationId: postCommand + summary: Send a command to device. + parameters: + - name: tenantid + description: Unique registry ID + schema: + type: string + in: path + required: true + - name: deviceid + description: Unique device ID + schema: + type: string + in: path + required: true + /configs/{tenantid}/{deviceid}: + summary: Device configs + description: Configs for a specific device + get: + tags: + - CONFIGS + parameters: + - name: numVersions + description: "The number of versions to list. Versions are listed in decreasing + order of the version number. The maximum number of versions retained is + 10. If this value is zero, it will return all the versions available." + schema: + type: integer + in: query + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ListDeviceConfigVersionsResponse' + description: Lists the device config versions + operationId: listConfigVersions + summary: List a device config versions + description: "Lists the last few versions of the device configuration in descending + order (i.e.: newest first)." + post: + requestBody: + description: DeviceConfigRequest object as JSON + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceConfigRequest' + examples: + Device-configs-example: + value: + versionToUpdate: some text + binaryData: some text + required: true + tags: + - CONFIGS + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceConfig' + description: Device config updated successfully + "400": + description: Validation error + "500": + description: Internal error + operationId: modifyCloudToDeviceConfig + summary: Modify cloud to device config + description: Modifies the configuration for a specific device and Returns the + modified configuration version and its metadata. + parameters: + - name: tenantid + description: Unique registry id + schema: + type: string + in: path + required: true + - name: deviceid + description: Unique device id + schema: + type: string + in: path + required: true +components: + schemas: + DeviceCommandRequest: + title: Root Type for Command + description: Command json object structure + required: + - binaryData + type: object + properties: + binaryData: + description: "The command data to send to the device in base64-encoded string\ + \ format.\r\n\r\n" + type: string + subfolder: + description: "Optional subfolder for the command. If empty, the command + will be delivered to the /devices/{device-id}/commands topic, otherwise + it will be delivered to the /devices/{device-id}/commands/{subfolder} + topic. Multi-level subfolders are allowed. This field must not have + more than 256 characters, and must not contain any MQTT wildcards + (\"+\" or \"#\") or null characters." + type: string + example: + binaryData: A base64-encoded string + subfolder: Optional subfolder for the command + DeviceConfigRequest: + description: Request body for modifying device configs + required: + - versionToUpdate + - binaryData + type: object + properties: + versionToUpdate: + description: "string (int64 format)\r\n\r\nThe version number to update. + If this value is zero, it will not check the version number of the server + and will always update the current version; otherwise, this update will + fail if the version number found on the server does not match this version + number. This is used to support multiple simultaneous updates without + losing data." + type: string + binaryData: + description: "string (bytes format)\r\n\r\nThe configuration data for the + device in string base64-encoded format.\r\n" + type: string + ListDeviceConfigVersionsResponse: + title: Root Type for ListDeviceConfigVersionsResponse + description: A list of a device config versions + type: object + properties: + deviceConfigs: + description: List of DeviceConfig objects + type: array + items: + $ref: '#/components/schemas/DeviceConfig' + example: + deviceConfigs: + - object: DeviceConfigResponse + DeviceConfig: + title: Root Type for DeviceConfigResponse + description: The device configuration. + type: object + properties: + version: + description: "String (int64 format) [Output only] The version + of this update. The version number is assigned by the server, and is + always greater than 0 after device creation. The version must be 0 on + the devices.create request if a config is specified; the response of + devices.create will always have a value of 1." + type: string + cloudUpdateTime: + description: "String (Timestamp format) [Output only] The time at + which this configuration version was updated in Cloud IoT Core. This + timestamp is set by the server. Timestamp in + RFC3339 UTC \"Zulu\" format, accurate to nanoseconds. + Example: \"2014-10-02T15:01:23.045123456Z\"." + type: string + deviceAckTime: + description: "string (Timestamp format) [Output only] The time at + which Cloud IoT Core received the acknowledgment from the device, indicating + that the device has received this configuration version. If this field + is not present, the device has not yet acknowledged that it received + this version. Note that when the config was sent to the device, many + config versions may have been available in Cloud IoT Core while the + device was disconnected, and on connection, only the latest version + is sent to the device. Some versions may never be sent to the device, + and therefore are never acknowledged. This timestamp is set by Cloud + IoT Core. Timestamp in RFC3339 UTC \"Zulu\" format, accurate + to nanoseconds. Example: \"2014-10-02T15:01:23.045123456Z\"." + type: string + binaryData: + description: "string (bytes format) The device configuration data + in string base64-encoded format." + type: string + example: + version: string + cloudUpdateTime: string + deviceAckTime: string + binaryData: string diff --git a/device-communication/src/main/resources/application.yaml b/device-communication/src/main/resources/application.yaml new file mode 100644 index 00000000..f1618822 --- /dev/null +++ b/device-communication/src/main/resources/application.yaml @@ -0,0 +1,37 @@ +app: + name: "Device Communication" + version: "v1" +vertx: + openapi: + file: "/api/hono-device-communication-v1.yaml" + server: + url: "localhost" + port: 8081 + +quarkus: + flyway: + migrate-at-start: true + # Device registration table configs. Used for validating devices + device-registration: + table: "device_registration" + tenant-id-column: "tenant_id" + device-id-column: "device_id" + datasource: + pool-max-size: 5 + name: "hono" + host: "localhost" + port: 5432 + username: "postgres" + password: "mysecretpassword" + db-kind: "postgresql" + jdbc: + url: "jdbc:postgresql://localhost:5432/hono" + + +"%dev": + app: + name: "Device Communication" + vertx: + server: + url: "localhost" + port: "8081" \ No newline at end of file diff --git a/device-communication/src/main/resources/db/migration/V1.0__create.sql b/device-communication/src/main/resources/db/migration/V1.0__create.sql new file mode 100644 index 00000000..69fb9487 --- /dev/null +++ b/device-communication/src/main/resources/db/migration/V1.0__create.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS public."deviceConfig" +( + version INT not null, + "tenantId" VARCHAR(100) not null, + "deviceId" VARCHAR(100) not null, + "cloudUpdateTime" VARCHAR(100) not null, + "deviceAckTime" VARCHAR(100), + "binaryData" VARCHAR not null, + + PRIMARY KEY (version, "tenantId", "deviceId") +) \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java new file mode 100644 index 00000000..53425a75 --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java @@ -0,0 +1,56 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api; + +import io.vertx.core.Vertx; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.http.HttpServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.*; + +class ApplicationTest { + + private HttpServer httpServerMock; + private Application application; + + @BeforeEach + void setUp() { + httpServerMock = mock(HttpServer.class); + Vertx vertxMock = mock(Vertx.class); + ApplicationConfig appConfigs = mock(ApplicationConfig.class); + application = new Application(vertxMock, appConfigs, httpServerMock); + + verifyNoMoreInteractions(httpServerMock, appConfigs, vertxMock); + } + + @AfterEach + void tearDown() { + } + + @Test + void doStart() { + doNothing().when(httpServerMock).start(); + + application.doStart(); + + verify(httpServerMock, times(1)).start(); + + } +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java new file mode 100644 index 00000000..d30d376b --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java @@ -0,0 +1,317 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api; + + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.quarkus.runtime.Quarkus; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.http.*; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.validation.BadRequestException; +import org.eclipse.hono.communication.api.handler.DeviceCommandsHandler; +import org.eclipse.hono.communication.api.service.DatabaseService; +import org.eclipse.hono.communication.api.service.DatabaseServiceImpl; +import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.ServerConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class DeviceCommunicationHttpServerTest { + + + private ApplicationConfig appConfigsMock; + private VertxHttpHandlerManagerService handlerServiceMock; + private Vertx vertxMock; + private Router routerMock; + private RouterBuilder routerBuilderMock; + private HttpServer httpServerMock; + private DeviceCommunicationHttpServer deviceCommunicationHttpServer; + private RoutingContext routingContextMock; + private HttpServerResponse httpServerResponseMock; + private HttpServerRequest httpServerRequestMock; + private BadRequestException badRequestExceptionMock; + private JsonObject jsonObjMock; + + private DatabaseService dbMock; + private ServerConfig serverConfigMock; + + @BeforeEach + void setUp() { + handlerServiceMock = mock(VertxHttpHandlerManagerService.class); + vertxMock = mock(Vertx.class); + routerMock = mock(Router.class); + routerBuilderMock = mock(RouterBuilder.class); + appConfigsMock = mock(ApplicationConfig.class); + serverConfigMock = mock(ServerConfig.class); + httpServerMock = mock(HttpServer.class); + routingContextMock = mock(RoutingContext.class); + httpServerResponseMock = mock(HttpServerResponse.class); + httpServerRequestMock = mock(HttpServerRequest.class); + badRequestExceptionMock = mock(BadRequestException.class); + jsonObjMock = mock(JsonObject.class); + dbMock = mock(DatabaseServiceImpl.class); + deviceCommunicationHttpServer = new DeviceCommunicationHttpServer(appConfigsMock, vertxMock, handlerServiceMock, dbMock); + + } + + + @AfterEach + void tearDown() { + Mockito.verifyNoMoreInteractions(handlerServiceMock, + vertxMock, + routerMock, + routerBuilderMock, + appConfigsMock, + httpServerMock, + routingContextMock, + httpServerResponseMock, + httpServerRequestMock, + badRequestExceptionMock, + jsonObjMock, + dbMock, + serverConfigMock); + } + + + @Test + void startSucceeded() { + var mockedCommandService = mock(DeviceCommandsHandler.class); + Mockito.verifyNoMoreInteractions(mockedCommandService); + doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); + try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { + mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) + .thenReturn(Future.succeededFuture(routerBuilderMock)); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); + when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of(mockedCommandService)); + when(routerBuilderMock.createRouter()).thenReturn(routerMock); + when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); + when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); + when(httpServerMock.requestHandler(routerMock)).thenReturn(httpServerMock); + when(httpServerMock.listen()).thenReturn(Future.succeededFuture(httpServerMock)); + when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); + + try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { + + this.deviceCommunicationHttpServer.start(); + + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); + + verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); + verify(routerBuilderMock, times(1)).createRouter(); + + verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); + verify(httpServerMock, times(1)).requestHandler(routerMock); + verify(httpServerMock, times(1)).listen(); + verify(appConfigsMock, times(2)).getServerConfig(); + verify(serverConfigMock, times(2)).getServerUrl(); + verify(serverConfigMock, times(2)).getServerPort(); + verify(mockedCommandService, times(1)).addRoutes(routerBuilderMock); + verify(serverConfigMock, times(1)).getOpenApiFilePath(); + quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); + quarkusMockedStatic.verifyNoMoreInteractions(); + + } + + } + + + } + + @Test + void createRouterFailed() { + var mockedCommandService = mock(DeviceCommandsHandler.class); + Mockito.verifyNoMoreInteractions(mockedCommandService); + doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); + try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { + mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) + .thenReturn(Future.failedFuture(new RuntimeException())); + when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); + + try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { + this.deviceCommunicationHttpServer.start(); + + verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); + verify(appConfigsMock, times(1)).getServerConfig(); + verify(serverConfigMock, times(1)).getOpenApiFilePath(); + quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); + quarkusMockedStatic.verifyNoMoreInteractions(); + } + + } + } + + + @Test + void createServerFailed() { + var mockedCommandService = mock(DeviceCommandsHandler.class); + Mockito.verifyNoMoreInteractions(mockedCommandService); + doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); + try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { + mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) + .thenReturn(Future.succeededFuture(routerBuilderMock)); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); + when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of()); + when(routerBuilderMock.createRouter()).thenReturn(routerMock); + when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); + when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); + when(httpServerMock.requestHandler(routerMock)).thenReturn(httpServerMock); + when(httpServerMock.listen()).thenReturn(Future.failedFuture(new Throwable())); + when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); + + try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { + + this.deviceCommunicationHttpServer.start(); + + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); + + verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); + verify(routerBuilderMock, times(1)).createRouter(); + verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); + verify(httpServerMock, times(1)).requestHandler(routerMock); + verify(httpServerMock, times(1)).listen(); + verify(appConfigsMock, times(2)).getServerConfig(); + verify(serverConfigMock, times(1)).getOpenApiFilePath(); + verify(serverConfigMock, times(1)).getServerPort(); + verify(serverConfigMock, times(1)).getServerUrl(); + quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); + quarkusMockedStatic.verifyNoMoreInteractions(); + } + + } + } + + + @Test + void addDefault400ExceptionHandler() { + var errorMsg = "This is an error message"; + int code = 400; + + when(routingContextMock.failure()).thenReturn(badRequestExceptionMock); + when(badRequestExceptionMock.toJson()).thenReturn(jsonObjMock); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.setStatusCode(code)).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.end()).thenReturn(Future.succeededFuture()); + when(jsonObjMock.toString()).thenReturn(errorMsg); + + deviceCommunicationHttpServer.addDefault400ExceptionHandler(routingContextMock); + + verify(routingContextMock, times(1)).response(); + verify(routingContextMock, times(1)).failure(); + verify(badRequestExceptionMock, times(1)).toJson(); + verify(httpServerResponseMock, times(1)).setStatusCode(code); + verify(httpServerResponseMock, times(1)).end(errorMsg); + + } + + @Test + void addDefault404ExceptionHandlerPutsHeader() { + + var errorMsg = "This is an error message"; + int code = 404; + + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.putHeader(eq(HttpHeaderNames.CONTENT_TYPE), + anyString())).thenReturn(httpServerResponseMock); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(routingContextMock.request()).thenReturn(httpServerRequestMock); + when(httpServerResponseMock.ended()).thenReturn(Boolean.FALSE); + when(routingContextMock.failure()).thenReturn(badRequestExceptionMock); + when(badRequestExceptionMock.toJson()).thenReturn(jsonObjMock); + when(httpServerRequestMock.method()).thenReturn(HttpMethod.GET); + when(httpServerResponseMock.setStatusCode(code)).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.end()).thenReturn(Future.succeededFuture()); + when(jsonObjMock.toString()).thenReturn(errorMsg); + + deviceCommunicationHttpServer.addDefault404ExceptionHandler(routingContextMock); + + verify(routingContextMock, times(3)).response(); + verify(routingContextMock, times(1)).request(); + verify(httpServerResponseMock, times(1)).setStatusCode(code); + verify(httpServerResponseMock, times(1)).putHeader(eq(HttpHeaderNames.CONTENT_TYPE), + anyString()); + verify(httpServerResponseMock, times(1)).end(anyString()); + verify(httpServerResponseMock, times(1)).ended(); + verify(httpServerRequestMock, times(1)).method(); + + } + + @Test + void addDefault404ExceptionHandlerResponseEnded() { + + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.ended()).thenReturn(Boolean.TRUE); + + deviceCommunicationHttpServer.addDefault404ExceptionHandler(routingContextMock); + + verify(routingContextMock, times(1)).response(); + verify(httpServerResponseMock, times(1)).ended(); + + } + + @Test + void addDefault404ExceptionHandlerMethodEqualsHead() { + + var errorMsg = "This is an error message"; + int code = 404; + + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.ended()).thenReturn(Boolean.FALSE); + when(routingContextMock.failure()).thenReturn(badRequestExceptionMock); + when(badRequestExceptionMock.toJson()).thenReturn(jsonObjMock); + + when(routingContextMock.request()).thenReturn(httpServerRequestMock); + when(httpServerRequestMock.method()).thenReturn(HttpMethod.HEAD); + when(httpServerResponseMock.setStatusCode(code)).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.end()).thenReturn(Future.succeededFuture()); + when(jsonObjMock.toString()).thenReturn(errorMsg); + + deviceCommunicationHttpServer.addDefault404ExceptionHandler(routingContextMock); + + verify(routingContextMock, times(3)).response(); + verify(routingContextMock, times(1)).request(); + verify(httpServerResponseMock, times(1)).setStatusCode(code); + + verify(httpServerResponseMock, times(1)).end(); + verify(httpServerResponseMock, times(1)).ended(); + verify(httpServerRequestMock, times(1)).method(); + } + + @Test + void stop() { + doNothing().when(dbMock).close(); + deviceCommunicationHttpServer.stop(); + verify(dbMock).close(); + } +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java new file mode 100644 index 00000000..ee96c36a --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java @@ -0,0 +1,89 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.handler; + +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.Operation; +import io.vertx.ext.web.openapi.RouterBuilder; +import org.eclipse.hono.communication.api.config.DeviceCommandConstants; +import org.eclipse.hono.communication.api.service.DeviceCommandService; +import org.eclipse.hono.communication.api.service.DeviceCommandServiceImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class DeviceCommandsHandlerTest { + + private final DeviceCommandService commandServiceMock; + private final RouterBuilder routerBuilderMock; + private final RoutingContext routingContextMock; + private final Operation operationMock; + private final DeviceCommandsHandler deviceCommandsHandler; + + public DeviceCommandsHandlerTest() { + operationMock = mock(Operation.class); + commandServiceMock = mock(DeviceCommandServiceImpl.class); + routerBuilderMock = mock(RouterBuilder.class); + routingContextMock = mock(RoutingContext.class); + deviceCommandsHandler = new DeviceCommandsHandler(commandServiceMock); + } + + @AfterEach + void tearDown() { + verifyNoMoreInteractions( + commandServiceMock, + routerBuilderMock, + routingContextMock, + operationMock); + } + + @BeforeEach + void setUp() { + verifyNoMoreInteractions( + commandServiceMock, + routerBuilderMock, + routingContextMock, + operationMock); + + } + + @Test + void addRoutes() { + when(routerBuilderMock.operation(anyString())).thenReturn(operationMock); + when(operationMock.handler(any())).thenReturn(operationMock); + + deviceCommandsHandler.addRoutes(routerBuilderMock); + + verify(routerBuilderMock, times(1)).operation(DeviceCommandConstants.POST_DEVICE_COMMAND_OP_ID); + verify(operationMock, times(1)).handler(any()); + + } + + @Test + void handlePostCommand() { + doNothing().when(commandServiceMock).postCommand(routingContextMock); + + deviceCommandsHandler.handlePostCommand(routingContextMock); + + verify(commandServiceMock, times(1)).postCommand(routingContextMock); + } + + +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java new file mode 100644 index 00000000..94d2640f --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java @@ -0,0 +1,238 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.handler; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RequestBody; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.Operation; +import io.vertx.ext.web.openapi.RouterBuilder; +import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; +import org.eclipse.hono.communication.api.entity.DeviceConfig; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.service.DeviceConfigService; +import org.eclipse.hono.communication.api.service.DeviceConfigServiceImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class DeviceConfigsHandlerTest { + + private final DeviceConfigService configServiceMock; + private final RouterBuilder routerBuilderMock; + private final RoutingContext routingContextMock; + private final Operation operationMock; + private final RequestBody requestBodyMock; + private final DeviceConfigsHandler deviceConfigsHandler; + private final JsonObject jsonObjMock; + private final HttpServerResponse httpServerResponseMock; + private final IllegalArgumentException illegalArgumentExceptionMock; + + private final String tenantID = "tenant_ID"; + private final String deviceID = "device_ID"; + private final String errorMsg = "test_error"; + DeviceConfigRequest deviceConfigRequest = new DeviceConfigRequest("1", "binary_data"); + DeviceConfigEntity deviceConfigEntity = new DeviceConfigEntity(); + DeviceConfig deviceConfig = new DeviceConfig(); + + public DeviceConfigsHandlerTest() { + operationMock = mock(Operation.class); + configServiceMock = mock(DeviceConfigServiceImpl.class); + routerBuilderMock = mock(RouterBuilder.class); + routingContextMock = mock(RoutingContext.class); + requestBodyMock = mock(RequestBody.class); + jsonObjMock = mock(JsonObject.class); + httpServerResponseMock = mock(HttpServerResponse.class); + illegalArgumentExceptionMock = mock(IllegalArgumentException.class); + deviceConfigsHandler = new DeviceConfigsHandler(configServiceMock); + + deviceConfigEntity.setVersion(1); + deviceConfigEntity.setTenantId(tenantID); + deviceConfigEntity.setDeviceId(deviceID); + + deviceConfig.setVersion(""); + + } + + @BeforeEach + void setUp() { + + } + + @AfterEach + void tearDown() { + verifyNoMoreInteractions( + configServiceMock, + routerBuilderMock, + routingContextMock, + operationMock, + requestBodyMock, + jsonObjMock, + httpServerResponseMock, + illegalArgumentExceptionMock); + } + + @Test + void addRoutes() { + when(routerBuilderMock.operation(anyString())).thenReturn(operationMock); + when(operationMock.handler(any())).thenReturn(operationMock); + + deviceConfigsHandler.addRoutes(routerBuilderMock); + + verify(routerBuilderMock, times(1)).operation(DeviceConfigsConstants.LIST_CONFIG_VERSIONS_OP_ID); + verify(routerBuilderMock, times(1)).operation(DeviceConfigsConstants.POST_MODIFY_DEVICE_CONFIG_OP_ID); + verify(operationMock, times(2)).handler(any()); + } + + @Test + void handleModifyCloudToDeviceConfig_success() { + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.body()).thenReturn(requestBodyMock); + when(requestBodyMock.asJsonObject()).thenReturn(jsonObjMock); + when(jsonObjMock.mapTo(DeviceConfigRequest.class)).thenReturn(deviceConfigRequest); + when(configServiceMock.modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID)).thenReturn(Future.succeededFuture(deviceConfigEntity)); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.putHeader("Content-Type", + "application/json")).thenReturn(httpServerResponseMock); + + var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); + + verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).body(); + verify(requestBodyMock, times(1)).asJsonObject(); + verify(jsonObjMock, times(1)).mapTo(DeviceConfigRequest.class); + verifySuccessResponse(results, deviceConfigEntity); + + } + + + @Test + void handleModifyCloudToDeviceConfig_failure() { + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.body()).thenReturn(requestBodyMock); + when(requestBodyMock.asJsonObject()).thenReturn(jsonObjMock); + when(jsonObjMock.mapTo(DeviceConfigRequest.class)).thenReturn(deviceConfigRequest); + when(illegalArgumentExceptionMock.getMessage()).thenReturn(errorMsg); + when(configServiceMock.modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID)).thenReturn(Future.failedFuture(illegalArgumentExceptionMock)); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.putHeader("Content-Type", + "application/json")).thenReturn(httpServerResponseMock); + + var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); + + verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).body(); + verify(requestBodyMock, times(1)).asJsonObject(); + verify(jsonObjMock, times(1)).mapTo(DeviceConfigRequest.class); + + verifyErrorResponse(results); + + + } + + @Test + void handleListConfigVersions_success() { + ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = new ListDeviceConfigVersionsResponse(List.of(deviceConfig)); + MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); + when(routingContextMock.queryParams()).thenReturn(queryParams); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.putHeader("Content-Type", + "application/json")).thenReturn(httpServerResponseMock); + when(configServiceMock.listAll(deviceID, tenantID, 10)).thenReturn(Future.succeededFuture(listDeviceConfigVersionsResponse)); + + var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); + + verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); + verify(routingContextMock, times(1)).queryParams(); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + + verifySuccessResponse(results, listDeviceConfigVersionsResponse); + + } + + + @Test + void handleListConfigVersions_failed() { + MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); + when(routingContextMock.queryParams()).thenReturn(queryParams); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.response()).thenReturn(httpServerResponseMock); + when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); + when(illegalArgumentExceptionMock.getMessage()).thenReturn(errorMsg); + when(httpServerResponseMock.putHeader("Content-Type", + "application/json")).thenReturn(httpServerResponseMock); + when(configServiceMock.listAll(deviceID, tenantID, 10)).thenReturn(Future.failedFuture(illegalArgumentExceptionMock)); + + var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); + + verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); + verify(routingContextMock, times(1)).queryParams(); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verifyErrorResponse(results); + + + } + + + void verifyErrorResponse(Future results) { + verify(routingContextMock, times(1)).response(); + verify(httpServerResponseMock).setStatusCode(400); + verify(illegalArgumentExceptionMock).getMessage(); + verify(httpServerResponseMock).putHeader("Content-Type", + "application/json"); + verify(httpServerResponseMock).end(new JsonObject().put("error", errorMsg).encodePrettily()); + Assertions.assertTrue(results.failed()); + } + + void verifySuccessResponse(Future results, Object responseObj) { + verify(routingContextMock, times(1)).response(); + verify(httpServerResponseMock).setStatusCode(200); + verify(httpServerResponseMock).putHeader("Content-Type", + "application/json"); + verify(httpServerResponseMock).end(Json.encodePrettily(responseObj)); + Assertions.assertTrue(results.succeeded()); + } +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java new file mode 100644 index 00000000..8fffb4a0 --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java @@ -0,0 +1,81 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgPool; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.utils.DbUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.mockito.Mockito.*; + +class DatabaseServiceImplTest { + + private final Vertx vertxMock; + private final ApplicationConfig applicationConfigMock; + + private final PgPool pgPoolMock; + private DatabaseService databaseService; + + public DatabaseServiceImplTest() { + vertxMock = mock(Vertx.class); + applicationConfigMock = mock(ApplicationConfig.class); + pgPoolMock = mock(PgPool.class); + + + } + + @AfterEach + void tearDown() { + verifyNoMoreInteractions(vertxMock, applicationConfigMock, pgPoolMock); + + } + + + @Test + void getDbClient() { + try (MockedStatic dbUtilsMockedStatic = mockStatic(DbUtils.class)) { + dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock)).thenReturn(pgPoolMock); + databaseService = new DatabaseServiceImpl(applicationConfigMock, vertxMock); + + var client = databaseService.getDbClient(); + + Assertions.assertSame(client, pgPoolMock); + + dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock), times(1)); + dbUtilsMockedStatic.verifyNoMoreInteractions(); + } + } + + @Test + void close() { + + try (MockedStatic dbUtilsMockedStatic = mockStatic(DbUtils.class)) { + dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock)).thenReturn(pgPoolMock); + databaseService = new DatabaseServiceImpl(applicationConfigMock, vertxMock); + + databaseService.close(); + verify(pgPoolMock, times(1)).close(); + dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock), times(1)); + dbUtilsMockedStatic.verifyNoMoreInteractions(); + } + } +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java new file mode 100644 index 00000000..8ecbf615 --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java @@ -0,0 +1,121 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.core.Future; +import io.vertx.pgclient.PgPool; +import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; +import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; +import org.eclipse.hono.communication.api.repository.DeviceConfigsRepositoryImpl; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.*; + +class DeviceConfigServiceImplTest { + + private final DeviceConfigsRepository repositoryMock; + private final DatabaseService dbMock; + private final PgPool poolMock; + private final DeviceConfigMapper mapperMock; + private final DeviceConfigService deviceConfigService; + + private final String tenantId = "tenant_ID"; + private final String deviceId = "device_ID"; + + public DeviceConfigServiceImplTest() { + this.repositoryMock = mock(DeviceConfigsRepositoryImpl.class); + this.dbMock = mock(DatabaseServiceImpl.class); + this.mapperMock = mock(DeviceConfigMapper.class); + poolMock = mock(PgPool.class); + deviceConfigService = new DeviceConfigServiceImpl(repositoryMock, dbMock, mapperMock); + } + + + @AfterEach + void tearDown() { + verifyNoMoreInteractions(repositoryMock, mapperMock, dbMock); + } + + @Test + void modifyCloudToDeviceConfig_success() { + var deviceConfigRequest = new DeviceConfigRequest(); + var deviceConfigEntity = new DeviceConfigEntity(); + when(mapperMock.configRequestToDeviceConfigEntity(deviceConfigRequest)).thenReturn(deviceConfigEntity); + when(dbMock.getDbClient()).thenReturn(poolMock); + when(poolMock.withTransaction(any())).thenReturn(Future.succeededFuture(deviceConfigEntity)); + + + var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); + + verify(mapperMock, times(1)).configRequestToDeviceConfigEntity(deviceConfigRequest); + verify(dbMock, times(1)).getDbClient(); + verify(poolMock, times(1)).withTransaction(any()); + Assertions.assertTrue(results.succeeded()); + } + + @Test + void modifyCloudToDeviceConfig_failure() { + var deviceConfigRequest = new DeviceConfigRequest(); + var deviceConfigEntity = new DeviceConfigEntity(); + when(mapperMock.configRequestToDeviceConfigEntity(deviceConfigRequest)).thenReturn(deviceConfigEntity); + when(dbMock.getDbClient()).thenReturn(poolMock); + when(poolMock.withTransaction(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); + + + var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); + + verify(mapperMock, times(1)).configRequestToDeviceConfigEntity(deviceConfigRequest); + verify(dbMock, times(1)).getDbClient(); + verify(poolMock, times(1)).withTransaction(any()); + Assertions.assertTrue(results.failed()); + } + + @Test + void listAll_success() { + var deviceConfigVersions = new ListDeviceConfigVersionsResponse(); + when(dbMock.getDbClient()).thenReturn(poolMock); + when(poolMock.withTransaction(any())).thenReturn(Future.succeededFuture(deviceConfigVersions)); + + var results = deviceConfigService.listAll(deviceId, tenantId, 10); + + verify(dbMock, times(1)).getDbClient(); + verify(poolMock, times(1)).withTransaction(any()); + Assertions.assertTrue(results.succeeded()); + + + } + + + @Test + void listAll_failed() { + when(dbMock.getDbClient()).thenReturn(poolMock); + when(poolMock.withTransaction(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); + + var results = deviceConfigService.listAll(deviceId, tenantId, 10); + + verify(dbMock, times(1)).getDbClient(); + verify(poolMock, times(1)).withTransaction(any()); + Assertions.assertTrue(results.failed()); + + + } +} \ No newline at end of file diff --git a/device-communication/src/test/resources/application.yaml b/device-communication/src/test/resources/application.yaml new file mode 100644 index 00000000..a2f4cbaf --- /dev/null +++ b/device-communication/src/test/resources/application.yaml @@ -0,0 +1,16 @@ +app: + name: "Device Communication" +vertx: + openapi: + file: "/api/hono-device-communication-v1.yaml" + server: + url: "localhost" + port: 8081 + +"%test": + app: + name: "Device Communication" + vertx: + server: + url: "localhost" + port: "8081" \ No newline at end of file From f0253064ee44dc90c405bdbcc7801465b3c1685e Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Fri, 10 Feb 2023 13:34:43 +0100 Subject: [PATCH 02/18] update README.md and add database functionality --- README.md | 10 +- device-communication/.dockerignore | 3 +- device-communication/README.md | 116 ++++++++--- device-communication/img.png | Bin 0 -> 110144 bytes device-communication/pom.xml | 79 ++++---- .../src/main/docker/Dockerfile.legacy-jar | 6 +- .../api/DeviceCommunicationHttpServer.java | 72 ++++++- .../api/config/DeviceConfigsConstants.java | 3 + .../api/data/DeviceCommandRequest.java | 84 ++++++++ .../communication/api/data/DeviceConfig.java | 128 ++++++++++++ .../api/data/DeviceConfigEntity.java | 116 +++++++++++ .../api/data/DeviceConfigRequest.java | 84 ++++++++ .../api/data/DeviceConfigResponse.java | 84 ++++++++ .../ListDeviceConfigVersionsResponse.java | 72 +++++++ .../api/handler/DeviceConfigsHandler.java | 15 +- .../api/mapper/DeviceConfigMapper.java | 13 +- .../repository/DeviceConfigsRepository.java | 6 +- .../DeviceConfigsRepositoryImpl.java | 140 ++++++------- .../api/service/DatabaseSchemaCreator.java | 24 +++ .../service/DatabaseSchemaCreatorImpl.java | 76 +++++++ .../api/service/DatabaseServiceImpl.java | 16 +- .../api/service/DeviceConfigService.java | 9 +- .../api/service/DeviceConfigServiceImpl.java | 43 ++-- .../core/app/DatabaseConfig.java | 18 +- .../communication/core/app/ServerConfig.java | 22 ++ .../communication/core/utils/DbUtils.java | 25 ++- .../api/hono-device-communication-v1.yaml | 58 ++++-- .../src/main/resources/application.yaml | 55 +++-- .../db/create_device_config_table.sql | 28 +++ .../DeviceCommunicationHttpServerTest.java | 191 ++++++++++++------ .../api/handler/DeviceConfigsHandlerTest.java | 13 +- .../DatabaseSchemaCreatorImplTest.java | 91 +++++++++ .../api/service/DatabaseServiceImplTest.java | 20 +- .../service/DeviceConfigServiceImplTest.java | 14 +- 34 files changed, 1380 insertions(+), 354 deletions(-) create mode 100644 device-communication/img.png create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java create mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java create mode 100644 device-communication/src/main/resources/db/create_device_config_table.sql create mode 100644 device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java diff --git a/README.md b/README.md index 3a7347db..ed9bff77 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,14 @@ Eclipse Hono Extras provides additional resources for [Eclipse Hono™](http ## Hono Protocol Gateways -See the [protocol-gateway](protocol-gateway) directory for a template and an example implementation for a [Hono protocol gateway](https://www.eclipse.org/hono/docs/concepts/connecting-devices/#connecting-via-a-protocol-gateway). +See the [protocol-gateway](protocol-gateway) directory for a template and an example implementation for +a [Hono protocol gateway](https://www.eclipse.org/hono/docs/concepts/connecting-devices/#connecting-via-a-protocol-gateway). ## Device registry migration -See the [device-registry-migration](device-registry-migration) directory for example migration scripts from one device registry type to another. +See the [device-registry-migration](device-registry-migration) directory for example migration scripts from one device +registry type to another. + +## Device communication API + +See the [device-communication](device-communication) directory for api specifications and implementation. diff --git a/device-communication/.dockerignore b/device-communication/.dockerignore index 94810d00..1e5989c0 100644 --- a/device-communication/.dockerignore +++ b/device-communication/.dockerignore @@ -2,4 +2,5 @@ !target/*-runner !target/*-runner.jar !target/lib/* -!target/quarkus-app/* \ No newline at end of file +!target/quarkus-app/* +!src/main/resources/* \ No newline at end of file diff --git a/device-communication/README.md b/device-communication/README.md index ce13e7f2..f036816d 100644 --- a/device-communication/README.md +++ b/device-communication/README.md @@ -1,54 +1,104 @@ -# device-communication Project +# Device Communication API -This project uses Quarkus, the Supersonic Subatomic Java Framework. +Device communication API enables users and applications to send configurations and commands to devices via HTTP +endpoints. -If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . +![img.png](img.png) -## Running the application in dev mode +### Application -You can run your application in dev mode that enables live coding using: +The application is reactive and uses Quarkus Framework for the application and Vertx tools for the HTTP server. -```shell script -./mvnw compile quarkus:dev -``` +### Hono internal communication -> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. +API uses [Googles PubSub](https://cloud.google.com/pubsub/docs/overview?hl=de) service to communicate with the command +router. -## Packaging and running the application +## API endpoints -The application can be packaged using: +#### commands/{tenantId}/{deviceId} -```shell script -./mvnw package -``` +- POST : post a command for a specific device -It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. -Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. +

-The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. +#### configs/{tenantId}/{deviceId} -If you want to build an _über-jar_, execute the following command: +- GET : list of device config versions -```shell script -./mvnw package -Dquarkus.package.type=uber-jar -``` +- POST: update or create a device config version -The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. +For more information please see resources/api/openApi file. -## Creating a native executable +## Database -You can create a native executable using: +Application uses PostgresSQL database. All the database configurations can be found in application.yaml file. -```shell script -./mvnw package -Pnative -``` +### Tables -Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +- DeviceConfig
+ Is used for saving device config versions +- DeviceRegistration
+ Is used for validating if a device exist -```shell script -./mvnw package -Pnative -Dquarkus.native.container-build=true -``` +### Migrations + +When Applications starts tables will be created by the DatabaseSchemaCreator service. + +### Running postgresSQL container local + +For running the PostgresSQL Database local with docker run: + +`````` + +docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres + +`````` + +After the container is running, log in to the container and with psql create the database and the tables. Then we have +to set the application settings. + +Default postgresSQl values: + +- userName = postgres +- password = mysecretpassword + +## Build and Push API Docker Image + +Mavens auto build and push functionality ca be enabled from application.yaml settings: + +```` + +quarkus: + container-image: + builder: docker + build: true + push: true + image: "gcr.io/sotec-iot-core-dev/hono-device-communication" + +```` + +By running maven package, install or deploy will automatically build the docker image and if push is enabled it will +push the image +to the given registry. + +## OpenApi Contract-first + +For creating the endpoints, Vertx takes the openApi definition file and maps every endpoint operation-ID with a specific +Handler +function. + +## Handlers + +Handlers are providing callBack functions for every endpoint. Functions are going to be called automatically from vertx +server every time a request is received. + +## Adding a new Endpoint + +Adding new Endpoint steps: + +1. Add Endpoint in openApi file and set an operationId +2. Use an existing const Class or create a new one under /config and set the operation id name +3. Implement an HttpEndpointHandler and set the Routes -You can then execute your native executable with: `./target/device-communication-1.0-SNAPSHOT-runner` -If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. diff --git a/device-communication/img.png b/device-communication/img.png new file mode 100644 index 0000000000000000000000000000000000000000..aff6c99d8d5427eadb35a33d4cfb8db19f1dd16d GIT binary patch literal 110144 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%0A+#6>AyKUE%W_<# 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

CPgYW4pq~ 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 - io.quarkus - quarkus-config-yaml - - - - - io.quarkus - quarkus-flyway + com.fasterxml.jackson.core + jackson-databind - - io.quarkus - quarkus-jdbc-postgresql + quarkus-config-yaml - - org.mockito mockito-inline @@ -113,6 +103,19 @@ mapstruct ${org.mapstruct.version} + + + + io.quarkus + quarkus-container-image-docker + 2.16.0.Final + + + io.vertx + vertx-health-check + + + @@ -150,15 +153,7 @@ - - maven-compiler-plugin - ${compiler-plugin.version} - - - -parameters - - - + maven-surefire-plugin ${surefire-plugin.version} @@ -171,26 +166,26 @@ - - maven-failsafe-plugin - ${surefire-plugin.version} - - - - integration-test - verify - - - - ${project.build.directory}/${project.build.finalName}-runner - - org.jboss.logmanager.LogManager - ${maven.home} - - - - - + + + + + + + + + + + + + + + + + + + + diff --git a/device-communication/src/main/docker/Dockerfile.legacy-jar b/device-communication/src/main/docker/Dockerfile.legacy-jar index e1a0c7e6..108988bd 100644 --- a/device-communication/src/main/docker/Dockerfile.legacy-jar +++ b/device-communication/src/main/docker/Dockerfile.legacy-jar @@ -75,11 +75,15 @@ # accessed directly. (example: "foo.example.com,bar.example.com") # ### -FROM registry.access.redhat.com/ubi8/openjdk-17:1.14 +FROM registry.access.redhat.com/ubi8/openjdk-17:1.14-10 ENV LANGUAGE='en_US:en' +RUN mkdir api/ +RUN mkdir db/ +COPY target/classes/api/* /api/ +COPY target/classes/db/* /db/ COPY target/lib/* /deployments/lib/ COPY target/*-runner.jar /deployments/quarkus-run.jar diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index 95086423..dd631f3f 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -21,16 +21,21 @@ import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.healthchecks.HealthCheckHandler; +import io.vertx.ext.healthchecks.Status; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.openapi.RouterBuilder; import io.vertx.ext.web.validation.BadRequestException; +import org.eclipse.hono.communication.api.service.DatabaseSchemaCreator; import org.eclipse.hono.communication.api.service.DatabaseService; import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.ServerConfig; import org.eclipse.hono.communication.core.http.AbstractVertxHttpServer; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; import org.eclipse.hono.communication.core.http.HttpServer; +import org.eclipse.hono.communication.core.utils.ResponseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,15 +55,18 @@ public class DeviceCommunicationHttpServer extends AbstractVertxHttpServer imple private final VertxHttpHandlerManagerService httpHandlerManager; private final DatabaseService db; + private final DatabaseSchemaCreator databaseSchemaCreator; private List httpEndpointHandlers; public DeviceCommunicationHttpServer(ApplicationConfig appConfigs, Vertx vertx, VertxHttpHandlerManagerService httpHandlerManager, - DatabaseService databaseService) { + DatabaseService databaseService, + DatabaseSchemaCreator databaseSchemaCreator) { super(appConfigs, vertx); this.httpHandlerManager = httpHandlerManager; + this.databaseSchemaCreator = databaseSchemaCreator; this.httpEndpointHandlers = new ArrayList<>(); this.db = databaseService; } @@ -66,6 +74,10 @@ public DeviceCommunicationHttpServer(ApplicationConfig appConfigs, @Override public void start() { + //Create Database Tables + databaseSchemaCreator.createDBTables(); + + // Create Endpoints Router this.httpEndpointHandlers = httpHandlerManager.getAvailableHandlerServices(); RouterBuilder.create(this.vertx, appConfigs.getServerConfig().getOpenApiFilePath()) .onSuccess(routerBuilder -> @@ -80,6 +92,8 @@ public void start() { } else { log.error("Can not create Router"); } + stop(); + Quarkus.asyncExit(-1); }); @@ -99,8 +113,62 @@ Router createRouterWithEndpoints(RouterBuilder routerBuilder, List + ResponseUtils.errorResponse(routingContext, routingContext.failure())); + apiRouter.errorHandler(404, routingContext -> + ResponseUtils.errorResponse(routingContext, routingContext.failure())); + + var serverConfig = appConfigs.getServerConfig(); + addHealthCheckHandles(apiRouter, serverConfig); + + apiRouter.route( + String.format("%s*", serverConfig.getBasePath()) // absolut path not allowed only /* + ).subRouter(router); + return apiRouter; + } + + /** + * Adds readiness and liveness handlers + * + * @param router Created router object + */ + private void addHealthCheckHandles(Router router, ServerConfig serverConfig) { + addReadinessHandles(router, serverConfig.getReadinessPath()); + addLivenessHandles(router, serverConfig.getLivenessPath()); + } + + private void addReadinessHandles(Router router, String readinessPath) { + log.info("Adding readiness path: {}", readinessPath); + final HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); + + healthCheckHandler.register("database-communication-is-ready", + promise -> + db.getDbClient().getConnection(connection -> { + if (connection.failed()) { + log.error(connection.cause().getMessage()); + promise.tryComplete(Status.KO()); + } else { + connection.result().close(); + promise.tryComplete(Status.OK()); + } + }) + ); + + router.get(readinessPath).handler(healthCheckHandler); + } + + + private void addLivenessHandles(Router router, String livenessPath) { + log.info("Adding liveness path: {}", livenessPath); + final HealthCheckHandler healthCheckHandler = HealthCheckHandler.create(vertx); + + healthCheckHandler.register("liveness", + promise -> promise.tryComplete(Status.OK()) + ); - return routerBuilder.createRouter(); + router.get(livenessPath).handler(healthCheckHandler); } /** diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java index b12e3ddf..89f3936f 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java @@ -24,7 +24,10 @@ public class DeviceConfigsConstants { // Open api operationIds public final static String POST_MODIFY_DEVICE_CONFIG_OP_ID = "modifyCloudToDeviceConfig"; public final static String LIST_CONFIG_VERSIONS_OP_ID = "listConfigVersions"; + + //Device Config Repository public final static String TENANT_PATH_PARAMETER = "tenantid"; public final static String DEVICE_PATH_PARAMETER = "deviceid"; public final static String NUM_VERSION_QUERY_PARAMS = "numVersions"; + public final static String CREATE_SQL_SCRIPT_PATH = "db/create_device_config_table.sql"; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java new file mode 100644 index 00000000..27a253be --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java @@ -0,0 +1,84 @@ +package org.eclipse.hono.communication.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Command json object structure + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceCommandRequest { + + private String binaryData; + private String subfolder; + + public DeviceCommandRequest() { + + } + + public DeviceCommandRequest(String binaryData, String subfolder) { + this.binaryData = binaryData; + this.subfolder = subfolder; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @JsonProperty("subfolder") + public String getSubfolder() { + return subfolder; + } + + public void setSubfolder(String subfolder) { + this.subfolder = subfolder; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceCommandRequest deviceCommandRequest = (DeviceCommandRequest) o; + return Objects.equals(binaryData, deviceCommandRequest.binaryData) && + Objects.equals(subfolder, deviceCommandRequest.subfolder); + } + + @Override + public int hashCode() { + return Objects.hash(binaryData, subfolder); + } + + @Override + public String toString() { + + return "class DeviceCommandRequest {\n" + + " binaryData: " + toIndentedString(binaryData) + "\n" + + " subfolder: " + toIndentedString(subfolder) + "\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java new file mode 100644 index 00000000..509f920a --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java @@ -0,0 +1,128 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * The device configuration. + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceConfig { + + private String version; + private String cloudUpdateTime; + private String deviceAckTime; + private String binaryData; + + public DeviceConfig() { + + } + + public DeviceConfig(String version, String cloudUpdateTime, String deviceAckTime, String binaryData) { + this.version = version; + this.cloudUpdateTime = cloudUpdateTime; + this.deviceAckTime = deviceAckTime; + this.binaryData = binaryData; + } + + + @JsonProperty("version") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + + @JsonProperty("cloudUpdateTime") + public String getCloudUpdateTime() { + return cloudUpdateTime; + } + + public void setCloudUpdateTime(String cloudUpdateTime) { + this.cloudUpdateTime = cloudUpdateTime; + } + + + @JsonProperty("deviceAckTime") + public String getDeviceAckTime() { + return deviceAckTime; + } + + public void setDeviceAckTime(String deviceAckTime) { + this.deviceAckTime = deviceAckTime; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceConfig deviceConfig = (DeviceConfig) o; + return Objects.equals(version, deviceConfig.version) && + Objects.equals(cloudUpdateTime, deviceConfig.cloudUpdateTime) && + Objects.equals(deviceAckTime, deviceConfig.deviceAckTime) && + Objects.equals(binaryData, deviceConfig.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(version, cloudUpdateTime, deviceAckTime, binaryData); + } + + @Override + public String toString() { + + return "class DeviceConfig {\n" + + " version: " + toIndentedString(version) + "\n" + + " cloudUpdateTime: " + toIndentedString(cloudUpdateTime) + "\n" + + " deviceAckTime: " + toIndentedString(deviceAckTime) + "\n" + + " binaryData: " + toIndentedString(binaryData) + "\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java new file mode 100644 index 00000000..69a3d87b --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java @@ -0,0 +1,116 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; + +import java.util.Objects; + +/** + * The device configuration entity object. + **/ +public class DeviceConfigEntity { + + + private int version; + private String tenantId; + private String deviceId; + + private String cloudUpdateTime; + private String deviceAckTime; + private String binaryData; + + + public DeviceConfigEntity() { + } + + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + + public String getCloudUpdateTime() { + return cloudUpdateTime; + } + + public void setCloudUpdateTime(String cloudUpdateTime) { + this.cloudUpdateTime = cloudUpdateTime; + } + + public String getDeviceAckTime() { + return deviceAckTime; + } + + public void setDeviceAckTime(String deviceAckTime) { + this.deviceAckTime = deviceAckTime; + } + + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public String toString() { + return "DeviceConfigEntity{" + + "version=" + version + + ", tenantId='" + tenantId + '\'' + + ", deviceId='" + deviceId + '\'' + + ", cloudUpdateTime='" + cloudUpdateTime + '\'' + + ", deviceAckTime='" + deviceAckTime + '\'' + + ", binaryData='" + binaryData + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceConfigEntity that = (DeviceConfigEntity) o; + return version == that.version && tenantId.equals(that.tenantId) && deviceId.equals(that.deviceId) && cloudUpdateTime.equals(that.cloudUpdateTime) && Objects.equals(deviceAckTime, that.deviceAckTime) && binaryData.equals(that.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(version, tenantId, deviceId, cloudUpdateTime, deviceAckTime, binaryData); + } + + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java new file mode 100644 index 00000000..7520dd34 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java @@ -0,0 +1,84 @@ +package org.eclipse.hono.communication.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +/** + * Request body for modifying device configs + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class DeviceConfigRequest { + + private String versionToUpdate; + private String binaryData; + + public DeviceConfigRequest() { + + } + + public DeviceConfigRequest(String versionToUpdate, String binaryData) { + this.versionToUpdate = versionToUpdate; + this.binaryData = binaryData; + } + + + @JsonProperty("versionToUpdate") + public String getVersionToUpdate() { + return versionToUpdate; + } + + public void setVersionToUpdate(String versionToUpdate) { + this.versionToUpdate = versionToUpdate; + } + + + @JsonProperty("binaryData") + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeviceConfigRequest deviceConfigRequest = (DeviceConfigRequest) o; + return Objects.equals(versionToUpdate, deviceConfigRequest.versionToUpdate) && + Objects.equals(binaryData, deviceConfigRequest.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(versionToUpdate, binaryData); + } + + @Override + public String toString() { + + return "class DeviceConfigRequest {\n" + + " versionToUpdate: " + toIndentedString(versionToUpdate) + "\n" + + " binaryData: " + toIndentedString(binaryData) + "\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java new file mode 100644 index 00000000..d17b5902 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java @@ -0,0 +1,84 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; + +import java.util.Objects; + +/** + * The device configuration entity object. + **/ +public class DeviceConfigResponse { + + + private int version; + private String cloudUpdateTime; + private String binaryData; + + + public DeviceConfigResponse() { + } + + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getCloudUpdateTime() { + return cloudUpdateTime; + } + + public void setCloudUpdateTime(String cloudUpdateTime) { + this.cloudUpdateTime = cloudUpdateTime; + } + + public String getBinaryData() { + return binaryData; + } + + public void setBinaryData(String binaryData) { + this.binaryData = binaryData; + } + + + @Override + public String toString() { + return "DeviceConfigEntity{" + + "version=" + version + + ", cloudUpdateTime='" + cloudUpdateTime + '\'' + + ", binaryData='" + binaryData + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DeviceConfigResponse that = (DeviceConfigResponse) o; + return version == that.version && cloudUpdateTime.equals(that.cloudUpdateTime) && binaryData.equals(that.binaryData); + } + + @Override + public int hashCode() { + return Objects.hash(version, cloudUpdateTime, binaryData); + } + + +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java new file mode 100644 index 00000000..2d4f5570 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java @@ -0,0 +1,72 @@ +package org.eclipse.hono.communication.api.data; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A list of a device config versions + **/ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ListDeviceConfigVersionsResponse { + + private List deviceConfigs = new ArrayList<>(); + + public ListDeviceConfigVersionsResponse() { + + } + + public ListDeviceConfigVersionsResponse(List deviceConfigs) { + this.deviceConfigs = deviceConfigs; + } + + + @JsonProperty("deviceConfigs") + public List getDeviceConfigs() { + return deviceConfigs; + } + + public void setDeviceConfigs(List deviceConfigs) { + this.deviceConfigs = deviceConfigs; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = (ListDeviceConfigVersionsResponse) o; + return Objects.equals(deviceConfigs, listDeviceConfigVersionsResponse.deviceConfigs); + } + + @Override + public int hashCode() { + return Objects.hash(deviceConfigs); + } + + @Override + public String toString() { + + return "class ListDeviceConfigVersionsResponse {\n" + + " deviceConfigs: " + toIndentedString(deviceConfigs) + "\n" + + "}"; + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java index b20b1021..c706f68a 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java @@ -16,14 +16,13 @@ package org.eclipse.hono.communication.api.handler; -import io.vertx.codegen.annotations.Nullable; import io.vertx.core.Future; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.openapi.RouterBuilder; import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; -import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; +import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.service.DeviceConfigService; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; import org.eclipse.hono.communication.core.utils.ResponseUtils; @@ -36,8 +35,6 @@ @ApplicationScoped public class DeviceConfigsHandler implements HttpEndpointHandler { - private final String QUERY_PARAMS_NUM_VERSION = "numVersions"; - private final DeviceConfigService configService; public DeviceConfigsHandler(DeviceConfigService configService) { @@ -53,7 +50,7 @@ public void addRoutes(RouterBuilder routerBuilder) { .handler(this::handleModifyCloudToDeviceConfig); } - public Future<@Nullable DeviceConfigEntity> handleModifyCloudToDeviceConfig(RoutingContext routingContext) { + public Future handleModifyCloudToDeviceConfig(RoutingContext routingContext) { var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); @@ -67,7 +64,9 @@ public void addRoutes(RouterBuilder routerBuilder) { } public Future handleListConfigVersions(RoutingContext routingContext) { - var limit = Integer.parseInt(routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS)); + var numVersions = routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS); + + var limit = numVersions == null ? 0 : Integer.parseInt(numVersions); var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java index cc009ad5..0a465125 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java @@ -16,9 +16,9 @@ package org.eclipse.hono.communication.api.mapper; -import org.eclipse.hono.communication.api.entity.DeviceConfig; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigEntity; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.NullValuePropertyMappingStrategy; @@ -29,14 +29,9 @@ nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface DeviceConfigMapper { - DeviceConfigEntity deviceConfigToEntity(DeviceConfig deviceConfig); + DeviceConfigResponse deviceConfigEntityToResponse(DeviceConfigEntity entity); - DeviceConfig entityToDeviceConfig(DeviceConfigEntity entity); - - @Mapping(target = "version", source = "request.versionToUpdate") - @Mapping(target = "cloudUpdateTime", expression = "java(getDateTime())") - DeviceConfig configRequestToDeviceConfig(DeviceConfigRequest request); @Mapping(target = "version", source = "request.versionToUpdate") @Mapping(target = "cloudUpdateTime", expression = "java(getDateTime())") diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java index 34c0bec0..f1e7effa 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java @@ -18,8 +18,8 @@ import io.vertx.core.Future; import io.vertx.sqlclient.SqlConnection; -import org.eclipse.hono.communication.api.entity.DeviceConfig; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.data.DeviceConfig; +import org.eclipse.hono.communication.api.data.DeviceConfigEntity; import java.util.List; @@ -30,5 +30,5 @@ public interface DeviceConfigsRepository { Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit); - Future createOrUpdate(SqlConnection sqlConnection, DeviceConfigEntity entity); + Future createNew(SqlConnection sqlConnection, DeviceConfigEntity entity); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java index 777be10d..24d9e960 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java @@ -23,9 +23,10 @@ import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.templates.RowMapper; import io.vertx.sqlclient.templates.SqlTemplate; -import org.eclipse.hono.communication.api.entity.DeviceConfig; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; +import org.eclipse.hono.communication.api.data.DeviceConfig; +import org.eclipse.hono.communication.api.data.DeviceConfigEntity; import org.eclipse.hono.communication.core.app.DatabaseConfig; +import org.graalvm.collections.Pair; import javax.enterprise.context.ApplicationScoped; import java.util.ArrayList; @@ -39,18 +40,19 @@ public class DeviceConfigsRepositoryImpl implements DeviceConfigsRepository { private final static String SQL_INSERT = "INSERT INTO \"deviceConfig\" (version, \"tenantId\", \"deviceId\", \"cloudUpdateTime\", \"deviceAckTime\", \"binaryData\") " + "VALUES (#{version}, #{tenantId}, #{deviceId}, #{cloudUpdateTime}, #{deviceAckTime}, #{binaryData}) RETURNING version"; - private final static String SQL_UPDATE = "UPDATE \"deviceConfig\" SET \"cloudUpdateTime\" = #{cloudUpdateTime}, \"deviceAckTime\" = #{deviceAckTime}, " + - "\"binaryData\" = #{binaryData} WHERE version = #{version} and \"tenantId\" = #{tenantId} and \"deviceId\" = #{deviceId}"; private final static String SQL_LIST = "SELECT version, \"cloudUpdateTime\", COALESCE(\"deviceAckTime\",'') AS \"deviceAckTime\", \"binaryData\" " + "FROM \"deviceConfig\" WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId} ORDER BY version DESC LIMIT #{limit}"; - private final static String SQL_FIND_MAX_VERSION = "SELECT COALESCE(max(version), 0) AS version from \"deviceConfig\" " + + private final static String SQL_DELETE_MIN_VERSION = "DELETE FROM \"deviceConfig\" WHERE\"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId} " + + "and version = (SELECT MIN(version) from \"deviceConfig\" WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId}) RETURNING version"; + private final static String SQL_FIND_TOTAL_AND_MAX_VERSION = "SELECT COALESCE(COUNT(*), 0) as total, COALESCE(MAX(version), 0) as max_version from \"deviceConfig\" " + "WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId}"; - private final Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); - private final DatabaseConfig databaseConfig; - private String SQL_COUNT_DEVICES_WITH_PK_FILTER = "SELECT COUNT(*) as total FROM %s where %s = #{tenantId} and %s = #{deviceId}"; + + private final static int MAX_LIMIT = 10; + private final static Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); + private String SQL_COUNT_DEVICES_WITH_PK_FILTER = "SELECT COUNT(*) as total FROM public.%s where %s = #{tenantId} and %s = #{deviceId}"; + public DeviceConfigsRepositoryImpl(DatabaseConfig databaseConfig) { - this.databaseConfig = databaseConfig; SQL_COUNT_DEVICES_WITH_PK_FILTER = String.format(SQL_COUNT_DEVICES_WITH_PK_FILTER, databaseConfig.getDeviceRegistrationTableName(), @@ -59,7 +61,7 @@ public DeviceConfigsRepositoryImpl(DatabaseConfig databaseConfig) { } - private Future countDevices(SqlConnection sqlConnection, String deviceId, String tenantId) { + private Future searchForDevice(SqlConnection sqlConnection, String deviceId, String tenantId) { final RowMapper ROW_MAPPER = row -> row.getInteger("total"); return SqlTemplate .forQuery(sqlConnection, SQL_COUNT_DEVICES_WITH_PK_FILTER) @@ -71,6 +73,19 @@ private Future countDevices(SqlConnection sqlConnection, String deviceI } + private Future> findMaxVersionAndTotalEntries(SqlConnection sqlConnection, String deviceId, String tenantId) { + final RowMapper> ROW_MAPPER = row -> + Pair.create(row.getInteger("total"), row.getInteger("max_version")); + return SqlTemplate + .forQuery(sqlConnection, SQL_FIND_TOTAL_AND_MAX_VERSION) + .mapTo(ROW_MAPPER) + .execute(Map.of("deviceId", deviceId, "tenantId", tenantId)).map(rowSet -> { + final RowIterator> iterator = rowSet.iterator(); + return iterator.next(); + }); + + } + /** * Lists all config versions for a specific device. Result is order by version desc * @@ -81,10 +96,12 @@ private Future countDevices(SqlConnection sqlConnection, String deviceI * @return A Future with a List of DeviceConfigs */ public Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit) { + int queryLimit = limit == 0 ? MAX_LIMIT : limit; + return SqlTemplate .forQuery(sqlConnection, SQL_LIST) .mapTo(DeviceConfig.class) - .execute(Map.of("limit", limit, "deviceId", deviceId, "tenantId", tenantId)) + .execute(Map.of("deviceId", deviceId, "tenantId", tenantId, "limit", queryLimit)) .map(rowSet -> { final List configs = new ArrayList<>(); rowSet.forEach(configs::add); @@ -96,107 +113,90 @@ public Future> listAll(SqlConnection sqlConnection, String de .onFailure(throwable -> log.error("Error: {}", throwable)); } + /** - * Updates an entity for a given version + * Inserts a new entity in to the db. * * @param sqlConnection The sql connection instance * @param entity The instance to insert * @return A Future of the created DeviceConfigEntity */ - private Future update(SqlConnection sqlConnection, DeviceConfigEntity entity) { - + private Future insert(SqlConnection sqlConnection, DeviceConfigEntity entity) { return SqlTemplate - .forQuery(sqlConnection, SQL_UPDATE) + .forUpdate(sqlConnection, SQL_INSERT) .mapFrom(DeviceConfigEntity.class) + .mapTo(DeviceConfigEntity.class) .execute(entity) .map(rowSet -> { - if (rowSet.rowCount() > 0) { + final RowIterator iterator = rowSet.iterator(); + if (iterator.hasNext()) { + entity.setVersion(iterator.next().getVersion()); return entity; } else { - throw new IllegalStateException(String.format("Device config version doesn't exist: %s", entity)); + throw new IllegalStateException(String.format("Can't create device config: %s", entity)); } }) - .onSuccess(success -> log.info(String.format("Device config updated successfully: %s", success.toString()))) - .onFailure(throwable -> log.error("Error: {}", throwable)); + .onSuccess(success -> log.info(String.format("Device config created successfully: %s", success.toString()))) + .onFailure(throwable -> log.error(throwable.getMessage())); + } /** - * Increases the version number and creates a new entity. + * Delete the smallest config version * * @param sqlConnection The sql connection instance - * @param entity The instance to insert - * @return A Future of the created DeviceConfigEntity + * @param entity The device config for searching and deleting the smallest version + * @return A Future of the deleted version */ - private Future create(SqlConnection sqlConnection, DeviceConfigEntity entity) { - final RowMapper ROW_MAPPER = row -> row.getInteger("version"); + private Future deleteMinVersion(SqlConnection sqlConnection, DeviceConfigEntity entity) { + final RowMapper ROW_MAPPER = row -> row.getInteger("version"); return SqlTemplate - .forQuery(sqlConnection, SQL_FIND_MAX_VERSION) + .forQuery(sqlConnection, SQL_DELETE_MIN_VERSION) .mapFrom(DeviceConfigEntity.class) .mapTo(ROW_MAPPER) .execute(entity) .map(rowSet -> { final RowIterator iterator = rowSet.iterator(); - return iterator.hasNext() ? iterator.next() + 1 : 1; - }).compose(maxResults -> { - entity.setVersion(maxResults); - return insertEntity(sqlConnection, entity); - } - ) - .onSuccess(success -> log.info(String.format("Device configs created successfully: %s", success.toString()))) - .onFailure(throwable -> log.error("Error: {}", throwable)); - } - - /** - * Inserts an entity to Database - * - * @param sqlConnection The sql connection instance - * @param entity The instance to insert - * @return A Future of the created DeviceConfigEntity - */ - private Future insertEntity(SqlConnection sqlConnection, DeviceConfigEntity entity) { - return SqlTemplate - .forUpdate(sqlConnection, SQL_INSERT) - .mapFrom(DeviceConfigEntity.class) - .mapTo(DeviceConfigEntity.class) - .execute(entity) - .map(rowSet -> { - final RowIterator iterator = rowSet.iterator(); - if (iterator.hasNext()) { - entity.setVersion(iterator.next().getVersion()); - return entity; - } else { - throw new IllegalStateException(String.format("Can't create device config: %s", entity)); - } - }); + return iterator.next(); + }) + .onSuccess(deletedVersion -> log.info(String.format("Device config version %s was deleted", deletedVersion))); } /** - * Creates a new config if version is 0 else updates the current config + * Creates a new config version and deletes the oldest version if the total num of versions in DB is bigger than the MAX_LIMIT. * * @param sqlConnection The sql connection instance * @param entity The instance to insert * @return A Future of the created DeviceConfigEntity */ - public Future createOrUpdate(SqlConnection sqlConnection, DeviceConfigEntity entity) { - return countDevices(sqlConnection, entity.getDeviceId(), entity.getTenantId()) + public Future createNew(SqlConnection sqlConnection, DeviceConfigEntity entity) { + return searchForDevice(sqlConnection, entity.getDeviceId(), entity.getTenantId()) .compose( counter -> { if (counter < 1) { throw new IllegalStateException(String.format("Device with id %s and tenant id %s doesn't exist", entity.getDeviceId(), entity.getTenantId())); - } else { - - if (entity.getVersion() == 0) { - return create(sqlConnection, entity); - } else if (entity.getVersion() > 0) { - return update(sqlConnection, entity); - } else { - throw new IllegalStateException("Config version must be >= 0"); - } } - }); + return findMaxVersionAndTotalEntries(sqlConnection, entity.getDeviceId(), entity.getTenantId()) + .compose( + values -> { + int total = values.getLeft(); + int maxVersion = values.getRight(); + + entity.setVersion(maxVersion + 1); + + if (total > MAX_LIMIT - 1) { + return deleteMinVersion(sqlConnection, entity).compose( + ok -> insert(sqlConnection, entity) + + ); + } + return insert(sqlConnection, entity); + } + ); + }).onFailure(error -> log.error(error.getMessage())); } } \ No newline at end of file diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java new file mode 100644 index 00000000..f880beb7 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java @@ -0,0 +1,24 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +/** + * Interface for creating Database Tables at application startup + */ +public interface DatabaseSchemaCreator { + void createDBTables(); +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java new file mode 100644 index 00000000..60a93326 --- /dev/null +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java @@ -0,0 +1,76 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.quarkus.runtime.Quarkus; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.sqlclient.templates.SqlTemplate; +import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.enterprise.context.ApplicationScoped; +import java.util.Map; + +/** + * Creates all Database tables if they are not exist + */ + +@ApplicationScoped +public class DatabaseSchemaCreatorImpl implements DatabaseSchemaCreator { + private static final Logger log = LoggerFactory.getLogger(DatabaseSchemaCreatorImpl.class); + private final Vertx vertx; + private final String tableCreationErrorMsg = "Table deviceConfig can not be created {}"; + private final String tableCreationSuccessMsg = "Successfully migrate Table: deviceConfig."; + private final DatabaseService db; + + + public DatabaseSchemaCreatorImpl(Vertx vertx, DatabaseService db) { + this.vertx = vertx; + this.db = db; + } + + public void createDBTables() { + createDeviceConfigTable(); + } + + + private void createDeviceConfigTable() { + log.info("Running database migration from file {}", DeviceConfigsConstants.CREATE_SQL_SCRIPT_PATH); + + final Promise loadScriptTracker = Promise.promise(); + vertx.fileSystem().readFile(DeviceConfigsConstants.CREATE_SQL_SCRIPT_PATH, loadScriptTracker); + db.getDbClient().withTransaction( + sqlConnection -> + loadScriptTracker.future() + .map(Buffer::toString) + .compose(script -> SqlTemplate + .forQuery(sqlConnection, script) + .execute(Map.of()))) + .onSuccess(ok -> log.info(tableCreationSuccessMsg)) + .onFailure(error -> + { + log.error(tableCreationErrorMsg, error.getMessage()); + db.close(); + Quarkus.asyncExit(-1); + }); + + + } +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java index a899e8c5..5bda32cb 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java @@ -20,7 +20,7 @@ import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.pgclient.PgPool; -import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.DatabaseConfig; import org.eclipse.hono.communication.core.utils.DbUtils; import javax.enterprise.context.ApplicationScoped; @@ -32,16 +32,14 @@ @ApplicationScoped public class DatabaseServiceImpl implements DatabaseService { + private final static String databaseConnectionOpenMsg = "Database connection is open."; + private final static String databaseConnectionCloseMsg = "Database connection is closed."; private final Logger log = LoggerFactory.getLogger(DatabaseServiceImpl.class); - private final ApplicationConfig appConfigs; - private final Vertx vertx; private final PgPool dbClient; - public DatabaseServiceImpl(ApplicationConfig appConfigs, Vertx vertx) { - this.appConfigs = appConfigs; - this.vertx = vertx; - this.dbClient = DbUtils.createDbClient(vertx, appConfigs); - log.debug("Database connection is open."); + public DatabaseServiceImpl(DatabaseConfig databaseConfigs, Vertx vertx) { + this.dbClient = DbUtils.createDbClient(vertx, databaseConfigs); + log.debug(databaseConnectionOpenMsg); } /** @@ -59,7 +57,7 @@ public PgPool getDbClient() { public void close() { if (this.dbClient != null) { this.dbClient.close(); - log.debug("Database connection is closed."); + log.info(databaseConnectionCloseMsg); } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java index e9072881..e1870f84 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java @@ -16,18 +16,17 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.codegen.annotations.Nullable; import io.vertx.core.Future; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; -import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; +import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; /** * Device config interface */ public interface DeviceConfigService { - Future<@Nullable DeviceConfigEntity> modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId); + Future modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId); Future listAll(String deviceId, String tenantId, int limit); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java index 9beeac2e..2374c51f 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java @@ -17,15 +17,12 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.codegen.annotations.Nullable; import io.vertx.core.Future; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; -import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; +import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.enterprise.context.ApplicationScoped; @@ -36,7 +33,6 @@ @ApplicationScoped public class DeviceConfigServiceImpl implements DeviceConfigService { - private final Logger log = LoggerFactory.getLogger(DeviceConfigServiceImpl.class); private final DeviceConfigsRepository repository; private final DatabaseService db; @@ -51,34 +47,29 @@ public DeviceConfigServiceImpl(DeviceConfigsRepository repository, this.mapper = mapper; } - public Future<@Nullable DeviceConfigEntity> modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId) { + public Future modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId) { var entity = mapper.configRequestToDeviceConfigEntity(deviceConfig); entity.setDeviceId(deviceId); entity.setTenantId(tenantId); return db.getDbClient().withTransaction( - sqlConnection -> { - return repository.createOrUpdate(sqlConnection, entity); - }) - .onSuccess(success -> log.info(success.toString())) - .onFailure(error -> log.error(error.getMessage())); + sqlConnection -> + repository.createNew(sqlConnection, entity)) + .map(mapper::deviceConfigEntityToResponse); } public Future listAll(String deviceId, String tenantId, int limit) { return db.getDbClient().withTransaction( - sqlConnection -> { - return repository.listAll(sqlConnection, deviceId, tenantId, limit) - .map( - result -> { - var listConfig = new ListDeviceConfigVersionsResponse(); - listConfig.setDeviceConfigs(result); - return listConfig; - } - ); - } - ) - .onSuccess(success -> log.info(success.getDeviceConfigs().toString())) - .onFailure(error -> log.error(error.getMessage())); + sqlConnection -> repository.listAll(sqlConnection, deviceId, tenantId, limit) + .map( + result -> { + var listConfig = new ListDeviceConfigVersionsResponse(); + listConfig.setDeviceConfigs(result); + return listConfig; + } + ) + ); + } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java index 97e7756a..ff9c7208 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java @@ -27,23 +27,23 @@ public class DatabaseConfig { // Datasource properties - @ConfigProperty(name = "quarkus.datasource.port") + @ConfigProperty(name = "vertx.database.port") int port; - @ConfigProperty(name = "quarkus.datasource.host") + @ConfigProperty(name = "vertx.database.host") String host; - @ConfigProperty(name = "quarkus.datasource.username") + @ConfigProperty(name = "vertx.database.username") String userName; - @ConfigProperty(name = "quarkus.datasource.password") + @ConfigProperty(name = "vertx.database.password") String password; - @ConfigProperty(name = "quarkus.datasource.name") + @ConfigProperty(name = "vertx.database.name") String name; - @ConfigProperty(name = "quarkus.datasource.pool-max-size") + @ConfigProperty(name = "vertx.database.pool-max-size") int poolMaxSize; - @ConfigProperty(name = "quarkus.device-registration.table") + @ConfigProperty(name = "vertx.device-registration.table") String deviceRegistrationTableName; - @ConfigProperty(name = "quarkus.device-registration.tenant-id-column") + @ConfigProperty(name = "vertx.device-registration.tenant-id-column") String deviceRegistrationTenantIdColumn; - @ConfigProperty(name = "quarkus.device-registration.device-id-column") + @ConfigProperty(name = "vertx.device-registration.device-id-column") String deviceRegistrationDeviceIdColumn; public String getDeviceRegistrationTableName() { diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java index 54903a13..a7fc4ef2 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java @@ -34,6 +34,28 @@ public class ServerConfig { @ConfigProperty(name = "vertx.server.port", defaultValue = "8080") int serverPort; + @ConfigProperty(name = "vertx.server.paths.readiness", defaultValue = "/readiness") + String readinessPath; + + + @ConfigProperty(name = "vertx.server.paths.liveness", defaultValue = "/liveness") + String livenessPath; + + @ConfigProperty(name = "vertx.server.paths.base") + String basePath; + + public String getBasePath() { + return basePath; + } + + public String getReadinessPath() { + return readinessPath; + } + + public String getLivenessPath() { + return livenessPath; + } + public String getOpenApiFilePath() { return openApiFilePath; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java index c3eb514f..072b4dbc 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java @@ -17,24 +17,31 @@ package org.eclipse.hono.communication.core.utils; import io.vertx.core.Vertx; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.pgclient.PgConnectOptions; import io.vertx.pgclient.PgPool; import io.vertx.sqlclient.PoolOptions; -import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.DatabaseConfig; /** * Database utilities class */ public class DbUtils { + final static Logger log = LoggerFactory.getLogger(DbUtils.class); + final static String connectionFailedMsg = "Failed to connect to Database: %s"; + final static String connectionSuccessMsg = "Database connection created successfully."; + /** * Build DB client that is used to manage a pool of connections * * @param vertx Vertx context * @return PostgreSQL pool */ - public static PgPool createDbClient(Vertx vertx, ApplicationConfig appConfigs) { - var dbConfigs = appConfigs.getDatabaseConfig(); + public static PgPool createDbClient(Vertx vertx, DatabaseConfig dbConfigs) { + + final PgConnectOptions connectOptions = new PgConnectOptions() .setHost(dbConfigs.getHost()) .setPort(dbConfigs.getPort()) @@ -43,8 +50,18 @@ public static PgPool createDbClient(Vertx vertx, ApplicationConfig appConfigs) { .setPassword(dbConfigs.getPassword()); final PoolOptions poolOptions = new PoolOptions().setMaxSize(dbConfigs.getPoolMaxSize()); + var pool = PgPool.pool(vertx, connectOptions, poolOptions); + pool.getConnection(connection -> { + if (connection.failed()) { + log.error(String.format(connectionFailedMsg, connection.cause().getMessage())); + System.exit(-1); + + } else { + log.info(connectionSuccessMsg); + } + }); + return pool; - return PgPool.pool(vertx, connectOptions, poolOptions); } } diff --git a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml index 5b94daa4..1c44c3ee 100644 --- a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml +++ b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml @@ -4,6 +4,9 @@ info: title: Hono Device Communication version: 1.0.0 description: Device commands and configs API. +servers: + - url: http://localhost:8080/api/v1 + paths: /commands/{tenantid}/{deviceid}: summary: Device commands @@ -40,19 +43,21 @@ paths: required: true /configs/{tenantid}/{deviceid}: summary: Device configs - description: Configs for a specific device + description: Create or list Configs for a specific device get: tags: - CONFIGS parameters: - name: numVersions description: "The number of versions to list. Versions are listed in decreasing - order of the version number. The maximum number of versions retained is + order of the version number. The maximum number of versions saved in Database is 10. If this value is zero, it will return all the versions available." schema: type: integer + minimum: 0 + maximum: 10 in: query - required: true + required: false responses: "200": content: @@ -84,16 +89,18 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeviceConfig' + $ref: '#/components/schemas/DeviceConfigResponse' description: Device config updated successfully "400": - description: Validation error + description: Validation error or Bad request + "404": + description: Not Found "500": - description: Internal error + description: Internal Server error operationId: modifyCloudToDeviceConfig summary: Modify cloud to device config - description: Modifies the configuration for a specific device and Returns the - modified configuration version and its metadata. + description: Creates an device config version and Returns the + the new configuration version and its metadata. parameters: - name: tenantid description: Unique registry id @@ -134,17 +141,11 @@ components: DeviceConfigRequest: description: Request body for modifying device configs required: - - versionToUpdate - binaryData type: object properties: versionToUpdate: - description: "string (int64 format)\r\n\r\nThe version number to update. - If this value is zero, it will not check the version number of the server - and will always update the current version; otherwise, this update will - fail if the version number found on the server does not match this version - number. This is used to support multiple simultaneous updates without - losing data." + description: "string (int64 format)\r\n\r\nThe Config version number." type: string binaryData: description: "string (bytes format)\r\n\r\nThe configuration data for the @@ -204,3 +205,30 @@ components: cloudUpdateTime: string deviceAckTime: string binaryData: string + DeviceConfigResponse: + title: New Config version response + description: The device configuration. + type: object + properties: + version: + description: "String (int64 format) [Output only] The version + of this update. The version number is assigned by the server, and is + always greater than 0 after device creation. The version must be 0 on + the devices.create request if a config is specified; the response of + devices.create will always have a value of 1." + type: string + cloudUpdateTime: + description: "String (Timestamp format) [Output only] The time at + which this configuration version was updated in Cloud IoT Core. This + timestamp is set by the server. Timestamp in + RFC3339 UTC \"Zulu\" format, accurate to nanoseconds. + Example: \"2014-10-02T15:01:23.045123456Z\"." + type: string + binaryData: + description: "string (bytes format) The device configuration data + in string base64-encoded format." + type: string + example: + version: string + cloudUpdateTime: string + binaryData: string diff --git a/device-communication/src/main/resources/application.yaml b/device-communication/src/main/resources/application.yaml index f1618822..fea04fc8 100644 --- a/device-communication/src/main/resources/application.yaml +++ b/device-communication/src/main/resources/application.yaml @@ -1,37 +1,36 @@ app: name: "Device Communication" - version: "v1" + version: ${COM_APP_VERSION:"v1"} vertx: openapi: - file: "/api/hono-device-communication-v1.yaml" + file: ${COM_OPENAPI_FILE_PATH:/api/hono-device-communication-v1.yaml} + server: - url: "localhost" - port: 8081 + url: ${COM_SERVER_HOST:0.0.0.0} + port: ${COM_SERVER_PORT:8080} + paths: + base: ${COM_SERVER_BASE_PATH:/api/v1/} # base path should always end with "/" + liveness: ${COM_SERVER_LIVENESS_PATH:/alive} + readiness: ${COM_SERVER_READINESS_PATH:/ready} -quarkus: - flyway: - migrate-at-start: true - # Device registration table configs. Used for validating devices - device-registration: - table: "device_registration" - tenant-id-column: "tenant_id" - device-id-column: "device_id" - datasource: - pool-max-size: 5 - name: "hono" - host: "localhost" - port: 5432 - username: "postgres" - password: "mysecretpassword" + database: + pool-max-size: ${COM_POOL_MAX_SIZE:5} + name: ${COM_DB_NAME:hono} + host: ${COM_DB_HOST:localhost} + port: ${COM_DB_PORT:5432} + username: ${COM_DB_USERNAME:postgres} + password: ${COM_DB_PASSWORD:mysecretpassword} db-kind: "postgresql" - jdbc: - url: "jdbc:postgresql://localhost:5432/hono" + # Device registration table configs. Used for validating devices + device-registration: + table: ${COM_DB_DEVICE_REG_TABLE:device_registrations} + tenant-id-column: ${COM_DB_DEVICE_REG_TENANT_COL_NAME:tenant_id} + device-id-column: ${COM_DB_DEVICE_REG_DEVICE_COL_NAME:device_id} -"%dev": - app: - name: "Device Communication" - vertx: - server: - url: "localhost" - port: "8081" \ No newline at end of file +quarkus: + container-image: + builder: docker + build: true + push: true + image: "gcr.io/sotec-iot-core-dev/hono-device-communication" diff --git a/device-communication/src/main/resources/db/create_device_config_table.sql b/device-communication/src/main/resources/db/create_device_config_table.sql new file mode 100644 index 00000000..1f11816d --- /dev/null +++ b/device-communication/src/main/resources/db/create_device_config_table.sql @@ -0,0 +1,28 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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 + * ********************************************************** + * + */ + + +CREATE TABLE IF NOT EXISTS "deviceConfig" +( + version INT not null, + "tenantId" VARCHAR(100) not null, + "deviceId" VARCHAR(100) not null, + "cloudUpdateTime" VARCHAR(100) not null, + "deviceAckTime" VARCHAR(100), + "binaryData" VARCHAR not null, + + PRIMARY KEY (version, "tenantId", "deviceId") +) \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java index d30d376b..a5a61fa5 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java @@ -23,14 +23,13 @@ import io.vertx.core.Vertx; import io.vertx.core.http.*; import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.openapi.RouterBuilder; import io.vertx.ext.web.validation.BadRequestException; import org.eclipse.hono.communication.api.handler.DeviceCommandsHandler; -import org.eclipse.hono.communication.api.service.DatabaseService; -import org.eclipse.hono.communication.api.service.DatabaseServiceImpl; -import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; +import org.eclipse.hono.communication.api.service.*; import org.eclipse.hono.communication.core.app.ApplicationConfig; import org.eclipse.hono.communication.core.app.ServerConfig; import org.junit.jupiter.api.AfterEach; @@ -46,7 +45,6 @@ class DeviceCommunicationHttpServerTest { - private ApplicationConfig appConfigsMock; private VertxHttpHandlerManagerService handlerServiceMock; private Vertx vertxMock; @@ -58,6 +56,8 @@ class DeviceCommunicationHttpServerTest { private HttpServerResponse httpServerResponseMock; private HttpServerRequest httpServerRequestMock; private BadRequestException badRequestExceptionMock; + private DatabaseSchemaCreator databaseSchemaCreatorMock; + private Route routeMock; private JsonObject jsonObjMock; private DatabaseService dbMock; @@ -78,7 +78,13 @@ void setUp() { badRequestExceptionMock = mock(BadRequestException.class); jsonObjMock = mock(JsonObject.class); dbMock = mock(DatabaseServiceImpl.class); - deviceCommunicationHttpServer = new DeviceCommunicationHttpServer(appConfigsMock, vertxMock, handlerServiceMock, dbMock); + databaseSchemaCreatorMock = mock(DatabaseSchemaCreatorImpl.class); + routeMock = mock(Route.class); + deviceCommunicationHttpServer = new DeviceCommunicationHttpServer(appConfigsMock, + vertxMock, + handlerServiceMock, + dbMock, + databaseSchemaCreatorMock); } @@ -97,7 +103,9 @@ void tearDown() { badRequestExceptionMock, jsonObjMock, dbMock, - serverConfigMock); + serverConfigMock, + databaseSchemaCreatorMock, + routeMock); } @@ -107,40 +115,67 @@ void startSucceeded() { Mockito.verifyNoMoreInteractions(mockedCommandService); doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { - mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) - .thenReturn(Future.succeededFuture(routerBuilderMock)); - mockedRouterBuilderStatic.verifyNoMoreInteractions(); - when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); - when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of(mockedCommandService)); - when(routerBuilderMock.createRouter()).thenReturn(routerMock); - when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); - when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); - when(httpServerMock.requestHandler(routerMock)).thenReturn(httpServerMock); - when(httpServerMock.listen()).thenReturn(Future.succeededFuture(httpServerMock)); - when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); - - try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { - - this.deviceCommunicationHttpServer.start(); - - mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); - - verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); - verify(routerBuilderMock, times(1)).createRouter(); - - verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); - verify(httpServerMock, times(1)).requestHandler(routerMock); - verify(httpServerMock, times(1)).listen(); - verify(appConfigsMock, times(2)).getServerConfig(); - verify(serverConfigMock, times(2)).getServerUrl(); - verify(serverConfigMock, times(2)).getServerPort(); - verify(mockedCommandService, times(1)).addRoutes(routerBuilderMock); - verify(serverConfigMock, times(1)).getOpenApiFilePath(); - quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); - quarkusMockedStatic.verifyNoMoreInteractions(); + try (MockedStatic routerMockedStatic = mockStatic(Router.class)) { + + routerMockedStatic.when(() -> Router.router(any())).thenReturn(routerMock); + mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) + .thenReturn(Future.succeededFuture(routerBuilderMock)); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); + when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of(mockedCommandService)); + when(routerBuilderMock.createRouter()).thenReturn(routerMock); + when(serverConfigMock.getLivenessPath()).thenReturn("/live"); + when(serverConfigMock.getReadinessPath()).thenReturn("/ready"); + when(routerMock.get(anyString())).thenReturn(routeMock); + when(routeMock.handler(any())).thenReturn(routeMock); + when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); + when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); + when(httpServerMock.requestHandler(any())).thenReturn(httpServerMock); + when(httpServerMock.listen()).thenReturn(Future.succeededFuture(httpServerMock)); + when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); + when(serverConfigMock.getBasePath()).thenReturn("/basePath"); + when(routerMock.route(any())).thenReturn(routeMock); + + try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { + + this.deviceCommunicationHttpServer.start(); + + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); + routerMockedStatic.verify(() -> Router.router(vertxMock), times(1)); + + + verify(databaseSchemaCreatorMock, times(1)).createDBTables(); + verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); + verify(routerBuilderMock, times(1)).createRouter(); + + + verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); + verify(httpServerMock, times(1)).requestHandler(any()); + verify(httpServerMock, times(1)).listen(); + verify(appConfigsMock, times(3)).getServerConfig(); + verify(serverConfigMock, times(2)).getServerUrl(); + verify(serverConfigMock, times(2)).getServerPort(); + verify(serverConfigMock, times(1)).getLivenessPath(); + verify(serverConfigMock, times(1)).getReadinessPath(); + verify(serverConfigMock, times(1)).getBasePath(); + verify(mockedCommandService, times(1)).addRoutes(routerBuilderMock); + verify(serverConfigMock, times(1)).getOpenApiFilePath(); + verify(routerMock, times(1)).errorHandler(eq(400), any()); + verify(routerMock, times(1)).errorHandler(eq(404), any()); + verify(routerMock, times(2)).get(anyString()); + verify(routeMock, times(2)).handler(any()); + verify(routerMock, times(1)).route(anyString()); + verify(routeMock, times(1)).subRouter(any()); + quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); + routerMockedStatic.verifyNoMoreInteractions(); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + quarkusMockedStatic.verifyNoMoreInteractions(); + + } } + } @@ -161,9 +196,13 @@ void createRouterFailed() { try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { this.deviceCommunicationHttpServer.start(); + + verify(dbMock, times(1)).close(); + verify(databaseSchemaCreatorMock, times(1)).createDBTables(); verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); verify(appConfigsMock, times(1)).getServerConfig(); verify(serverConfigMock, times(1)).getOpenApiFilePath(); + quarkusMockedStatic.verify(() -> Quarkus.asyncExit(-1), times(1)); quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); quarkusMockedStatic.verifyNoMoreInteractions(); } @@ -178,35 +217,57 @@ void createServerFailed() { Mockito.verifyNoMoreInteractions(mockedCommandService); doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { - mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) - .thenReturn(Future.succeededFuture(routerBuilderMock)); - mockedRouterBuilderStatic.verifyNoMoreInteractions(); - when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); - when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of()); - when(routerBuilderMock.createRouter()).thenReturn(routerMock); - when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); - when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); - when(httpServerMock.requestHandler(routerMock)).thenReturn(httpServerMock); - when(httpServerMock.listen()).thenReturn(Future.failedFuture(new Throwable())); - when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); - try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { - - this.deviceCommunicationHttpServer.start(); - - mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); - - verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); - verify(routerBuilderMock, times(1)).createRouter(); - verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); - verify(httpServerMock, times(1)).requestHandler(routerMock); - verify(httpServerMock, times(1)).listen(); - verify(appConfigsMock, times(2)).getServerConfig(); - verify(serverConfigMock, times(1)).getOpenApiFilePath(); - verify(serverConfigMock, times(1)).getServerPort(); - verify(serverConfigMock, times(1)).getServerUrl(); - quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); - quarkusMockedStatic.verifyNoMoreInteractions(); + try (MockedStatic routerMockedStatic = mockStatic(Router.class)) { + mockedRouterBuilderStatic.when(() -> RouterBuilder.create(any(), any())) + .thenReturn(Future.succeededFuture(routerBuilderMock)); + + routerMockedStatic.when(() -> Router.router(any())).thenReturn(routerMock); + + when(appConfigsMock.getServerConfig()).thenReturn(serverConfigMock); + when(handlerServiceMock.getAvailableHandlerServices()).thenReturn(List.of()); + when(routerBuilderMock.createRouter()).thenReturn(routerMock); + when(routerMock.errorHandler(anyInt(), any())).thenReturn(routerMock); + when(serverConfigMock.getLivenessPath()).thenReturn("/live"); + when(serverConfigMock.getReadinessPath()).thenReturn("/ready"); + when(routerMock.get(anyString())).thenReturn(routeMock); + when(routeMock.handler(any())).thenReturn(routeMock); + when(vertxMock.createHttpServer(any(HttpServerOptions.class))).thenReturn(httpServerMock); + when(httpServerMock.requestHandler(routerMock)).thenReturn(httpServerMock); + when(httpServerMock.listen()).thenReturn(Future.failedFuture(new Throwable())); + when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); + when(serverConfigMock.getBasePath()).thenReturn("/basePath"); + when(routerMock.route(any())).thenReturn(routeMock); + + this.deviceCommunicationHttpServer.start(); + + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); + + verify(databaseSchemaCreatorMock, times(1)).createDBTables(); + verify(handlerServiceMock, times(1)).getAvailableHandlerServices(); + verify(routerBuilderMock, times(1)).createRouter(); + verify(vertxMock, times(1)).createHttpServer(any(HttpServerOptions.class)); + verify(httpServerMock, times(1)).requestHandler(routerMock); + verify(httpServerMock, times(1)).listen(); + verify(appConfigsMock, times(3)).getServerConfig(); + verify(serverConfigMock, times(1)).getOpenApiFilePath(); + verify(serverConfigMock, times(1)).getServerPort(); + verify(serverConfigMock, times(1)).getServerUrl(); + verify(serverConfigMock, times(1)).getLivenessPath(); + verify(serverConfigMock, times(1)).getReadinessPath(); + verify(serverConfigMock, times(1)).getBasePath(); + verify(routerMock, times(1)).errorHandler(eq(400), any()); + verify(routerMock, times(1)).errorHandler(eq(404), any()); + verify(routerMock, times(2)).get(anyString()); + verify(routeMock, times(2)).handler(any()); + verify(routeMock, times(1)).subRouter(any()); + verify(routerMock, times(1)).route(anyString()); + routerMockedStatic.verify(() -> Router.router(vertxMock), times(1)); + quarkusMockedStatic.verify(Quarkus::waitForExit, times(1)); + mockedRouterBuilderStatic.verifyNoMoreInteractions(); + routerMockedStatic.verifyNoMoreInteractions(); + quarkusMockedStatic.verifyNoMoreInteractions(); + } } } diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java index 94d2640f..ad563f8e 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java @@ -26,10 +26,10 @@ import io.vertx.ext.web.openapi.Operation; import io.vertx.ext.web.openapi.RouterBuilder; import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; -import org.eclipse.hono.communication.api.entity.DeviceConfig; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; -import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.data.DeviceConfig; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; +import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.service.DeviceConfigService; import org.eclipse.hono.communication.api.service.DeviceConfigServiceImpl; import org.junit.jupiter.api.AfterEach; @@ -59,7 +59,7 @@ class DeviceConfigsHandlerTest { private final String deviceID = "device_ID"; private final String errorMsg = "test_error"; DeviceConfigRequest deviceConfigRequest = new DeviceConfigRequest("1", "binary_data"); - DeviceConfigEntity deviceConfigEntity = new DeviceConfigEntity(); + DeviceConfigResponse deviceConfigEntity = new DeviceConfigResponse(); DeviceConfig deviceConfig = new DeviceConfig(); public DeviceConfigsHandlerTest() { @@ -74,8 +74,7 @@ public DeviceConfigsHandlerTest() { deviceConfigsHandler = new DeviceConfigsHandler(configServiceMock); deviceConfigEntity.setVersion(1); - deviceConfigEntity.setTenantId(tenantID); - deviceConfigEntity.setDeviceId(deviceID); + deviceConfig.setVersion(""); diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java new file mode 100644 index 00000000..8b818edc --- /dev/null +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java @@ -0,0 +1,91 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.service; + +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import io.vertx.core.file.FileSystem; +import io.vertx.pgclient.PgPool; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +class DatabaseSchemaCreatorImplTest { + + private final Vertx vertxMock; + private final DatabaseService dbMock; + private final FileSystem fileSystemMock; + private final PgPool pgPoolMock; + + private final DatabaseSchemaCreatorImpl databaseSchemaCreator; + + DatabaseSchemaCreatorImplTest() { + this.dbMock = mock(DatabaseService.class); + this.vertxMock = mock(Vertx.class); + this.fileSystemMock = mock(FileSystem.class); + this.pgPoolMock = mock(PgPool.class); + + this.databaseSchemaCreator = new DatabaseSchemaCreatorImpl(vertxMock, dbMock); + } + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + verifyNoMoreInteractions(vertxMock, dbMock, pgPoolMock, fileSystemMock); + } + + @Test + void createDBTables_success() { + when(vertxMock.fileSystem()).thenReturn(fileSystemMock); + when(fileSystemMock.readFile(anyString(), any())).thenReturn(fileSystemMock); + when(dbMock.getDbClient()).thenReturn(pgPoolMock); + when(pgPoolMock.withTransaction(any())).thenReturn(Future.succeededFuture()); + + databaseSchemaCreator.createDBTables(); + + verify(vertxMock).fileSystem(); + verify(fileSystemMock).readFile(anyString(), any()); + verify(dbMock).getDbClient(); + verify(pgPoolMock).withTransaction(any()); + + } + + + @Test + void createDBTables_failed() { + when(vertxMock.fileSystem()).thenReturn(fileSystemMock); + when(fileSystemMock.readFile(anyString(), any())).thenReturn(fileSystemMock); + when(dbMock.getDbClient()).thenReturn(pgPoolMock); + when(pgPoolMock.withTransaction(any())).thenReturn(Future.failedFuture(new Throwable())); + + databaseSchemaCreator.createDBTables(); + + verify(vertxMock).fileSystem(); + verify(fileSystemMock).readFile(anyString(), any()); + verify(dbMock).getDbClient(); + verify(dbMock).close(); + verify(pgPoolMock).withTransaction(any()); + + } +} \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java index 8fffb4a0..33025dbd 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java @@ -18,7 +18,7 @@ import io.vertx.core.Vertx; import io.vertx.pgclient.PgPool; -import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.DatabaseConfig; import org.eclipse.hono.communication.core.utils.DbUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -30,14 +30,14 @@ class DatabaseServiceImplTest { private final Vertx vertxMock; - private final ApplicationConfig applicationConfigMock; + private final DatabaseConfig databaseConfigMock; private final PgPool pgPoolMock; private DatabaseService databaseService; public DatabaseServiceImplTest() { vertxMock = mock(Vertx.class); - applicationConfigMock = mock(ApplicationConfig.class); + databaseConfigMock = mock(DatabaseConfig.class); pgPoolMock = mock(PgPool.class); @@ -45,7 +45,7 @@ public DatabaseServiceImplTest() { @AfterEach void tearDown() { - verifyNoMoreInteractions(vertxMock, applicationConfigMock, pgPoolMock); + verifyNoMoreInteractions(vertxMock, databaseConfigMock, pgPoolMock); } @@ -53,14 +53,14 @@ void tearDown() { @Test void getDbClient() { try (MockedStatic dbUtilsMockedStatic = mockStatic(DbUtils.class)) { - dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock)).thenReturn(pgPoolMock); - databaseService = new DatabaseServiceImpl(applicationConfigMock, vertxMock); + dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, databaseConfigMock)).thenReturn(pgPoolMock); + databaseService = new DatabaseServiceImpl(databaseConfigMock, vertxMock); var client = databaseService.getDbClient(); Assertions.assertSame(client, pgPoolMock); - dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock), times(1)); + dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, databaseConfigMock), times(1)); dbUtilsMockedStatic.verifyNoMoreInteractions(); } } @@ -69,12 +69,12 @@ void getDbClient() { void close() { try (MockedStatic dbUtilsMockedStatic = mockStatic(DbUtils.class)) { - dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock)).thenReturn(pgPoolMock); - databaseService = new DatabaseServiceImpl(applicationConfigMock, vertxMock); + dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, databaseConfigMock)).thenReturn(pgPoolMock); + databaseService = new DatabaseServiceImpl(databaseConfigMock, vertxMock); databaseService.close(); verify(pgPoolMock, times(1)).close(); - dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, applicationConfigMock), times(1)); + dbUtilsMockedStatic.verify(() -> DbUtils.createDbClient(vertxMock, databaseConfigMock), times(1)); dbUtilsMockedStatic.verifyNoMoreInteractions(); } } diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java index 8ecbf615..b248bb13 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java @@ -18,9 +18,10 @@ import io.vertx.core.Future; import io.vertx.pgclient.PgPool; -import org.eclipse.hono.communication.api.entity.DeviceConfigEntity; -import org.eclipse.hono.communication.api.entity.DeviceConfigRequest; -import org.eclipse.hono.communication.api.entity.ListDeviceConfigVersionsResponse; +import org.eclipse.hono.communication.api.data.DeviceConfigEntity; +import org.eclipse.hono.communication.api.data.DeviceConfigRequest; +import org.eclipse.hono.communication.api.data.DeviceConfigResponse; +import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; import org.eclipse.hono.communication.api.repository.DeviceConfigsRepositoryImpl; @@ -45,8 +46,8 @@ public DeviceConfigServiceImplTest() { this.repositoryMock = mock(DeviceConfigsRepositoryImpl.class); this.dbMock = mock(DatabaseServiceImpl.class); this.mapperMock = mock(DeviceConfigMapper.class); - poolMock = mock(PgPool.class); - deviceConfigService = new DeviceConfigServiceImpl(repositoryMock, dbMock, mapperMock); + this.poolMock = mock(PgPool.class); + this.deviceConfigService = new DeviceConfigServiceImpl(repositoryMock, dbMock, mapperMock); } @@ -59,7 +60,9 @@ void tearDown() { void modifyCloudToDeviceConfig_success() { var deviceConfigRequest = new DeviceConfigRequest(); var deviceConfigEntity = new DeviceConfigEntity(); + var deviceConfigEntityResponse = new DeviceConfigResponse(); when(mapperMock.configRequestToDeviceConfigEntity(deviceConfigRequest)).thenReturn(deviceConfigEntity); + when(mapperMock.deviceConfigEntityToResponse(deviceConfigEntity)).thenReturn(deviceConfigEntityResponse); when(dbMock.getDbClient()).thenReturn(poolMock); when(poolMock.withTransaction(any())).thenReturn(Future.succeededFuture(deviceConfigEntity)); @@ -67,6 +70,7 @@ void modifyCloudToDeviceConfig_success() { var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); verify(mapperMock, times(1)).configRequestToDeviceConfigEntity(deviceConfigRequest); + verify(mapperMock, times(1)).deviceConfigEntityToResponse(deviceConfigEntity); verify(dbMock, times(1)).getDbClient(); verify(poolMock, times(1)).withTransaction(any()); Assertions.assertTrue(results.succeeded()); From 363c4e2cd747c5d5998db24f49a3d832aea72381 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Wed, 1 Feb 2023 10:56:01 +0100 Subject: [PATCH 03/18] code refactoring and update readme --- device-communication/README.md | 10 +- device-communication/pom.xml | 20 -- .../api/DeviceCommunicationHttpServer.java | 19 +- .../api/config/DeviceConfigsConstants.java | 6 +- .../api/data/DeviceConfigResponse.java | 6 +- .../DeviceNotFoundException.java} | 22 +- .../api/handler/DeviceCommandsHandler.java | 1 - .../api/handler/DeviceConfigsHandler.java | 8 +- .../DeviceConfigsRepositoryImpl.java | 39 +-- .../api/service/DeviceCommandServiceImpl.java | 2 + .../api/service/DeviceConfigServiceImpl.java | 2 +- .../core/app/AbstractServiceApplication.java | 1 + .../core/utils/ResponseUtils.java | 14 +- .../api/hono-device-communication-v1.yaml | 12 +- .../src/main/resources/api/hono-endpoint.yaml | 233 ++++++++++++++++++ .../DeviceCommunicationHttpServerTest.java | 12 +- .../api/handler/DeviceConfigsHandlerTest.java | 32 +-- .../service/DeviceConfigServiceImplTest.java | 8 +- 18 files changed, 353 insertions(+), 94 deletions(-) rename device-communication/src/main/java/org/eclipse/hono/communication/api/{config/EndpointConstants.java => exception/DeviceNotFoundException.java} (53%) create mode 100644 device-communication/src/main/resources/api/hono-endpoint.yaml diff --git a/device-communication/README.md b/device-communication/README.md index f036816d..681e5895 100644 --- a/device-communication/README.md +++ b/device-communication/README.md @@ -11,7 +11,7 @@ The application is reactive and uses Quarkus Framework for the application and V ### Hono internal communication -API uses [Googles PubSub](https://cloud.google.com/pubsub/docs/overview?hl=de) service to communicate with the command +API uses [Google's PubSub](https://cloud.google.com/pubsub/docs/overview?hl=de) service to communicate with the command router. ## API endpoints @@ -22,11 +22,11 @@ router.

-#### configs/{tenantId}/{deviceId} +#### configs/{tenantId}/{deviceId}?numVersion=(int 0 - 10) - GET : list of device config versions -- POST: update or create a device config version +- POST: create a device config version For more information please see resources/api/openApi file. @@ -51,11 +51,11 @@ For running the PostgresSQL Database local with docker run: `````` -docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres +docker run -p 5432:5432 --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres `````` -After the container is running, log in to the container and with psql create the database and the tables. Then we have +After the container is running, log in to the container and with psql create the database. Then we have to set the application settings. Default postgresSQl values: diff --git a/device-communication/pom.xml b/device-communication/pom.xml index afa70b5e..470b55f5 100644 --- a/device-communication/pom.xml +++ b/device-communication/pom.xml @@ -166,26 +166,6 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index dd631f3f..16dbd644 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -121,11 +121,12 @@ Router createRouterWithEndpoints(RouterBuilder routerBuilder, List handleModifyCloudToDeviceConfig(RoutingContext routingContext) { - var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); - var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); final DeviceConfigRequest deviceConfig = routingContext.body() .asJsonObject() @@ -67,8 +67,8 @@ public Future handleListConfigVersions(Routing var numVersions = routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS); var limit = numVersions == null ? 0 : Integer.parseInt(numVersions); - var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); - var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); return configService.listAll(deviceId, tenantId, limit) .onSuccess(result -> ResponseUtils.successResponse(routingContext, result)) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java index 24d9e960..b3ec4e7c 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java @@ -25,6 +25,7 @@ import io.vertx.sqlclient.templates.SqlTemplate; import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigEntity; +import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; import org.eclipse.hono.communication.core.app.DatabaseConfig; import org.graalvm.collections.Pair; @@ -97,20 +98,28 @@ private Future> findMaxVersionAndTotalEntries(SqlConnecti */ public Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit) { int queryLimit = limit == 0 ? MAX_LIMIT : limit; - - return SqlTemplate - .forQuery(sqlConnection, SQL_LIST) - .mapTo(DeviceConfig.class) - .execute(Map.of("deviceId", deviceId, "tenantId", tenantId, "limit", queryLimit)) - .map(rowSet -> { - final List configs = new ArrayList<>(); - rowSet.forEach(configs::add); - return configs; - }) - .onSuccess(success -> log.info( - String.format("Listing all configs for device %s and tenant %s", - deviceId, tenantId))) - .onFailure(throwable -> log.error("Error: {}", throwable)); + return searchForDevice(sqlConnection, deviceId, tenantId) + .compose( + counter -> { + if (counter < 1) { + throw new DeviceNotFoundException(String.format("Device with id %s and tenant id %s doesn't exist", + deviceId, + tenantId)); + } + return SqlTemplate + .forQuery(sqlConnection, SQL_LIST) + .mapTo(DeviceConfig.class) + .execute(Map.of("deviceId", deviceId, "tenantId", tenantId, "limit", queryLimit)) + .map(rowSet -> { + final List configs = new ArrayList<>(); + rowSet.forEach(configs::add); + return configs; + }) + .onSuccess(success -> log.info( + String.format("Listing all configs for device %s and tenant %s", + deviceId, tenantId))) + .onFailure(throwable -> log.error("Error: {}", throwable)); + }); } @@ -176,7 +185,7 @@ public Future createNew(SqlConnection sqlConnection, DeviceC .compose( counter -> { if (counter < 1) { - throw new IllegalStateException(String.format("Device with id %s and tenant id %s doesn't exist", + throw new DeviceNotFoundException(String.format("Device with id %s and tenant id %s doesn't exist", entity.getDeviceId(), entity.getTenantId())); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java index 23f939fe..f3d40fdd 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java @@ -36,6 +36,8 @@ public class DeviceCommandServiceImpl implements DeviceCommandService { * @param routingContext The routing context */ public void postCommand(RoutingContext routingContext) { + // TODO publish command and send response log.info("postCommand received"); + routingContext.response().setStatusCode(501).end(); } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java index 2374c51f..f2d33012 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java @@ -60,7 +60,7 @@ public Future modifyCloudToDeviceConfig(DeviceConfigReques } public Future listAll(String deviceId, String tenantId, int limit) { - return db.getDbClient().withTransaction( + return db.getDbClient().withConnection( sqlConnection -> repository.listAll(sqlConnection, deviceId, tenantId, limit) .map( result -> { diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java index dd894ea0..23a8d33e 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java @@ -93,6 +93,7 @@ private void registerVertxCloseHook() { element.getLineNumber())) .collect(Collectors.joining(System.lineSeparator())); log.warn("managed vert.x instance has been closed unexpectedly{}{}", System.lineSeparator(), s); + this.doStop(); completion.complete(); }; vertxInternal.addCloseHook(closeHook); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java index 2af8a2a8..a37b6b18 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java @@ -20,8 +20,7 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.validation.BadRequestException; - -import java.util.NoSuchElementException; +import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; /** * HTTP Response utilities class @@ -80,18 +79,23 @@ public static void errorResponse(RoutingContext rc, Throwable error) { || error instanceof IllegalStateException || error instanceof NullPointerException || error instanceof BadRequestException) { - + // Bad Request status = 400; message = error.getMessage(); - } else if (error instanceof NoSuchElementException) { + } else if (error instanceof DeviceNotFoundException) { // Not Found status = 404; message = error.getMessage(); } else { // Internal Server Error status = 500; - message = "Internal Server Error"; + if (error != null) { + message = String.format("Internal Server Error: %s", error.getMessage()); + } else { + message = "Internal Server Error"; + } + } rc.response() diff --git a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml index 1c44c3ee..20aa6466 100644 --- a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml +++ b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml @@ -25,7 +25,11 @@ paths: "200": description: Command was sent successfully "400": - description: Command can not be send to device + description: Command Validation error or Bad request + "404": + description: Device not found + "500": + description: Internal server error operationId: postCommand summary: Send a command to device. parameters: @@ -65,6 +69,10 @@ paths: schema: $ref: '#/components/schemas/ListDeviceConfigVersionsResponse' description: Lists the device config versions + "404": + description: Device not found + "500": + description: Internal server error operationId: listConfigVersions summary: List a device config versions description: "Lists the last few versions of the device configuration in descending @@ -94,7 +102,7 @@ paths: "400": description: Validation error or Bad request "404": - description: Not Found + description: Device not found "500": description: Internal Server error operationId: modifyCloudToDeviceConfig diff --git a/device-communication/src/main/resources/api/hono-endpoint.yaml b/device-communication/src/main/resources/api/hono-endpoint.yaml new file mode 100644 index 00000000..e8c29a34 --- /dev/null +++ b/device-communication/src/main/resources/api/hono-endpoint.yaml @@ -0,0 +1,233 @@ +swagger: "2.0" +x-components: { } +host: "hono-device-communication-gce.endpoints.sotec-iot-core-dev.cloud.goog" +info: + description: Device commands and configs API. + title: Hono Device Communication API + version: 1.0.0 +schemes: + - http +basePath: /api/v1 +definitions: + DeviceCommandRequest: + description: Command json object structure + example: + binaryData: A base64-encoded string + subfolder: Optional subfolder for the command + properties: + binaryData: + description: "The command data to send to the device in base64-encoded string format.\r\n\r\n" + type: string + subfolder: + description: >- + Optional subfolder for the command. If empty, the command will be + delivered to the /devices/{device-id}/commands topic, otherwise it + will be delivered to the /devices/{device-id}/commands/{subfolder} + topic. Multi-level subfolders are allowed. This field must not have + more than 256 characters, and must not contain any MQTT wildcards + ("+" or "#") or null characters. + type: string + required: + - binaryData + title: Root Type for Command + type: object + DeviceConfig: + description: The device configuration. + example: + binaryData: string + cloudUpdateTime: string + deviceAckTime: string + version: string + properties: + binaryData: + description: >- + string (bytes format) The device configuration data in string + base64-encoded format. + type: string + cloudUpdateTime: + description: >- + String (Timestamp format) [Output only] The time at which this + configuration version was updated in Cloud IoT Core. This timestamp is + set by the server. Timestamp in RFC3339 UTC "Zulu" format, accurate + to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". + type: string + deviceAckTime: + description: >- + string (Timestamp format) [Output only] The time at which Cloud IoT + Core received the acknowledgment from the device, indicating that the + device has received this configuration version. If this field is not + present, the device has not yet acknowledged that it received this + version. Note that when the config was sent to the device, many config + versions may have been available in Cloud IoT Core while the device + was disconnected, and on connection, only the latest version is sent + to the device. Some versions may never be sent to the device, and + therefore are never acknowledged. This timestamp is set by Cloud IoT + Core. Timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. + Example: "2014-10-02T15:01:23.045123456Z". + type: string + version: + description: >- + String (int64 format) [Output only] The version of this update. The + version number is assigned by the server, and is always greater than 0 + after device creation. The version must be 0 on the devices.create + request if a config is specified; the response of devices.create will + always have a value of 1. + type: string + title: Root Type for DeviceConfigResponse + type: object + DeviceConfigRequest: + description: Request body for modifying device configs + properties: + binaryData: + description: "string (bytes format) The configuration data for the device in string base64-encoded format.\r\n" + type: string + versionToUpdate: + description: "string (int64 format) The Config version number." + type: string + required: + - binaryData + type: object + DeviceConfigResponse: + description: The device configuration. + example: + binaryData: string + cloudUpdateTime: string + version: string + properties: + binaryData: + description: >- + string (bytes format) The device configuration data in string + base64-encoded format. + type: string + cloudUpdateTime: + description: >- + String (Timestamp format) [Output only] The time at which this + configuration version was updated in Cloud IoT Core. This timestamp is + set by the server. Timestamp in RFC3339 UTC "Zulu" format, accurate + to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". + type: string + version: + description: >- + String (int64 format) [Output only] The version of this update. The + version number is assigned by the server, and is always greater than 0 + after device creation. The version must be 0 on the devices.create + request if a config is specified; the response of devices.create will + always have a value of 1. + type: string + title: New Config version response + type: object + ListDeviceConfigVersionsResponse: + description: A list of a device config versions + example: + deviceConfigs: + - object: DeviceConfigResponse + properties: + deviceConfigs: + description: List of DeviceConfig objects + items: + $ref: "#/definitions/DeviceConfig" + type: array + title: Root Type for ListDeviceConfigVersionsResponse + type: object +paths: + "/commands/{tenantid}/{deviceid}": + parameters: + - description: Unique registry ID + in: path + name: tenantid + required: true + type: string + - description: Unique device ID + in: path + name: deviceid + required: true + type: string + post: + consumes: + - application/json + operationId: postCommand + parameters: + - description: CommandRequest object as JSON + in: body + name: body + required: true + schema: + $ref: "#/definitions/DeviceCommandRequest" + responses: + "200": + description: Command was sent successfully + "400": + description: Command can not be send to device + summary: Send a command to device. + tags: + - COMMANDS + + "/configs/{tenantid}/{deviceid}": + get: + description: >- + Lists the last few versions of the device configuration in descending + order (i.e.: newest first). + operationId: listConfigVersions + parameters: + - description: >- + The number of versions to list. Versions are listed in decreasing + order of the version number. The maximum number of versions saved in + Database is 10. If this value is zero, it will return all the + versions available. + in: query + maximum: 10 + minimum: 0 + name: numVersions + required: false + type: integer + produces: + - application/json + responses: + "200": + description: Lists the device config versions + schema: + $ref: "#/definitions/ListDeviceConfigVersionsResponse" + summary: List a device config versions + tags: + - CONFIGS + parameters: + - description: Unique registry id + in: path + name: tenantid + required: true + type: string + - description: Unique device id + in: path + name: deviceid + required: true + type: string + post: + consumes: + - application/json + description: >- + Creates an device config version and Returns the the new configuration + version and its metadata. + operationId: modifyCloudToDeviceConfig + parameters: + - description: DeviceConfigRequest object as JSON + in: body + name: body + required: true + schema: + $ref: "#/definitions/DeviceConfigRequest" + produces: + - application/json + responses: + "200": + description: Device config updated successfully + schema: + $ref: "#/definitions/DeviceConfigResponse" + "400": + description: Validation error or Bad request + "404": + description: Not Found + "500": + description: Internal Server error + summary: Modify cloud to device config + tags: + - CONFIGS \ No newline at end of file diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java index a5a61fa5..b7ddf227 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java @@ -138,9 +138,12 @@ void startSucceeded() { when(routerMock.route(any())).thenReturn(routeMock); try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { + DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); + deviceCommunicationHttpServerSpy.start(); - this.deviceCommunicationHttpServer.start(); + verify(deviceCommunicationHttpServerSpy, times(1)).createRouterWithEndpoints(eq(routerBuilderMock), any()); + verify(deviceCommunicationHttpServerSpy, times(1)).startVertxServer(any()); mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); routerMockedStatic.verify(() -> Router.router(vertxMock), times(1)); @@ -238,8 +241,13 @@ void createServerFailed() { when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); when(serverConfigMock.getBasePath()).thenReturn("/basePath"); when(routerMock.route(any())).thenReturn(routeMock); + DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); + deviceCommunicationHttpServerSpy.start(); - this.deviceCommunicationHttpServer.start(); + + verify(deviceCommunicationHttpServerSpy, times(1)).createRouterWithEndpoints(eq(routerBuilderMock), any()); + verify(deviceCommunicationHttpServerSpy, times(1)).startVertxServer(any()); + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java index ad563f8e..081a71f1 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java @@ -112,8 +112,8 @@ void addRoutes() { @Test void handleModifyCloudToDeviceConfig_success() { - when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); - when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); when(routingContextMock.body()).thenReturn(requestBodyMock); when(requestBodyMock.asJsonObject()).thenReturn(jsonObjMock); when(jsonObjMock.mapTo(DeviceConfigRequest.class)).thenReturn(deviceConfigRequest); @@ -126,8 +126,8 @@ void handleModifyCloudToDeviceConfig_success() { var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); verify(routingContextMock, times(1)).body(); verify(requestBodyMock, times(1)).asJsonObject(); verify(jsonObjMock, times(1)).mapTo(DeviceConfigRequest.class); @@ -138,8 +138,8 @@ void handleModifyCloudToDeviceConfig_success() { @Test void handleModifyCloudToDeviceConfig_failure() { - when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); - when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); when(routingContextMock.body()).thenReturn(requestBodyMock); when(requestBodyMock.asJsonObject()).thenReturn(jsonObjMock); when(jsonObjMock.mapTo(DeviceConfigRequest.class)).thenReturn(deviceConfigRequest); @@ -153,9 +153,9 @@ void handleModifyCloudToDeviceConfig_failure() { var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); verify(routingContextMock, times(1)).body(); verify(requestBodyMock, times(1)).asJsonObject(); verify(jsonObjMock, times(1)).mapTo(DeviceConfigRequest.class); @@ -170,8 +170,8 @@ void handleListConfigVersions_success() { ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = new ListDeviceConfigVersionsResponse(List.of(deviceConfig)); MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); when(routingContextMock.queryParams()).thenReturn(queryParams); - when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); - when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); when(routingContextMock.response()).thenReturn(httpServerResponseMock); when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); when(httpServerResponseMock.putHeader("Content-Type", @@ -182,8 +182,8 @@ void handleListConfigVersions_success() { verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); verify(routingContextMock, times(1)).queryParams(); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); verifySuccessResponse(results, listDeviceConfigVersionsResponse); @@ -194,8 +194,8 @@ void handleListConfigVersions_success() { void handleListConfigVersions_failed() { MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); when(routingContextMock.queryParams()).thenReturn(queryParams); - when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER)).thenReturn(tenantID); - when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER)).thenReturn(deviceID); + when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); + when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); when(routingContextMock.response()).thenReturn(httpServerResponseMock); when(httpServerResponseMock.setStatusCode(anyInt())).thenReturn(httpServerResponseMock); when(illegalArgumentExceptionMock.getMessage()).thenReturn(errorMsg); @@ -207,9 +207,9 @@ void handleListConfigVersions_failed() { verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); verify(routingContextMock, times(1)).queryParams(); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); - verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMETER); + verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); verifyErrorResponse(results); diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java index b248bb13..a8099eb3 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java @@ -97,12 +97,12 @@ void modifyCloudToDeviceConfig_failure() { void listAll_success() { var deviceConfigVersions = new ListDeviceConfigVersionsResponse(); when(dbMock.getDbClient()).thenReturn(poolMock); - when(poolMock.withTransaction(any())).thenReturn(Future.succeededFuture(deviceConfigVersions)); + when(poolMock.withConnection(any())).thenReturn(Future.succeededFuture(deviceConfigVersions)); var results = deviceConfigService.listAll(deviceId, tenantId, 10); verify(dbMock, times(1)).getDbClient(); - verify(poolMock, times(1)).withTransaction(any()); + verify(poolMock, times(1)).withConnection(any()); Assertions.assertTrue(results.succeeded()); @@ -112,12 +112,12 @@ void listAll_success() { @Test void listAll_failed() { when(dbMock.getDbClient()).thenReturn(poolMock); - when(poolMock.withTransaction(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); + when(poolMock.withConnection(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); var results = deviceConfigService.listAll(deviceId, tenantId, 10); verify(dbMock, times(1)).getDbClient(); - verify(poolMock, times(1)).withTransaction(any()); + verify(poolMock, times(1)).withConnection(any()); Assertions.assertTrue(results.failed()); From 85989cd4dd301a0bf634b840e9c9f7fc3e19798c Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Thu, 23 Feb 2023 10:25:34 +0100 Subject: [PATCH 04/18] remove entity package --- .../api/entity/DeviceCommandRequest.java | 83 ----------- .../api/entity/DeviceConfig.java | 129 ----------------- .../api/entity/DeviceConfigEntity.java | 136 ------------------ .../api/entity/DeviceConfigRequest.java | 83 ----------- .../ListDeviceConfigVersionsResponse.java | 73 ---------- 5 files changed, 504 deletions(-) delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java deleted file mode 100644 index 23b12f65..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceCommandRequest.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.eclipse.hono.communication.api.entity; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Command json object structure - **/ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class DeviceCommandRequest { - - private String binaryData; - private String subfolder; - - public DeviceCommandRequest () { - - } - - public DeviceCommandRequest (String binaryData, String subfolder) { - this.binaryData = binaryData; - this.subfolder = subfolder; - } - - - @JsonProperty("binaryData") - public String getBinaryData() { - return binaryData; - } - public void setBinaryData(String binaryData) { - this.binaryData = binaryData; - } - - - @JsonProperty("subfolder") - public String getSubfolder() { - return subfolder; - } - public void setSubfolder(String subfolder) { - this.subfolder = subfolder; - } - - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DeviceCommandRequest deviceCommandRequest = (DeviceCommandRequest) o; - return Objects.equals(binaryData, deviceCommandRequest.binaryData) && - Objects.equals(subfolder, deviceCommandRequest.subfolder); - } - - @Override - public int hashCode() { - return Objects.hash(binaryData, subfolder); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class DeviceCommandRequest {\n"); - - sb.append(" binaryData: ").append(toIndentedString(binaryData)).append("\n"); - sb.append(" subfolder: ").append(toIndentedString(subfolder)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java deleted file mode 100644 index fa2590af..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfig.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * *********************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation - *

- * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - *

- * 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.eclipse.hono.communication.api.entity; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.Objects; - -/** - * The device configuration. - **/ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class DeviceConfig { - - private String version; - private String cloudUpdateTime; - private String deviceAckTime; - private String binaryData; - - public DeviceConfig() { - - } - - public DeviceConfig(String version, String cloudUpdateTime, String deviceAckTime, String binaryData) { - this.version = version; - this.cloudUpdateTime = cloudUpdateTime; - this.deviceAckTime = deviceAckTime; - this.binaryData = binaryData; - } - - - @JsonProperty("version") - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - - @JsonProperty("cloudUpdateTime") - public String getCloudUpdateTime() { - return cloudUpdateTime; - } - - public void setCloudUpdateTime(String cloudUpdateTime) { - this.cloudUpdateTime = cloudUpdateTime; - } - - - @JsonProperty("deviceAckTime") - public String getDeviceAckTime() { - return deviceAckTime; - } - - public void setDeviceAckTime(String deviceAckTime) { - this.deviceAckTime = deviceAckTime; - } - - - @JsonProperty("binaryData") - public String getBinaryData() { - return binaryData; - } - - public void setBinaryData(String binaryData) { - this.binaryData = binaryData; - } - - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DeviceConfig deviceConfig = (DeviceConfig) o; - return Objects.equals(version, deviceConfig.version) && - Objects.equals(cloudUpdateTime, deviceConfig.cloudUpdateTime) && - Objects.equals(deviceAckTime, deviceConfig.deviceAckTime) && - Objects.equals(binaryData, deviceConfig.binaryData); - } - - @Override - public int hashCode() { - return Objects.hash(version, cloudUpdateTime, deviceAckTime, binaryData); - } - - @Override - public String toString() { - - String sb = "class DeviceConfig {\n" + - " version: " + toIndentedString(version) + "\n" + - " cloudUpdateTime: " + toIndentedString(cloudUpdateTime) + "\n" + - " deviceAckTime: " + toIndentedString(deviceAckTime) + "\n" + - " binaryData: " + toIndentedString(binaryData) + "\n" + - "}"; - return sb; - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java deleted file mode 100644 index 3008d5e7..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigEntity.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * *********************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation - *

- * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - *

- * 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.eclipse.hono.communication.api.entity; - -import java.util.Objects; - -/** - * The device configuration entity object. - **/ -public class DeviceConfigEntity { - - - private int version; - private String tenantId; - private String deviceId; - - private String cloudUpdateTime; - private String deviceAckTime; - private String binaryData; - - public DeviceConfigEntity(int version, - String tenantId, - String deviceId, - String cloudUpdateTime, - String deviceAckTime, - String binaryData) { - this.version = version; - this.tenantId = tenantId; - this.deviceId = deviceId; - this.cloudUpdateTime = cloudUpdateTime; - this.deviceAckTime = deviceAckTime; - this.binaryData = binaryData; - } - - public DeviceConfigEntity(String tenantId, String deviceId, DeviceConfig deviceConfig) { - this.version = Integer.parseInt(deviceConfig.getVersion()); - this.cloudUpdateTime = deviceConfig.getCloudUpdateTime(); - this.deviceAckTime = deviceConfig.getDeviceAckTime(); - this.binaryData = deviceConfig.getBinaryData(); - } - - public DeviceConfigEntity() { - } - - - public int getVersion() { - return version; - } - - public void setVersion(int version) { - this.version = version; - } - - public String getTenantId() { - return tenantId; - } - - public void setTenantId(String tenantId) { - this.tenantId = tenantId; - } - - public String getDeviceId() { - return deviceId; - } - - public void setDeviceId(String deviceId) { - this.deviceId = deviceId; - } - - - public String getCloudUpdateTime() { - return cloudUpdateTime; - } - - public void setCloudUpdateTime(String cloudUpdateTime) { - this.cloudUpdateTime = cloudUpdateTime; - } - - public String getDeviceAckTime() { - return deviceAckTime; - } - - public void setDeviceAckTime(String deviceAckTime) { - this.deviceAckTime = deviceAckTime; - } - - public String getBinaryData() { - return binaryData; - } - - public void setBinaryData(String binaryData) { - this.binaryData = binaryData; - } - - - @Override - public String toString() { - return "DeviceConfigEntity{" + - "version=" + version + - ", tenantId='" + tenantId + '\'' + - ", deviceId='" + deviceId + '\'' + - ", cloudUpdateTime='" + cloudUpdateTime + '\'' + - ", deviceAckTime='" + deviceAckTime + '\'' + - ", binaryData='" + binaryData + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceConfigEntity that = (DeviceConfigEntity) o; - return version == that.version && tenantId.equals(that.tenantId) && deviceId.equals(that.deviceId) && cloudUpdateTime.equals(that.cloudUpdateTime) && Objects.equals(deviceAckTime, that.deviceAckTime) && binaryData.equals(that.binaryData); - } - - @Override - public int hashCode() { - return Objects.hash(version, tenantId, deviceId, cloudUpdateTime, deviceAckTime, binaryData); - } - - -} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java deleted file mode 100644 index c1913b0d..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/DeviceConfigRequest.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.eclipse.hono.communication.api.entity; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Request body for modifying device configs - **/ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class DeviceConfigRequest { - - private String versionToUpdate; - private String binaryData; - - public DeviceConfigRequest () { - - } - - public DeviceConfigRequest (String versionToUpdate, String binaryData) { - this.versionToUpdate = versionToUpdate; - this.binaryData = binaryData; - } - - - @JsonProperty("versionToUpdate") - public String getVersionToUpdate() { - return versionToUpdate; - } - public void setVersionToUpdate(String versionToUpdate) { - this.versionToUpdate = versionToUpdate; - } - - - @JsonProperty("binaryData") - public String getBinaryData() { - return binaryData; - } - public void setBinaryData(String binaryData) { - this.binaryData = binaryData; - } - - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - DeviceConfigRequest deviceConfigRequest = (DeviceConfigRequest) o; - return Objects.equals(versionToUpdate, deviceConfigRequest.versionToUpdate) && - Objects.equals(binaryData, deviceConfigRequest.binaryData); - } - - @Override - public int hashCode() { - return Objects.hash(versionToUpdate, binaryData); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class DeviceConfigRequest {\n"); - - sb.append(" versionToUpdate: ").append(toIndentedString(versionToUpdate)).append("\n"); - sb.append(" binaryData: ").append(toIndentedString(binaryData)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java deleted file mode 100644 index 892d4001..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/entity/ListDeviceConfigVersionsResponse.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.eclipse.hono.communication.api.entity; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * A list of a device config versions - **/ -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ListDeviceConfigVersionsResponse { - - private List deviceConfigs = new ArrayList<>(); - - public ListDeviceConfigVersionsResponse() { - - } - - public ListDeviceConfigVersionsResponse(List deviceConfigs) { - this.deviceConfigs = deviceConfigs; - } - - - @JsonProperty("deviceConfigs") - public List getDeviceConfigs() { - return deviceConfigs; - } - - public void setDeviceConfigs(List deviceConfigs) { - this.deviceConfigs = deviceConfigs; - } - - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = (ListDeviceConfigVersionsResponse) o; - return Objects.equals(deviceConfigs, listDeviceConfigVersionsResponse.deviceConfigs); - } - - @Override - public int hashCode() { - return Objects.hash(deviceConfigs); - } - - @Override - public String toString() { - - String sb = "class ListDeviceConfigVersionsResponse {\n" + - " deviceConfigs: " + toIndentedString(deviceConfigs) + "\n" + - "}"; - return sb; - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} From 3c4940fd9a0ed80e3820cea4ca239b64350020d8 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Thu, 23 Feb 2023 13:50:06 +0100 Subject: [PATCH 05/18] change database naming --- .../hono/communication/api/data/DeviceConfig.java | 4 ++++ .../repository/DeviceConfigsRepositoryImpl.java | 14 +++++++------- .../resources/db/create_device_config_table.sql | 14 +++++++------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java index 509f920a..bebec8c7 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java @@ -49,6 +49,7 @@ public String getVersion() { return version; } + @JsonProperty("version") public void setVersion(String version) { this.version = version; } @@ -59,6 +60,7 @@ public String getCloudUpdateTime() { return cloudUpdateTime; } + @JsonProperty("cloud_update_time") public void setCloudUpdateTime(String cloudUpdateTime) { this.cloudUpdateTime = cloudUpdateTime; } @@ -69,6 +71,7 @@ public String getDeviceAckTime() { return deviceAckTime; } + @JsonProperty("device_ack_time") public void setDeviceAckTime(String deviceAckTime) { this.deviceAckTime = deviceAckTime; } @@ -79,6 +82,7 @@ public String getBinaryData() { return binaryData; } + @JsonProperty("binary_data") public void setBinaryData(String binaryData) { this.binaryData = binaryData; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java index b3ec4e7c..705540c6 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java @@ -39,14 +39,14 @@ */ @ApplicationScoped public class DeviceConfigsRepositoryImpl implements DeviceConfigsRepository { - private final static String SQL_INSERT = "INSERT INTO \"deviceConfig\" (version, \"tenantId\", \"deviceId\", \"cloudUpdateTime\", \"deviceAckTime\", \"binaryData\") " + + private final static String SQL_INSERT = "INSERT INTO device_configs (version, tenant_id, device_id, cloud_update_time, device_ack_time, binary_data) " + "VALUES (#{version}, #{tenantId}, #{deviceId}, #{cloudUpdateTime}, #{deviceAckTime}, #{binaryData}) RETURNING version"; - private final static String SQL_LIST = "SELECT version, \"cloudUpdateTime\", COALESCE(\"deviceAckTime\",'') AS \"deviceAckTime\", \"binaryData\" " + - "FROM \"deviceConfig\" WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId} ORDER BY version DESC LIMIT #{limit}"; - private final static String SQL_DELETE_MIN_VERSION = "DELETE FROM \"deviceConfig\" WHERE\"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId} " + - "and version = (SELECT MIN(version) from \"deviceConfig\" WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId}) RETURNING version"; - private final static String SQL_FIND_TOTAL_AND_MAX_VERSION = "SELECT COALESCE(COUNT(*), 0) as total, COALESCE(MAX(version), 0) as max_version from \"deviceConfig\" " + - "WHERE \"deviceId\" = #{deviceId} and \"tenantId\" = #{tenantId}"; + private final static String SQL_LIST = "SELECT version, cloud_update_time, device_ack_time, binary_data " + + "FROM device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId} ORDER BY version DESC LIMIT #{limit}"; + private final static String SQL_DELETE_MIN_VERSION = "DELETE FROM device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId} " + + "and version = (SELECT MIN(version) from device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId}) RETURNING version"; + private final static String SQL_FIND_TOTAL_AND_MAX_VERSION = "SELECT COALESCE(COUNT(*), 0) as total, COALESCE(MAX(version), 0) as max_version from device_configs " + + "WHERE device_id = #{deviceId} and tenant_id = #{tenantId}"; private final static int MAX_LIMIT = 10; private final static Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); diff --git a/device-communication/src/main/resources/db/create_device_config_table.sql b/device-communication/src/main/resources/db/create_device_config_table.sql index 1f11816d..ae425132 100644 --- a/device-communication/src/main/resources/db/create_device_config_table.sql +++ b/device-communication/src/main/resources/db/create_device_config_table.sql @@ -15,14 +15,14 @@ */ -CREATE TABLE IF NOT EXISTS "deviceConfig" +CREATE TABLE IF NOT EXISTS device_configs ( version INT not null, - "tenantId" VARCHAR(100) not null, - "deviceId" VARCHAR(100) not null, - "cloudUpdateTime" VARCHAR(100) not null, - "deviceAckTime" VARCHAR(100), - "binaryData" VARCHAR not null, + tenant_id VARCHAR(100) not null, + device_id VARCHAR(100) not null, + cloud_update_time VARCHAR(100) not null, + device_ack_time VARCHAR(100), + binary_data VARCHAR not null, - PRIMARY KEY (version, "tenantId", "deviceId") + PRIMARY KEY (version, tenant_id, device_id) ) \ No newline at end of file From 466b487695480816e0b43e22c6dce488302192ae Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Mon, 27 Feb 2023 13:34:06 +0100 Subject: [PATCH 06/18] fix hono check style --- device-communication/pom.xml | 32 ++++++ .../hono/communication/api/Application.java | 18 +++- .../api/DeviceCommunicationHttpServer.java | 102 +++++++++--------- .../api/config/DeviceCommandConstants.java | 14 ++- .../api/config/DeviceConfigsConstants.java | 40 +++++-- .../api/data/DeviceCommandRequest.java | 43 ++++++-- .../communication/api/data/DeviceConfig.java | 31 ++++-- .../api/data/DeviceConfigEntity.java | 29 +++-- .../api/data/DeviceConfigRequest.java | 43 ++++++-- .../api/data/DeviceConfigResponse.java | 84 --------------- .../ListDeviceConfigVersionsResponse.java | 43 ++++++-- .../exception/DeviceNotFoundException.java | 36 +++++-- ...Handler.java => DeviceCommandHandler.java} | 27 +++-- .../api/handler/DeviceConfigsHandler.java | 53 ++++++--- .../api/mapper/DeviceConfigMapper.java | 26 +++-- .../repository/DeviceConfigsRepository.java | 26 ++++- .../DeviceConfigsRepositoryImpl.java | 83 +++++++------- .../api/service/DatabaseSchemaCreator.java | 6 +- .../service/DatabaseSchemaCreatorImpl.java | 28 +++-- .../api/service/DatabaseService.java | 10 +- .../api/service/DatabaseServiceImpl.java | 36 +++---- .../api/service/DeviceCommandService.java | 8 +- .../api/service/DeviceCommandServiceImpl.java | 15 ++- .../api/service/DeviceConfigService.java | 25 ++++- .../api/service/DeviceConfigServiceImpl.java | 35 +++--- .../VertxHttpHandlerManagerService.java | 20 ++-- .../core/app/AbstractServiceApplication.java | 43 +++++--- .../core/app/ApplicationConfig.java | 25 +++-- .../core/app/DatabaseConfig.java | 5 +- .../communication/core/app/ServerConfig.java | 5 +- .../core/http/AbstractVertxHttpServer.java | 14 ++- .../communication/core/http/HttpServer.java | 6 +- .../core/http/HttpServiceBase.java | 7 +- .../communication/core/utils/DbUtils.java | 27 +++-- .../core/utils/ResponseUtils.java | 33 ++++-- .../api/hono-device-communication-v1.yaml | 33 +----- .../src/main/resources/api/hono-endpoint.yaml | 36 +------ .../communication/api/ApplicationTest.java | 17 ++- .../DeviceCommunicationHttpServerTest.java | 64 ++++++----- .../handler/DeviceCommandsHandlerTest.java | 21 ++-- .../api/handler/DeviceConfigsHandlerTest.java | 59 +++++----- .../DatabaseSchemaCreatorImplTest.java | 16 +-- .../api/service/DatabaseServiceImplTest.java | 14 +-- .../service/DeviceConfigServiceImplTest.java | 38 +++---- 44 files changed, 805 insertions(+), 571 deletions(-) delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java rename device-communication/src/main/java/org/eclipse/hono/communication/api/handler/{DeviceCommandsHandler.java => DeviceCommandHandler.java} (70%) diff --git a/device-communication/pom.xml b/device-communication/pom.xml index 470b55f5..23fa49a4 100644 --- a/device-communication/pom.xml +++ b/device-communication/pom.xml @@ -17,6 +17,7 @@ true 3.0.0-M7 1.5.3.Final + 2.1.0 @@ -166,6 +167,37 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.2 + + + org.eclipse.hono + hono-legal + ${hono.version} + + + com.puppycrawl.tools + checkstyle + 10.2 + + + + + checkstyle-check + verify + + check + + + + + checkstyle/default.xml + checkstyle/suppressions.xml + true + + diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java index 6fbbd88f..5be95fd9 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/Application.java @@ -16,24 +16,32 @@ package org.eclipse.hono.communication.api; -import io.vertx.core.Vertx; import org.eclipse.hono.communication.core.app.AbstractServiceApplication; import org.eclipse.hono.communication.core.app.ApplicationConfig; import org.eclipse.hono.communication.core.http.HttpServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import io.vertx.core.Vertx; + /** - * Device Communication application + * Device Communication application. */ public class Application extends AbstractServiceApplication { private final Logger log = LoggerFactory.getLogger(AbstractServiceApplication.class); private final HttpServer server; - public Application(Vertx vertx, - ApplicationConfig appConfigs, - HttpServer server) { + /** + * Creates new Application with all dependencies. + * + * @param vertx The quarkus Vertx instance + * @param appConfigs The application configs + * @param server The http server + */ + public Application(final Vertx vertx, + final ApplicationConfig appConfigs, + final HttpServer server) { super(vertx, appConfigs); this.server = server; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index 16dbd644..24e78682 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -16,17 +16,12 @@ package org.eclipse.hono.communication.api; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.quarkus.runtime.Quarkus; -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.ext.healthchecks.HealthCheckHandler; -import io.vertx.ext.healthchecks.Status; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.RouterBuilder; -import io.vertx.ext.web.validation.BadRequestException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.inject.Singleton; + import org.eclipse.hono.communication.api.service.DatabaseSchemaCreator; import org.eclipse.hono.communication.api.service.DatabaseService; import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; @@ -39,13 +34,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Singleton; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.quarkus.runtime.Quarkus; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.ext.healthchecks.HealthCheckHandler; +import io.vertx.ext.healthchecks.Status; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; +import io.vertx.ext.web.validation.BadRequestException; + /** - * Vertx HTTP Server for the device communication api + * Vertx HTTP Server for the device communication api. */ @Singleton public class DeviceCommunicationHttpServer extends AbstractVertxHttpServer implements HttpServer { @@ -59,11 +62,20 @@ public class DeviceCommunicationHttpServer extends AbstractVertxHttpServer imple private List httpEndpointHandlers; - public DeviceCommunicationHttpServer(ApplicationConfig appConfigs, - Vertx vertx, - VertxHttpHandlerManagerService httpHandlerManager, - DatabaseService databaseService, - DatabaseSchemaCreator databaseSchemaCreator) { + /** + * Creates a new DeviceCommunicationHttpServer with all dependencies. + * + * @param appConfigs THe application configurations + * @param vertx The quarkus Vertx instance + * @param httpHandlerManager The http handler manager + * @param databaseService The database connection + * @param databaseSchemaCreator The database migrations service + */ + public DeviceCommunicationHttpServer(final ApplicationConfig appConfigs, + final Vertx vertx, + final VertxHttpHandlerManagerService httpHandlerManager, + final DatabaseService databaseService, + final DatabaseSchemaCreator databaseSchemaCreator) { super(appConfigs, vertx); this.httpHandlerManager = httpHandlerManager; this.databaseSchemaCreator = databaseSchemaCreator; @@ -80,10 +92,8 @@ public void start() { // Create Endpoints Router this.httpEndpointHandlers = httpHandlerManager.getAvailableHandlerServices(); RouterBuilder.create(this.vertx, appConfigs.getServerConfig().getOpenApiFilePath()) - .onSuccess(routerBuilder -> - { - - Router apiRouter = this.createRouterWithEndpoints(routerBuilder, httpEndpointHandlers); + .onSuccess(routerBuilder -> { + final Router apiRouter = this.createRouterWithEndpoints(routerBuilder, httpEndpointHandlers); this.startVertxServer(apiRouter); }) .onFailure(error -> { @@ -103,26 +113,26 @@ public void start() { } /** - * Creates the Router object and adds endpoints and handlers + * Creates the Router object and adds endpoints and handlers. * * @param routerBuilder Vertx RouterBuilder object * @param httpEndpointHandlers All available http endpoint handlers * @return The created Router object */ - Router createRouterWithEndpoints(RouterBuilder routerBuilder, List httpEndpointHandlers) { + Router createRouterWithEndpoints(final RouterBuilder routerBuilder, final List httpEndpointHandlers) { for (HttpEndpointHandler handlerService : httpEndpointHandlers) { handlerService.addRoutes(routerBuilder); } - var apiRouter = Router.router(vertx); - var router = routerBuilder.createRouter(); + final var apiRouter = Router.router(vertx); + final var router = routerBuilder.createRouter(); apiRouter.errorHandler(400, routingContext -> ResponseUtils.errorResponse(routingContext, routingContext.failure())); apiRouter.errorHandler(404, routingContext -> ResponseUtils.errorResponse(routingContext, routingContext.failure())); - var serverConfig = appConfigs.getServerConfig(); + final var serverConfig = appConfigs.getServerConfig(); addHealthCheckHandlers(apiRouter, serverConfig); - var basePath = String.format("%s*", serverConfig.getBasePath()); // absolut path not allowed only /* + final var basePath = String.format("%s*", serverConfig.getBasePath()); // absolut path not allowed only /* log.info("API base path: {}", basePath); @@ -131,16 +141,16 @@ Router createRouterWithEndpoints(RouterBuilder routerBuilder, List promise.tryComplete(Status.OK()) - ); - + healthCheckHandler.register("liveness", promise -> promise.tryComplete(Status.OK())); router.get(livenessPath).handler(healthCheckHandler); } /** - * Starts the server and blocks until application is stopped + * Starts the server and blocks until application is stopped. * * @param router The Router object */ - void startVertxServer(Router router) { - var serverConfigs = appConfigs.getServerConfig(); - var serverOptions = new HttpServerOptions() + void startVertxServer(final Router router) { + final var serverConfigs = appConfigs.getServerConfig(); + final var serverOptions = new HttpServerOptions() .setPort(serverConfigs.getServerPort()) .setHost(serverConfigs.getServerUrl()); - var serverCreationFuture = vertx + final var serverCreationFuture = vertx .createHttpServer(serverOptions) .requestHandler(router) .listen(); @@ -200,9 +206,9 @@ void startVertxServer(Router router) { * @param routingContext the routing context object * @Throws: NullPointerException – if routingContext is {@code null}. */ - void addDefault400ExceptionHandler(RoutingContext routingContext) { + void addDefault400ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); - String errorMsg = ((BadRequestException) routingContext.failure()).toJson().toString(); + final String errorMsg = ((BadRequestException) routingContext.failure()).toJson().toString(); log.error(errorMsg); routingContext.response().setStatusCode(400).end(errorMsg); } @@ -213,7 +219,7 @@ void addDefault400ExceptionHandler(RoutingContext routingContext) { * @param routingContext the routing context object * @Throws: NullPointerException – if routingContext is {@code null}. */ - void addDefault404ExceptionHandler(RoutingContext routingContext) { + void addDefault404ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); if (!routingContext.response().ended()) { routingContext.response().setStatusCode(404); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java index 2e1f3c2f..ec3778f2 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceCommandConstants.java @@ -17,10 +17,16 @@ package org.eclipse.hono.communication.api.config; /** - * Device commands constant values + * Device commands constant values. */ -public class DeviceCommandConstants { +public final class DeviceCommandConstants { - // Open api operationIds - public final static String POST_DEVICE_COMMAND_OP_ID = "postCommand"; + /** + * OpenApi POST device command operation id. + */ + public static final String POST_DEVICE_COMMAND_OP_ID = "postCommand"; + + private DeviceCommandConstants() { + // avoid instantiation + } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java index 18bbc869..86ff6ed2 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/config/DeviceConfigsConstants.java @@ -17,17 +17,37 @@ package org.eclipse.hono.communication.api.config; /** - * Device configs constant values + * Device configs constant values. */ -public class DeviceConfigsConstants { +public final class DeviceConfigsConstants { - // Open api operationIds - public final static String POST_MODIFY_DEVICE_CONFIG_OP_ID = "modifyCloudToDeviceConfig"; - public final static String LIST_CONFIG_VERSIONS_OP_ID = "listConfigVersions"; + /** + * OpenApi GET device configs operation id. + */ + public static final String LIST_CONFIG_VERSIONS_OP_ID = "listConfigVersions"; + /** + * Path parameter name for tenantId. + */ + public static final String TENANT_PATH_PARAMS = "tenantid"; + /** + * Path parameter name for deviceId. + */ + public static final String DEVICE_PATH_PARAMS = "deviceid"; + /** + * Path parameter name for number of versions. + */ + public static final String NUM_VERSION_QUERY_PARAMS = "numVersions"; - //Device Config Repository - public final static String TENANT_PATH_PARAMS = "tenantid"; - public final static String DEVICE_PATH_PARAMS = "deviceid"; - public final static String NUM_VERSION_QUERY_PARAMS = "numVersions"; - public final static String CREATE_SQL_SCRIPT_PATH = "db/create_device_config_table.sql"; + /** + * Sql migrations script path. + */ + public static final String CREATE_SQL_SCRIPT_PATH = "db/create_device_config_table.sql"; + /** + * OpenApi POST device configs operation id. + */ + public static final String POST_MODIFY_DEVICE_CONFIG_OP_ID = "modifyCloudToDeviceConfig"; + + private DeviceConfigsConstants() { + // avoid instantiation + } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java index 27a253be..91d7874e 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceCommandRequest.java @@ -1,12 +1,30 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; + + /** - * Command json object structure + * Command json object structure. **/ @JsonInclude(JsonInclude.Include.NON_NULL) public class DeviceCommandRequest { @@ -14,11 +32,20 @@ public class DeviceCommandRequest { private String binaryData; private String subfolder; + /** + * Creates a new DeviceCommandRequest. + */ public DeviceCommandRequest() { } - public DeviceCommandRequest(String binaryData, String subfolder) { + /** + * Creates a new DeviceCommandRequest. + * + * @param binaryData Binary data + * @param subfolder THe subfolder + */ + public DeviceCommandRequest(final String binaryData, final String subfolder) { this.binaryData = binaryData; this.subfolder = subfolder; } @@ -29,7 +56,7 @@ public String getBinaryData() { return binaryData; } - public void setBinaryData(String binaryData) { + public void setBinaryData(final String binaryData) { this.binaryData = binaryData; } @@ -39,20 +66,20 @@ public String getSubfolder() { return subfolder; } - public void setSubfolder(String subfolder) { + public void setSubfolder(final String subfolder) { this.subfolder = subfolder; } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } - DeviceCommandRequest deviceCommandRequest = (DeviceCommandRequest) o; + final DeviceCommandRequest deviceCommandRequest = (DeviceCommandRequest) o; return Objects.equals(binaryData, deviceCommandRequest.binaryData) && Objects.equals(subfolder, deviceCommandRequest.subfolder); } @@ -75,7 +102,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(final Object o) { if (o == null) { return "null"; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java index bebec8c7..47425745 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfig.java @@ -16,10 +16,12 @@ package org.eclipse.hono.communication.api.data; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; + /** * The device configuration. @@ -32,11 +34,22 @@ public class DeviceConfig { private String deviceAckTime; private String binaryData; + /** + * Creates a new device config. + */ public DeviceConfig() { } - public DeviceConfig(String version, String cloudUpdateTime, String deviceAckTime, String binaryData) { + /** + * Creates a new device config. + * + * @param version Config version + * @param cloudUpdateTime Cloud update time + * @param deviceAckTime Device ack time + * @param binaryData Binary data + */ + public DeviceConfig(final String version, final String cloudUpdateTime, final String deviceAckTime, final String binaryData) { this.version = version; this.cloudUpdateTime = cloudUpdateTime; this.deviceAckTime = deviceAckTime; @@ -50,7 +63,7 @@ public String getVersion() { } @JsonProperty("version") - public void setVersion(String version) { + public void setVersion(final String version) { this.version = version; } @@ -61,7 +74,7 @@ public String getCloudUpdateTime() { } @JsonProperty("cloud_update_time") - public void setCloudUpdateTime(String cloudUpdateTime) { + public void setCloudUpdateTime(final String cloudUpdateTime) { this.cloudUpdateTime = cloudUpdateTime; } @@ -72,7 +85,7 @@ public String getDeviceAckTime() { } @JsonProperty("device_ack_time") - public void setDeviceAckTime(String deviceAckTime) { + public void setDeviceAckTime(final String deviceAckTime) { this.deviceAckTime = deviceAckTime; } @@ -83,20 +96,20 @@ public String getBinaryData() { } @JsonProperty("binary_data") - public void setBinaryData(String binaryData) { + public void setBinaryData(final String binaryData) { this.binaryData = binaryData; } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } - DeviceConfig deviceConfig = (DeviceConfig) o; + final DeviceConfig deviceConfig = (DeviceConfig) o; return Objects.equals(version, deviceConfig.version) && Objects.equals(cloudUpdateTime, deviceConfig.cloudUpdateTime) && Objects.equals(deviceAckTime, deviceConfig.deviceAckTime) && @@ -123,7 +136,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(final Object o) { if (o == null) { return "null"; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java index 69a3d87b..186ccc44 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigEntity.java @@ -32,7 +32,10 @@ public class DeviceConfigEntity { private String deviceAckTime; private String binaryData; - + + /** + * Creates new DeviceConfigEntity. + */ public DeviceConfigEntity() { } @@ -41,7 +44,7 @@ public int getVersion() { return version; } - public void setVersion(int version) { + public void setVersion(final int version) { this.version = version; } @@ -49,7 +52,7 @@ public String getTenantId() { return tenantId; } - public void setTenantId(String tenantId) { + public void setTenantId(final String tenantId) { this.tenantId = tenantId; } @@ -57,7 +60,7 @@ public String getDeviceId() { return deviceId; } - public void setDeviceId(String deviceId) { + public void setDeviceId(final String deviceId) { this.deviceId = deviceId; } @@ -66,7 +69,7 @@ public String getCloudUpdateTime() { return cloudUpdateTime; } - public void setCloudUpdateTime(String cloudUpdateTime) { + public void setCloudUpdateTime(final String cloudUpdateTime) { this.cloudUpdateTime = cloudUpdateTime; } @@ -74,7 +77,7 @@ public String getDeviceAckTime() { return deviceAckTime; } - public void setDeviceAckTime(String deviceAckTime) { + public void setDeviceAckTime(final String deviceAckTime) { this.deviceAckTime = deviceAckTime; } @@ -82,7 +85,7 @@ public String getBinaryData() { return binaryData; } - public void setBinaryData(String binaryData) { + public void setBinaryData(final String binaryData) { this.binaryData = binaryData; } @@ -100,10 +103,14 @@ public String toString() { } @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceConfigEntity that = (DeviceConfigEntity) o; + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final var that = (DeviceConfigEntity) o; return version == that.version && tenantId.equals(that.tenantId) && deviceId.equals(that.deviceId) && cloudUpdateTime.equals(that.cloudUpdateTime) && Objects.equals(deviceAckTime, that.deviceAckTime) && binaryData.equals(that.binaryData); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java index 7520dd34..fd33dbdb 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigRequest.java @@ -1,12 +1,30 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; +import java.util.Objects; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.Objects; + /** - * Request body for modifying device configs + * Request body for modifying device configs. **/ @JsonInclude(JsonInclude.Include.NON_NULL) public class DeviceConfigRequest { @@ -14,11 +32,20 @@ public class DeviceConfigRequest { private String versionToUpdate; private String binaryData; + /** + * Creates a new DeviceConfigRequest. + */ public DeviceConfigRequest() { } - public DeviceConfigRequest(String versionToUpdate, String binaryData) { + /** + * Creates a new DeviceConfigRequest. + * + * @param versionToUpdate Version to update + * @param binaryData The binary data + */ + public DeviceConfigRequest(final String versionToUpdate, final String binaryData) { this.versionToUpdate = versionToUpdate; this.binaryData = binaryData; } @@ -29,7 +56,7 @@ public String getVersionToUpdate() { return versionToUpdate; } - public void setVersionToUpdate(String versionToUpdate) { + public void setVersionToUpdate(final String versionToUpdate) { this.versionToUpdate = versionToUpdate; } @@ -39,20 +66,20 @@ public String getBinaryData() { return binaryData; } - public void setBinaryData(String binaryData) { + public void setBinaryData(final String binaryData) { this.binaryData = binaryData; } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } - DeviceConfigRequest deviceConfigRequest = (DeviceConfigRequest) o; + final var deviceConfigRequest = (DeviceConfigRequest) o; return Objects.equals(versionToUpdate, deviceConfigRequest.versionToUpdate) && Objects.equals(binaryData, deviceConfigRequest.binaryData); } @@ -75,7 +102,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(final Object o) { if (o == null) { return "null"; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java deleted file mode 100644 index 017fa248..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/DeviceConfigResponse.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * *********************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation - *

- * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - *

- * 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.eclipse.hono.communication.api.data; - -import java.util.Objects; - -/** - * The device configuration response class. - **/ -public class DeviceConfigResponse { - - - private int version; - private String cloudUpdateTime; - private String binaryData; - - - public DeviceConfigResponse() { - } - - - public int getVersion() { - return version; - } - - public void setVersion(int version) { - this.version = version; - } - - public String getCloudUpdateTime() { - return cloudUpdateTime; - } - - public void setCloudUpdateTime(String cloudUpdateTime) { - this.cloudUpdateTime = cloudUpdateTime; - } - - public String getBinaryData() { - return binaryData; - } - - public void setBinaryData(String binaryData) { - this.binaryData = binaryData; - } - - - @Override - public String toString() { - return "DeviceConfigResponse{" + - "version=" + version + - ", cloudUpdateTime='" + cloudUpdateTime + '\'' + - ", binaryData='" + binaryData + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DeviceConfigResponse that = (DeviceConfigResponse) o; - return version == that.version && cloudUpdateTime.equals(that.cloudUpdateTime) && binaryData.equals(that.binaryData); - } - - @Override - public int hashCode() { - return Objects.hash(version, cloudUpdateTime, binaryData); - } - - -} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java index 2d4f5570..9e0894cf 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/data/ListDeviceConfigVersionsResponse.java @@ -1,25 +1,50 @@ +/* + * *********************************************************** + * Copyright (c) 2023 Contributors to the Eclipse Foundation + *

+ * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + *

+ * 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.eclipse.hono.communication.api.data; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.ArrayList; import java.util.List; import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + + + /** - * A list of a device config versions + * A list of a device config versions. **/ @JsonInclude(JsonInclude.Include.NON_NULL) public class ListDeviceConfigVersionsResponse { private List deviceConfigs = new ArrayList<>(); + /** + * Creates a new ListDeviceConfigVersionsResponse. + */ public ListDeviceConfigVersionsResponse() { } - public ListDeviceConfigVersionsResponse(List deviceConfigs) { + /** + * Creates a new ListDeviceConfigVersionsResponse. + * + * @param deviceConfigs The device configs + */ + public ListDeviceConfigVersionsResponse(final List deviceConfigs) { this.deviceConfigs = deviceConfigs; } @@ -29,20 +54,20 @@ public List getDeviceConfigs() { return deviceConfigs; } - public void setDeviceConfigs(List deviceConfigs) { + public void setDeviceConfigs(final List deviceConfigs) { this.deviceConfigs = deviceConfigs; } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } - ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = (ListDeviceConfigVersionsResponse) o; + final var listDeviceConfigVersionsResponse = (ListDeviceConfigVersionsResponse) o; return Objects.equals(deviceConfigs, listDeviceConfigVersionsResponse.deviceConfigs); } @@ -63,7 +88,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(final Object o) { if (o == null) { return "null"; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/exception/DeviceNotFoundException.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/exception/DeviceNotFoundException.java index af805085..7c83515f 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/exception/DeviceNotFoundException.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/exception/DeviceNotFoundException.java @@ -19,21 +19,43 @@ import java.util.NoSuchElementException; /** - * Device Not found exception code: 404 + * Device Not found exception code: 404. */ -public class DeviceNotFoundException extends NoSuchElementException { +public class +DeviceNotFoundException extends NoSuchElementException { + + /** + * Creates a new DeviceNotFoundException. + */ public DeviceNotFoundException() { } - public DeviceNotFoundException(String s, Throwable cause) { - super(s, cause); + /** + * Creates a new DeviceNotFoundException. + * + * @param msg String message + * @param cause Throwable + */ + public DeviceNotFoundException(final String msg, final Throwable cause) { + super(msg, cause); } - public DeviceNotFoundException(Throwable cause) { + /** + * Creates a new DeviceNotFoundException. + * + * @param cause Throwable + */ + public DeviceNotFoundException(final Throwable cause) { super(cause); } - public DeviceNotFoundException(String s) { - super(s); + + /** + * Creates a new DeviceNotFoundException. + * + * @param msg String message + */ + public DeviceNotFoundException(final String msg) { + super(msg); } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandHandler.java similarity index 70% rename from device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java rename to device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandHandler.java index f27b6afc..e95fc0aa 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandler.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceCommandHandler.java @@ -16,33 +16,44 @@ package org.eclipse.hono.communication.api.handler; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.RouterBuilder; +import javax.enterprise.context.ApplicationScoped; + import org.eclipse.hono.communication.api.config.DeviceCommandConstants; import org.eclipse.hono.communication.api.service.DeviceCommandService; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; -import javax.enterprise.context.ApplicationScoped; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; /** - * Handler for device command endpoints + * Handler for device command endpoints. */ @ApplicationScoped -public class DeviceCommandsHandler implements HttpEndpointHandler { +public class DeviceCommandHandler implements HttpEndpointHandler { private final DeviceCommandService commandService; - public DeviceCommandsHandler(DeviceCommandService commandService) { + /** + * Creates a new DeviceCommandHandler. + * + * @param commandService The device command service + */ + public DeviceCommandHandler(final DeviceCommandService commandService) { this.commandService = commandService; } @Override - public void addRoutes(RouterBuilder routerBuilder) { + public void addRoutes(final RouterBuilder routerBuilder) { routerBuilder.operation(DeviceCommandConstants.POST_DEVICE_COMMAND_OP_ID) .handler(this::handlePostCommand); } - public void handlePostCommand(RoutingContext routingContext) { + /** + * Handle post device commands. + * + * @param routingContext The RoutingContext + */ + public void handlePostCommand(final RoutingContext routingContext) { commandService.postCommand(routingContext); } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java index 26ab7620..a1649e23 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandler.java @@ -16,43 +16,58 @@ package org.eclipse.hono.communication.api.handler; -import io.vertx.core.Future; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.RouterBuilder; +import javax.enterprise.context.ApplicationScoped; + import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; +import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.service.DeviceConfigService; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; import org.eclipse.hono.communication.core.utils.ResponseUtils; -import javax.enterprise.context.ApplicationScoped; +import io.vertx.core.Future; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.RouterBuilder; + + + /** - * Handler for device config endpoints + * Handler for device config endpoints. */ @ApplicationScoped public class DeviceConfigsHandler implements HttpEndpointHandler { private final DeviceConfigService configService; - public DeviceConfigsHandler(DeviceConfigService configService) { + /** + * Creates a new DeviceConfigsHandler. + * + * @param configService The device configs + */ + public DeviceConfigsHandler(final DeviceConfigService configService) { this.configService = configService; } @Override - public void addRoutes(RouterBuilder routerBuilder) { + public void addRoutes(final RouterBuilder routerBuilder) { routerBuilder.operation(DeviceConfigsConstants.LIST_CONFIG_VERSIONS_OP_ID) .handler(this::handleListConfigVersions); routerBuilder.operation(DeviceConfigsConstants.POST_MODIFY_DEVICE_CONFIG_OP_ID) .handler(this::handleModifyCloudToDeviceConfig); } - public Future handleModifyCloudToDeviceConfig(RoutingContext routingContext) { - var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); - var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); + /** + * Handles post device configs. + * + * @param routingContext The RoutingContext + * @return Future of DeviceConfig + */ + public Future handleModifyCloudToDeviceConfig(final RoutingContext routingContext) { + final var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + final var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); final DeviceConfigRequest deviceConfig = routingContext.body() .asJsonObject() @@ -63,12 +78,18 @@ public Future handleModifyCloudToDeviceConfig(RoutingConte .onFailure(err -> ResponseUtils.errorResponse(routingContext, err)); } - public Future handleListConfigVersions(RoutingContext routingContext) { - var numVersions = routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS); + /** + * Handles get device configs. + * + * @param routingContext The RoutingContext + * @return Future of ListDeviceConfigVersionsResponse + */ + public Future handleListConfigVersions(final RoutingContext routingContext) { + final var numVersions = routingContext.queryParams().get(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS); - var limit = numVersions == null ? 0 : Integer.parseInt(numVersions); - var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); - var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); + final var limit = numVersions == null ? 0 : Integer.parseInt(numVersions); + final var tenantId = routingContext.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); + final var deviceId = routingContext.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS); return configService.listAll(deviceId, tenantId, limit) .onSuccess(result -> ResponseUtils.successResponse(routingContext, result)) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java index 0a465125..5a7783f5 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/mapper/DeviceConfigMapper.java @@ -16,23 +16,37 @@ package org.eclipse.hono.communication.api.mapper; +import java.time.Instant; + +import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigEntity; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.NullValuePropertyMappingStrategy; -import java.time.Instant; - +/** + * Mapper for device config objects. + */ @Mapper(componentModel = "cdi", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface DeviceConfigMapper { - DeviceConfigResponse deviceConfigEntityToResponse(DeviceConfigEntity entity); - - + /** + * Convert device config entity to device config. + * + * @param entity The device config entity + * @return The device config + */ + DeviceConfig deviceConfigEntityToConfig(DeviceConfigEntity entity); + + /** + * Convert device config request to device config entity. + * + * @param request The device config request + * @return The device config entity + */ @Mapping(target = "version", source = "request.versionToUpdate") @Mapping(target = "cloudUpdateTime", expression = "java(getDateTime())") DeviceConfigEntity configRequestToDeviceConfigEntity(DeviceConfigRequest request); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java index f1e7effa..4e08c1b5 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepository.java @@ -16,19 +16,37 @@ package org.eclipse.hono.communication.api.repository; -import io.vertx.core.Future; -import io.vertx.sqlclient.SqlConnection; +import java.util.List; + import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigEntity; -import java.util.List; +import io.vertx.core.Future; +import io.vertx.sqlclient.SqlConnection; /** - * Device config repository interface + * Device config repository interface. */ public interface DeviceConfigsRepository { + /** + * Lists all config versions for a specific device. Result is order by version desc + * + * @param sqlConnection The sql connection instance + * @param deviceId The device id + * @param tenantId The tenant id + * @param limit The number of config to show + * @return A Future with a List of DeviceConfigs + */ Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit); + + /** + * Creates a new config version and deletes the oldest version if the total num of versions in DB is bigger than the MAX_LIMIT. + * + * @param sqlConnection The sql connection instance + * @param entity The instance to insert + * @return A Future of the created DeviceConfigEntity + */ Future createNew(SqlConnection sqlConnection, DeviceConfigEntity entity); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java index 705540c6..f2ace5b8 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/repository/DeviceConfigsRepositoryImpl.java @@ -16,6 +16,18 @@ package org.eclipse.hono.communication.api.repository; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.hono.communication.api.data.DeviceConfig; +import org.eclipse.hono.communication.api.data.DeviceConfigEntity; +import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; +import org.eclipse.hono.communication.core.app.DatabaseConfig; +import org.graalvm.collections.Pair; + import io.vertx.core.Future; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; @@ -23,37 +35,32 @@ import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.templates.RowMapper; import io.vertx.sqlclient.templates.SqlTemplate; -import org.eclipse.hono.communication.api.data.DeviceConfig; -import org.eclipse.hono.communication.api.data.DeviceConfigEntity; -import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; -import org.eclipse.hono.communication.core.app.DatabaseConfig; -import org.graalvm.collections.Pair; - -import javax.enterprise.context.ApplicationScoped; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; /** - * Repository class for making CRUD operations for device config entities + * Repository class for making CRUD operations for device config entities. */ @ApplicationScoped public class DeviceConfigsRepositoryImpl implements DeviceConfigsRepository { - private final static String SQL_INSERT = "INSERT INTO device_configs (version, tenant_id, device_id, cloud_update_time, device_ack_time, binary_data) " + + private final String SQL_INSERT = "INSERT INTO device_configs (version, tenant_id, device_id, cloud_update_time, device_ack_time, binary_data) " + "VALUES (#{version}, #{tenantId}, #{deviceId}, #{cloudUpdateTime}, #{deviceAckTime}, #{binaryData}) RETURNING version"; - private final static String SQL_LIST = "SELECT version, cloud_update_time, device_ack_time, binary_data " + + private final String SQL_LIST = "SELECT version, cloud_update_time, device_ack_time, binary_data " + "FROM device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId} ORDER BY version DESC LIMIT #{limit}"; - private final static String SQL_DELETE_MIN_VERSION = "DELETE FROM device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId} " + + private final String SQL_DELETE_MIN_VERSION = "DELETE FROM device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId} " + "and version = (SELECT MIN(version) from device_configs WHERE device_id = #{deviceId} and tenant_id = #{tenantId}) RETURNING version"; - private final static String SQL_FIND_TOTAL_AND_MAX_VERSION = "SELECT COALESCE(COUNT(*), 0) as total, COALESCE(MAX(version), 0) as max_version from device_configs " + + private final String SQL_FIND_TOTAL_AND_MAX_VERSION = "SELECT COALESCE(COUNT(*), 0) as total, COALESCE(MAX(version), 0) as max_version from device_configs " + "WHERE device_id = #{deviceId} and tenant_id = #{tenantId}"; - private final static int MAX_LIMIT = 10; - private final static Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); + private final int MAX_LIMIT = 10; + private final Logger log = LoggerFactory.getLogger(DeviceConfigsRepositoryImpl.class); private String SQL_COUNT_DEVICES_WITH_PK_FILTER = "SELECT COUNT(*) as total FROM public.%s where %s = #{tenantId} and %s = #{deviceId}"; - public DeviceConfigsRepositoryImpl(DatabaseConfig databaseConfig) { + /** + * Creates a new DeviceConfigsRepositoryImpl. + * + * @param databaseConfig The database configs + */ + public DeviceConfigsRepositoryImpl(final DatabaseConfig databaseConfig) { SQL_COUNT_DEVICES_WITH_PK_FILTER = String.format(SQL_COUNT_DEVICES_WITH_PK_FILTER, databaseConfig.getDeviceRegistrationTableName(), @@ -62,7 +69,7 @@ public DeviceConfigsRepositoryImpl(DatabaseConfig databaseConfig) { } - private Future searchForDevice(SqlConnection sqlConnection, String deviceId, String tenantId) { + private Future searchForDevice(final SqlConnection sqlConnection, final String deviceId, final String tenantId) { final RowMapper ROW_MAPPER = row -> row.getInteger("total"); return SqlTemplate .forQuery(sqlConnection, SQL_COUNT_DEVICES_WITH_PK_FILTER) @@ -74,7 +81,7 @@ private Future searchForDevice(SqlConnection sqlConnection, String devi } - private Future> findMaxVersionAndTotalEntries(SqlConnection sqlConnection, String deviceId, String tenantId) { + private Future> findMaxVersionAndTotalEntries(final SqlConnection sqlConnection, final String deviceId, final String tenantId) { final RowMapper> ROW_MAPPER = row -> Pair.create(row.getInteger("total"), row.getInteger("max_version")); return SqlTemplate @@ -87,17 +94,9 @@ private Future> findMaxVersionAndTotalEntries(SqlConnecti } - /** - * Lists all config versions for a specific device. Result is order by version desc - * - * @param sqlConnection The sql connection instance - * @param deviceId The device id - * @param tenantId The tenant id - * @param limit The number of config to show - * @return A Future with a List of DeviceConfigs - */ - public Future> listAll(SqlConnection sqlConnection, String deviceId, String tenantId, int limit) { - int queryLimit = limit == 0 ? MAX_LIMIT : limit; + @Override + public Future> listAll(final SqlConnection sqlConnection, final String deviceId, final String tenantId, final int limit) { + final int queryLimit = limit == 0 ? MAX_LIMIT : limit; return searchForDevice(sqlConnection, deviceId, tenantId) .compose( counter -> { @@ -130,7 +129,7 @@ public Future> listAll(SqlConnection sqlConnection, String de * @param entity The instance to insert * @return A Future of the created DeviceConfigEntity */ - private Future insert(SqlConnection sqlConnection, DeviceConfigEntity entity) { + private Future insert(final SqlConnection sqlConnection, final DeviceConfigEntity entity) { return SqlTemplate .forUpdate(sqlConnection, SQL_INSERT) .mapFrom(DeviceConfigEntity.class) @@ -151,14 +150,14 @@ private Future insert(SqlConnection sqlConnection, DeviceCon } /** - * Delete the smallest config version + * Delete the smallest config version. * * @param sqlConnection The sql connection instance * @param entity The device config for searching and deleting the smallest version * @return A Future of the deleted version */ - private Future deleteMinVersion(SqlConnection sqlConnection, DeviceConfigEntity entity) { + private Future deleteMinVersion(final SqlConnection sqlConnection, final DeviceConfigEntity entity) { final RowMapper ROW_MAPPER = row -> row.getInteger("version"); return SqlTemplate .forQuery(sqlConnection, SQL_DELETE_MIN_VERSION) @@ -173,14 +172,8 @@ private Future deleteMinVersion(SqlConnection sqlConnection, DeviceConf } - /** - * Creates a new config version and deletes the oldest version if the total num of versions in DB is bigger than the MAX_LIMIT. - * - * @param sqlConnection The sql connection instance - * @param entity The instance to insert - * @return A Future of the created DeviceConfigEntity - */ - public Future createNew(SqlConnection sqlConnection, DeviceConfigEntity entity) { + @Override + public Future createNew(final SqlConnection sqlConnection, final DeviceConfigEntity entity) { return searchForDevice(sqlConnection, entity.getDeviceId(), entity.getTenantId()) .compose( counter -> { @@ -192,8 +185,8 @@ public Future createNew(SqlConnection sqlConnection, DeviceC return findMaxVersionAndTotalEntries(sqlConnection, entity.getDeviceId(), entity.getTenantId()) .compose( values -> { - int total = values.getLeft(); - int maxVersion = values.getRight(); + final int total = values.getLeft(); + final int maxVersion = values.getRight(); entity.setVersion(maxVersion + 1); @@ -208,4 +201,4 @@ public Future createNew(SqlConnection sqlConnection, DeviceC ); }).onFailure(error -> log.error(error.getMessage())); } -} \ No newline at end of file +} diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java index f880beb7..86702dd9 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreator.java @@ -17,8 +17,12 @@ package org.eclipse.hono.communication.api.service; /** - * Interface for creating Database Tables at application startup + * Interface for creating Database Tables at application startup. */ public interface DatabaseSchemaCreator { + + /** + * Create database tables. + */ void createDBTables(); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java index 60a93326..d890db0b 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImpl.java @@ -16,20 +16,22 @@ package org.eclipse.hono.communication.api.service; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.quarkus.runtime.Quarkus; import io.vertx.core.Promise; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; import io.vertx.sqlclient.templates.SqlTemplate; -import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.enterprise.context.ApplicationScoped; -import java.util.Map; /** - * Creates all Database tables if they are not exist + * Creates all Database tables if they are not exist. */ @ApplicationScoped @@ -41,11 +43,18 @@ public class DatabaseSchemaCreatorImpl implements DatabaseSchemaCreator { private final DatabaseService db; - public DatabaseSchemaCreatorImpl(Vertx vertx, DatabaseService db) { + /** + * Creates a new DatabaseSchemaCreatorImpl. + * + * @param vertx The quarkus Vertx instance + * @param db The database service + */ + public DatabaseSchemaCreatorImpl(final Vertx vertx, final DatabaseService db) { this.vertx = vertx; this.db = db; } + @Override public void createDBTables() { createDeviceConfigTable(); } @@ -64,8 +73,7 @@ private void createDeviceConfigTable() { .forQuery(sqlConnection, script) .execute(Map.of()))) .onSuccess(ok -> log.info(tableCreationSuccessMsg)) - .onFailure(error -> - { + .onFailure(error -> { log.error(tableCreationErrorMsg, error.getMessage()); db.close(); Quarkus.asyncExit(-1); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java index 09f293d4..e8e5bd87 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseService.java @@ -19,10 +19,18 @@ import io.vertx.pgclient.PgPool; /** - * Database service interface + * Database service interface. */ public interface DatabaseService { + /** + * Gets the database client instance. + * + * @return The database client + */ PgPool getDbClient(); + /** + * Closes the database connection. + */ void close(); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java index 5bda32cb..ab815ba2 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DatabaseServiceImpl.java @@ -16,48 +16,46 @@ package org.eclipse.hono.communication.api.service; +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.hono.communication.core.app.DatabaseConfig; +import org.eclipse.hono.communication.core.utils.DbUtils; + import io.vertx.core.Vertx; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.pgclient.PgPool; -import org.eclipse.hono.communication.core.app.DatabaseConfig; -import org.eclipse.hono.communication.core.utils.DbUtils; - -import javax.enterprise.context.ApplicationScoped; - /** - * Service for database + * Service for database. */ @ApplicationScoped public class DatabaseServiceImpl implements DatabaseService { - private final static String databaseConnectionOpenMsg = "Database connection is open."; - private final static String databaseConnectionCloseMsg = "Database connection is closed."; private final Logger log = LoggerFactory.getLogger(DatabaseServiceImpl.class); private final PgPool dbClient; - public DatabaseServiceImpl(DatabaseConfig databaseConfigs, Vertx vertx) { - this.dbClient = DbUtils.createDbClient(vertx, databaseConfigs); - log.debug(databaseConnectionOpenMsg); - } - /** - * Gets the database client instance. + * Creates a new DatabaseServiceImpl. * - * @return The database client + * @param databaseConfigs The database configs + * @param vertx The quarkus Vertx instance */ + public DatabaseServiceImpl(final DatabaseConfig databaseConfigs, final Vertx vertx) { + this.dbClient = DbUtils.createDbClient(vertx, databaseConfigs); + log.debug("Database connection is open."); + } + + @Override public PgPool getDbClient() { return dbClient; } - /** - * Close database connection - */ + @Override public void close() { if (this.dbClient != null) { this.dbClient.close(); - log.info(databaseConnectionCloseMsg); + log.info("Database connection is closed."); } } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java index 4716b0a0..316d47b5 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandService.java @@ -19,8 +19,14 @@ import io.vertx.ext.web.RoutingContext; /** - * Device commands interface + * Device commands interface. */ public interface DeviceCommandService { + + /** + * Post device command. + * + * @param routingContext The RoutingContext + */ void postCommand(RoutingContext routingContext); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java index f3d40fdd..a8d2709c 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceCommandServiceImpl.java @@ -16,26 +16,23 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.ext.web.RoutingContext; +import javax.enterprise.context.ApplicationScoped; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.enterprise.context.ApplicationScoped; +import io.vertx.ext.web.RoutingContext; /** - * Service for device commands + * Service for device commands. */ @ApplicationScoped public class DeviceCommandServiceImpl implements DeviceCommandService { private final Logger log = LoggerFactory.getLogger(DeviceCommandServiceImpl.class); - /** - * Handles device post commands - * - * @param routingContext The routing context - */ - public void postCommand(RoutingContext routingContext) { + @Override + public void postCommand(final RoutingContext routingContext) { // TODO publish command and send response log.info("postCommand received"); routingContext.response().setStatusCode(501).end(); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java index e1870f84..cf282858 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigService.java @@ -16,17 +16,34 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.core.Future; + +import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; +import io.vertx.core.Future; /** - * Device config interface + * Device config interface. */ public interface DeviceConfigService { - Future modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId); + /** + * Create a new device config and send it to the device. + * + * @param deviceConfig The device config + * @param deviceId The device id + * @param tenantId The tenant id + * @return Future of DeviceConfig + */ + Future modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId); + /** + * Lists all the configuration for a specific device. + * + * @param deviceId Device Id + * @param tenantId Tenant Id + * @param limit Limit between 1 and 10 + * @return Future of ListDeviceConfigVersionsResponse + */ Future listAll(String deviceId, String tenantId, int limit); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java index f2d33012..a8abca20 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImpl.java @@ -16,19 +16,18 @@ package org.eclipse.hono.communication.api.service; +import javax.enterprise.context.ApplicationScoped; -import io.vertx.core.Future; +import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; -import javax.enterprise.context.ApplicationScoped; - +import io.vertx.core.Future; /** - * Service for device commands + * Service for device commands. */ @ApplicationScoped @@ -38,33 +37,43 @@ public class DeviceConfigServiceImpl implements DeviceConfigService { private final DatabaseService db; private final DeviceConfigMapper mapper; - public DeviceConfigServiceImpl(DeviceConfigsRepository repository, - DatabaseService db, - DeviceConfigMapper mapper) { + /** + * Creates a new DeviceConfigServiceImpl with all dependencies. + * + * @param repository The DeviceConfigsRepository + * @param db The database service + * @param mapper The DeviceConfigMapper + */ + public DeviceConfigServiceImpl(final DeviceConfigsRepository repository, + final DatabaseService db, + final DeviceConfigMapper mapper) { this.repository = repository; this.db = db; this.mapper = mapper; } - public Future modifyCloudToDeviceConfig(DeviceConfigRequest deviceConfig, String deviceId, String tenantId) { + @Override + public Future modifyCloudToDeviceConfig(final DeviceConfigRequest deviceConfig, final String deviceId, final String tenantId) { - var entity = mapper.configRequestToDeviceConfigEntity(deviceConfig); + final var entity = mapper.configRequestToDeviceConfigEntity(deviceConfig); entity.setDeviceId(deviceId); entity.setTenantId(tenantId); return db.getDbClient().withTransaction( sqlConnection -> repository.createNew(sqlConnection, entity)) - .map(mapper::deviceConfigEntityToResponse); + .map(mapper::deviceConfigEntityToConfig); } - public Future listAll(String deviceId, String tenantId, int limit) { + + @Override + public Future listAll(final String deviceId, final String tenantId, final int limit) { return db.getDbClient().withConnection( sqlConnection -> repository.listAll(sqlConnection, deviceId, tenantId, limit) .map( result -> { - var listConfig = new ListDeviceConfigVersionsResponse(); + final var listConfig = new ListDeviceConfigVersionsResponse(); listConfig.setDeviceConfigs(result); return listConfig; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java index d062701e..f7909951 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/service/VertxHttpHandlerManagerService.java @@ -16,25 +16,33 @@ package org.eclipse.hono.communication.api.service; -import org.eclipse.hono.communication.api.handler.DeviceCommandsHandler; +import java.util.List; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.hono.communication.api.handler.DeviceCommandHandler; import org.eclipse.hono.communication.api.handler.DeviceConfigsHandler; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; -import javax.enterprise.context.ApplicationScoped; -import java.util.List; /** - * Provides and Manages available HTTP vertx handlers + * Provides and Manages available HTTP vertx handlers. */ @ApplicationScoped public class VertxHttpHandlerManagerService { /** - * Available vertx endpoints handler services + * Available vertx endpoints handler services. */ private final List availableHandlerServices; - public VertxHttpHandlerManagerService(DeviceConfigsHandler configHandler, DeviceCommandsHandler commandHandler) { + /** + * Creates a new VertxHttpHandlerManagerService with all dependencies. + * + * @param configHandler The configuration handler + * @param commandHandler The command handler + */ + public VertxHttpHandlerManagerService(final DeviceConfigsHandler configHandler, final DeviceCommandHandler commandHandler) { this.availableHandlerServices = List.of(configHandler, commandHandler); } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java index 23a8d33e..d4b00d3d 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/AbstractServiceApplication.java @@ -16,6 +16,16 @@ package org.eclipse.hono.communication.core.app; +import java.util.Arrays; +import java.util.Base64; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import javax.enterprise.event.Observes; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.quarkus.runtime.ShutdownEvent; import io.quarkus.runtime.StartupEvent; import io.vertx.core.Closeable; @@ -23,35 +33,33 @@ import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.cpu.CpuCoreSensor; import io.vertx.core.json.impl.JsonUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.enterprise.event.Observes; -import java.util.Arrays; -import java.util.Base64; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; /** - * Abstract Service application class + * Abstract Service application class. */ public abstract class AbstractServiceApplication { - private final Logger log = LoggerFactory.getLogger(AbstractServiceApplication.class); - /** - * The vert.x instance managed by Quarkus. + * YAML file application configurations properties. */ - protected Vertx vertx; + protected final ApplicationConfig appConfigs; /** - * YAML file application configurations properties + * The vert.x instance managed by Quarkus. */ - protected ApplicationConfig appConfigs; + protected final Vertx vertx; + private final Logger log = LoggerFactory.getLogger(AbstractServiceApplication.class); private Closeable addedVertxCloseHook; + /** + * Creates a new AbstractServiceApplication. + * + * @param vertx The quarkus Vertx instance + * @param appConfigs The application configs + */ public AbstractServiceApplication(final Vertx vertx, - ApplicationConfig appConfigs) { + final ApplicationConfig appConfigs) { this.vertx = vertx; this.appConfigs = appConfigs; } @@ -78,7 +86,7 @@ protected void logJvmDetails() { } /** - * Registers a close hook that will be notified when the Vertx instance is being closed + * Registers a close hook that will be notified when the Vertx instance is being closed. */ private void registerVertxCloseHook() { if (vertx instanceof VertxInternal vertxInternal) { @@ -132,6 +140,9 @@ protected void doStart() { // do nothing } + /** + * Do work on application stop signal. + */ protected void doStop() { // do nothing } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java index 5fc2fde0..174204bf 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ApplicationConfig.java @@ -16,29 +16,32 @@ package org.eclipse.hono.communication.core.app; -import org.eclipse.microprofile.config.inject.ConfigProperty; - import javax.inject.Singleton; +import org.eclipse.microprofile.config.inject.ConfigProperty; + /** - * Application configurations + * Application configurations. */ @Singleton public class ApplicationConfig { - private final ServerConfig serverConfig; - private final DatabaseConfig databaseConfig; - - // Application + @ConfigProperty(name = "app.version") + String version; @ConfigProperty(name = "app.name") String componentName; + private final ServerConfig serverConfig; + private final DatabaseConfig databaseConfig; - @ConfigProperty(name = "app.version") - String version; - - public ApplicationConfig(ServerConfig serverConfig, DatabaseConfig databaseConfig) { + /** + * Creates a new ApplicationConfig. + * + * @param serverConfig The server configs + * @param databaseConfig The database configs + */ + public ApplicationConfig(final ServerConfig serverConfig, final DatabaseConfig databaseConfig) { this.serverConfig = serverConfig; this.databaseConfig = databaseConfig; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java index ff9c7208..0819106a 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/DatabaseConfig.java @@ -16,12 +16,13 @@ package org.eclipse.hono.communication.core.app; +import javax.inject.Singleton; + import org.eclipse.microprofile.config.inject.ConfigProperty; -import javax.inject.Singleton; /** - * Database configurations + * Database configurations. */ @Singleton public class DatabaseConfig { diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java index a7fc4ef2..4c5ee60c 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/app/ServerConfig.java @@ -16,12 +16,13 @@ package org.eclipse.hono.communication.core.app; +import javax.inject.Singleton; + import org.eclipse.microprofile.config.inject.ConfigProperty; -import javax.inject.Singleton; /** - * Server configurations + * Server configurations. */ @Singleton public class ServerConfig { diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java index a0441fef..493f22af 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/AbstractVertxHttpServer.java @@ -16,11 +16,13 @@ package org.eclipse.hono.communication.core.http; -import io.vertx.core.Vertx; import org.eclipse.hono.communication.core.app.ApplicationConfig; +import io.vertx.core.Vertx; + + /** - * Abstract class for creating HTTP server in quarkus + * Abstract class for creating HTTP server in quarkus. * using the managed vertx api */ public abstract class AbstractVertxHttpServer { @@ -28,7 +30,13 @@ public abstract class AbstractVertxHttpServer { protected final Vertx vertx; - public AbstractVertxHttpServer(ApplicationConfig appConfigs, Vertx vertx) { + /** + * Creates a new AbstractVertxHttpServer. + * + * @param appConfigs The application configs + * @param vertx The quarkus Vertx instance + */ + public AbstractVertxHttpServer(final ApplicationConfig appConfigs, final Vertx vertx) { this.appConfigs = appConfigs; this.vertx = vertx; } diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java index cbfce4c4..3431c190 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServer.java @@ -17,17 +17,17 @@ package org.eclipse.hono.communication.core.http; /** - * HTTP server service + * HTTP server service. */ public interface HttpServer { /** - * Starts the http server + * Starts the http server. */ void start(); /** - * Stops the http server + * Stops the http server. */ void stop(); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java index 8202b152..c0167c0a 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java @@ -16,14 +16,15 @@ package org.eclipse.hono.communication.core.http; +import java.util.HashMap; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; /** - * Base HTTP service class + * Base HTTP service class. */ public class HttpServiceBase { diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java index 072b4dbc..2d21812d 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java @@ -16,30 +16,37 @@ package org.eclipse.hono.communication.core.utils; +import org.eclipse.hono.communication.core.app.DatabaseConfig; + import io.vertx.core.Vertx; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.pgclient.PgConnectOptions; import io.vertx.pgclient.PgPool; import io.vertx.sqlclient.PoolOptions; -import org.eclipse.hono.communication.core.app.DatabaseConfig; + /** - * Database utilities class + * Database utilities class. */ -public class DbUtils { +public final class DbUtils { + + static final Logger log = LoggerFactory.getLogger(DbUtils.class); + static final String connectionFailedMsg = "Failed to connect to Database: %s"; + static final String connectionSuccessMsg = "Database connection created successfully."; - final static Logger log = LoggerFactory.getLogger(DbUtils.class); - final static String connectionFailedMsg = "Failed to connect to Database: %s"; - final static String connectionSuccessMsg = "Database connection created successfully."; + private DbUtils() { + // avoid instantiation + } /** - * Build DB client that is used to manage a pool of connections + * Build DB client that is used to manage a pool of connections. * - * @param vertx Vertx context + * @param vertx The quarkus Vertx instance + * @param dbConfigs The database configs * @return PostgreSQL pool */ - public static PgPool createDbClient(Vertx vertx, DatabaseConfig dbConfigs) { + public static PgPool createDbClient(final Vertx vertx, final DatabaseConfig dbConfigs) { final PgConnectOptions connectOptions = new PgConnectOptions() @@ -50,7 +57,7 @@ public static PgPool createDbClient(Vertx vertx, DatabaseConfig dbConfigs) { .setPassword(dbConfigs.getPassword()); final PoolOptions poolOptions = new PoolOptions().setMaxSize(dbConfigs.getPoolMaxSize()); - var pool = PgPool.pool(vertx, connectOptions, poolOptions); + final var pool = PgPool.pool(vertx, connectOptions, poolOptions); pool.getConnection(connection -> { if (connection.failed()) { log.error(String.format(connectionFailedMsg, connection.cause().getMessage())); diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java index a37b6b18..f53db74a 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/ResponseUtils.java @@ -16,22 +16,33 @@ package org.eclipse.hono.communication.core.utils; +import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; + import io.vertx.core.json.Json; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.validation.BadRequestException; -import org.eclipse.hono.communication.api.exception.DeviceNotFoundException; + /** - * HTTP Response utilities class + * HTTP Response utilities class. */ public abstract class ResponseUtils { private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String APPLICATION_JSON_TYPE = "application/json"; + private ResponseUtils() { + // avoid instantiation + } - public static void successResponse(RoutingContext rc, - Object response) { + /** + * Build success response using 200 as its status code and response object as body. + * + * @param rc The routing context + * @param response The response object + */ + public static void successResponse(final RoutingContext rc, + final Object response) { rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON_TYPE) @@ -39,13 +50,13 @@ public static void successResponse(RoutingContext rc, } /** - * Build success response using 201 Created as its status code and response as its body + * Build success response using 201 Created as its status code and response object as body. * * @param rc Routing context * @param response Response body */ - public static void createdResponse(RoutingContext rc, - Object response) { + public static void createdResponse(final RoutingContext rc, + final Object response) { rc.response() .setStatusCode(201) .putHeader(CONTENT_TYPE_HEADER, APPLICATION_JSON_TYPE) @@ -53,11 +64,11 @@ public static void createdResponse(RoutingContext rc, } /** - * Build success response using 204 No Content as its status code and no body + * Build success response using 204 No Content as its status code and no response body. * * @param rc Routing context */ - public static void noContentResponse(RoutingContext rc) { + public static void noContentResponse(final RoutingContext rc) { rc.response() .setStatusCode(204) .end(); @@ -65,12 +76,12 @@ public static void noContentResponse(RoutingContext rc) { /** * Build error response using 400 Bad Request, 404 Not Found or 500 Internal Server Error - * as its status code and throwable as its body + * as its status code and throwable as its body. * * @param rc Routing context * @param error Throwable exception */ - public static void errorResponse(RoutingContext rc, Throwable error) { + public static void errorResponse(final RoutingContext rc, final Throwable error) { final int status; final String message; diff --git a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml index 20aa6466..63764c51 100644 --- a/device-communication/src/main/resources/api/hono-device-communication-v1.yaml +++ b/device-communication/src/main/resources/api/hono-device-communication-v1.yaml @@ -97,7 +97,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/DeviceConfigResponse' + $ref: '#/components/schemas/DeviceConfig' description: Device config updated successfully "400": description: Validation error or Bad request @@ -171,9 +171,9 @@ components: $ref: '#/components/schemas/DeviceConfig' example: deviceConfigs: - - object: DeviceConfigResponse + - object: DeviceConfig DeviceConfig: - title: Root Type for DeviceConfigResponse + title: Root Type for DeviceConfig description: The device configuration. type: object properties: @@ -213,30 +213,3 @@ components: cloudUpdateTime: string deviceAckTime: string binaryData: string - DeviceConfigResponse: - title: New Config version response - description: The device configuration. - type: object - properties: - version: - description: "String (int64 format) [Output only] The version - of this update. The version number is assigned by the server, and is - always greater than 0 after device creation. The version must be 0 on - the devices.create request if a config is specified; the response of - devices.create will always have a value of 1." - type: string - cloudUpdateTime: - description: "String (Timestamp format) [Output only] The time at - which this configuration version was updated in Cloud IoT Core. This - timestamp is set by the server. Timestamp in - RFC3339 UTC \"Zulu\" format, accurate to nanoseconds. - Example: \"2014-10-02T15:01:23.045123456Z\"." - type: string - binaryData: - description: "string (bytes format) The device configuration data - in string base64-encoded format." - type: string - example: - version: string - cloudUpdateTime: string - binaryData: string diff --git a/device-communication/src/main/resources/api/hono-endpoint.yaml b/device-communication/src/main/resources/api/hono-endpoint.yaml index e8c29a34..e2e1491b 100644 --- a/device-communication/src/main/resources/api/hono-endpoint.yaml +++ b/device-communication/src/main/resources/api/hono-endpoint.yaml @@ -73,7 +73,7 @@ definitions: request if a config is specified; the response of devices.create will always have a value of 1. type: string - title: Root Type for DeviceConfigResponse + title: Root Type for DeviceConfig type: object DeviceConfigRequest: description: Request body for modifying device configs @@ -87,40 +87,12 @@ definitions: required: - binaryData type: object - DeviceConfigResponse: - description: The device configuration. - example: - binaryData: string - cloudUpdateTime: string - version: string - properties: - binaryData: - description: >- - string (bytes format) The device configuration data in string - base64-encoded format. - type: string - cloudUpdateTime: - description: >- - String (Timestamp format) [Output only] The time at which this - configuration version was updated in Cloud IoT Core. This timestamp is - set by the server. Timestamp in RFC3339 UTC "Zulu" format, accurate - to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". - type: string - version: - description: >- - String (int64 format) [Output only] The version of this update. The - version number is assigned by the server, and is always greater than 0 - after device creation. The version must be 0 on the devices.create - request if a config is specified; the response of devices.create will - always have a value of 1. - type: string - title: New Config version response - type: object + ListDeviceConfigVersionsResponse: description: A list of a device config versions example: deviceConfigs: - - object: DeviceConfigResponse + - object: DeviceConfig properties: deviceConfigs: description: List of DeviceConfig objects @@ -221,7 +193,7 @@ paths: "200": description: Device config updated successfully schema: - $ref: "#/definitions/DeviceConfigResponse" + $ref: "#/definitions/DeviceConfig" "400": description: Validation error or Bad request "404": diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java index 53425a75..c45b74ed 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/ApplicationTest.java @@ -16,14 +16,21 @@ package org.eclipse.hono.communication.api; -import io.vertx.core.Vertx; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + import org.eclipse.hono.communication.core.app.ApplicationConfig; import org.eclipse.hono.communication.core.http.HttpServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.mockito.Mockito.*; + +import io.vertx.core.Vertx; + class ApplicationTest { @@ -33,8 +40,8 @@ class ApplicationTest { @BeforeEach void setUp() { httpServerMock = mock(HttpServer.class); - Vertx vertxMock = mock(Vertx.class); - ApplicationConfig appConfigs = mock(ApplicationConfig.class); + final Vertx vertxMock = mock(Vertx.class); + final ApplicationConfig appConfigs = mock(ApplicationConfig.class); application = new Application(vertxMock, appConfigs, httpServerMock); verifyNoMoreInteractions(httpServerMock, appConfigs, vertxMock); @@ -53,4 +60,4 @@ void doStart() { verify(httpServerMock, times(1)).start(); } -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java index b7ddf227..c25a29db 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServerTest.java @@ -16,32 +16,42 @@ package org.eclipse.hono.communication.api; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + + +import java.util.List; + +import org.eclipse.hono.communication.api.handler.DeviceCommandHandler; +import org.eclipse.hono.communication.api.service.DatabaseSchemaCreator; +import org.eclipse.hono.communication.api.service.DatabaseSchemaCreatorImpl; +import org.eclipse.hono.communication.api.service.DatabaseService; +import org.eclipse.hono.communication.api.service.DatabaseServiceImpl; +import org.eclipse.hono.communication.api.service.VertxHttpHandlerManagerService; +import org.eclipse.hono.communication.core.app.ApplicationConfig; +import org.eclipse.hono.communication.core.app.ServerConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import io.netty.handler.codec.http.HttpHeaderNames; import io.quarkus.runtime.Quarkus; import io.vertx.core.Future; import io.vertx.core.Vertx; -import io.vertx.core.http.*; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Route; import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.openapi.RouterBuilder; import io.vertx.ext.web.validation.BadRequestException; -import org.eclipse.hono.communication.api.handler.DeviceCommandsHandler; -import org.eclipse.hono.communication.api.service.*; -import org.eclipse.hono.communication.core.app.ApplicationConfig; -import org.eclipse.hono.communication.core.app.ServerConfig; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - -import java.util.List; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; class DeviceCommunicationHttpServerTest { @@ -111,7 +121,7 @@ void tearDown() { @Test void startSucceeded() { - var mockedCommandService = mock(DeviceCommandsHandler.class); + final var mockedCommandService = mock(DeviceCommandHandler.class); Mockito.verifyNoMoreInteractions(mockedCommandService); doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { @@ -138,7 +148,7 @@ void startSucceeded() { when(routerMock.route(any())).thenReturn(routeMock); try (MockedStatic quarkusMockedStatic = mockStatic(Quarkus.class)) { - DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); + final DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); deviceCommunicationHttpServerSpy.start(); @@ -186,7 +196,7 @@ void startSucceeded() { @Test void createRouterFailed() { - var mockedCommandService = mock(DeviceCommandsHandler.class); + final var mockedCommandService = mock(DeviceCommandHandler.class); Mockito.verifyNoMoreInteractions(mockedCommandService); doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { @@ -216,7 +226,7 @@ void createRouterFailed() { @Test void createServerFailed() { - var mockedCommandService = mock(DeviceCommandsHandler.class); + final var mockedCommandService = mock(DeviceCommandHandler.class); Mockito.verifyNoMoreInteractions(mockedCommandService); doNothing().when(mockedCommandService).addRoutes(this.routerBuilderMock); try (MockedStatic mockedRouterBuilderStatic = mockStatic(RouterBuilder.class)) { @@ -241,13 +251,13 @@ void createServerFailed() { when(serverConfigMock.getOpenApiFilePath()).thenReturn("/myPath"); when(serverConfigMock.getBasePath()).thenReturn("/basePath"); when(routerMock.route(any())).thenReturn(routeMock); - DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); + final DeviceCommunicationHttpServer deviceCommunicationHttpServerSpy = spy(this.deviceCommunicationHttpServer); deviceCommunicationHttpServerSpy.start(); verify(deviceCommunicationHttpServerSpy, times(1)).createRouterWithEndpoints(eq(routerBuilderMock), any()); verify(deviceCommunicationHttpServerSpy, times(1)).startVertxServer(any()); - + mockedRouterBuilderStatic.verify(() -> RouterBuilder.create(vertxMock, "/myPath"), times(1)); @@ -284,8 +294,8 @@ void createServerFailed() { @Test void addDefault400ExceptionHandler() { - var errorMsg = "This is an error message"; - int code = 400; + final var errorMsg = "This is an error message"; + final int code = 400; when(routingContextMock.failure()).thenReturn(badRequestExceptionMock); when(badRequestExceptionMock.toJson()).thenReturn(jsonObjMock); @@ -307,8 +317,8 @@ void addDefault400ExceptionHandler() { @Test void addDefault404ExceptionHandlerPutsHeader() { - var errorMsg = "This is an error message"; - int code = 404; + final var errorMsg = "This is an error message"; + final int code = 404; when(routingContextMock.response()).thenReturn(httpServerResponseMock); when(httpServerResponseMock.putHeader(eq(HttpHeaderNames.CONTENT_TYPE), @@ -352,8 +362,8 @@ void addDefault404ExceptionHandlerResponseEnded() { @Test void addDefault404ExceptionHandlerMethodEqualsHead() { - var errorMsg = "This is an error message"; - int code = 404; + final var errorMsg = "This is an error message"; + final int code = 404; when(routingContextMock.response()).thenReturn(httpServerResponseMock); when(httpServerResponseMock.ended()).thenReturn(Boolean.FALSE); @@ -383,4 +393,4 @@ void stop() { deviceCommunicationHttpServer.stop(); verify(dbMock).close(); } -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java index ee96c36a..533259d2 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceCommandsHandlerTest.java @@ -16,9 +16,9 @@ package org.eclipse.hono.communication.api.handler; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.Operation; -import io.vertx.ext.web.openapi.RouterBuilder; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + import org.eclipse.hono.communication.api.config.DeviceCommandConstants; import org.eclipse.hono.communication.api.service.DeviceCommandService; import org.eclipse.hono.communication.api.service.DeviceCommandServiceImpl; @@ -26,8 +26,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.Operation; +import io.vertx.ext.web.openapi.RouterBuilder; + class DeviceCommandsHandlerTest { @@ -35,14 +37,14 @@ class DeviceCommandsHandlerTest { private final RouterBuilder routerBuilderMock; private final RoutingContext routingContextMock; private final Operation operationMock; - private final DeviceCommandsHandler deviceCommandsHandler; + private final DeviceCommandHandler deviceCommandsHandler; - public DeviceCommandsHandlerTest() { + DeviceCommandsHandlerTest() { operationMock = mock(Operation.class); commandServiceMock = mock(DeviceCommandServiceImpl.class); routerBuilderMock = mock(RouterBuilder.class); routingContextMock = mock(RoutingContext.class); - deviceCommandsHandler = new DeviceCommandsHandler(commandServiceMock); + deviceCommandsHandler = new DeviceCommandHandler(commandServiceMock); } @AfterEach @@ -85,5 +87,4 @@ void handlePostCommand() { verify(commandServiceMock, times(1)).postCommand(routingContextMock); } - -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java index 081a71f1..e1a65770 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/handler/DeviceConfigsHandlerTest.java @@ -16,19 +16,15 @@ package org.eclipse.hono.communication.api.handler; -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.json.Json; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RequestBody; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.openapi.Operation; -import io.vertx.ext.web.openapi.RouterBuilder; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.util.List; + import org.eclipse.hono.communication.api.config.DeviceConfigsConstants; import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.service.DeviceConfigService; import org.eclipse.hono.communication.api.service.DeviceConfigServiceImpl; @@ -37,11 +33,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RequestBody; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.openapi.Operation; +import io.vertx.ext.web.openapi.RouterBuilder; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; class DeviceConfigsHandlerTest { @@ -58,11 +59,11 @@ class DeviceConfigsHandlerTest { private final String tenantID = "tenant_ID"; private final String deviceID = "device_ID"; private final String errorMsg = "test_error"; - DeviceConfigRequest deviceConfigRequest = new DeviceConfigRequest("1", "binary_data"); - DeviceConfigResponse deviceConfigEntity = new DeviceConfigResponse(); - DeviceConfig deviceConfig = new DeviceConfig(); + private final DeviceConfigRequest deviceConfigRequest = new DeviceConfigRequest("1", "binary_data"); + private final DeviceConfig deviceConfigEntity = new DeviceConfig(); + private final DeviceConfig deviceConfig = new DeviceConfig(); - public DeviceConfigsHandlerTest() { + DeviceConfigsHandlerTest() { operationMock = mock(Operation.class); configServiceMock = mock(DeviceConfigServiceImpl.class); routerBuilderMock = mock(RouterBuilder.class); @@ -73,7 +74,7 @@ public DeviceConfigsHandlerTest() { illegalArgumentExceptionMock = mock(IllegalArgumentException.class); deviceConfigsHandler = new DeviceConfigsHandler(configServiceMock); - deviceConfigEntity.setVersion(1); + deviceConfigEntity.setVersion("1"); deviceConfig.setVersion(""); @@ -123,7 +124,7 @@ void handleModifyCloudToDeviceConfig_success() { when(httpServerResponseMock.putHeader("Content-Type", "application/json")).thenReturn(httpServerResponseMock); - var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); + final var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); @@ -150,7 +151,7 @@ void handleModifyCloudToDeviceConfig_failure() { when(httpServerResponseMock.putHeader("Content-Type", "application/json")).thenReturn(httpServerResponseMock); - var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); + final var results = deviceConfigsHandler.handleModifyCloudToDeviceConfig(routingContextMock); verify(configServiceMock).modifyCloudToDeviceConfig(deviceConfigRequest, deviceID, tenantID); verify(routingContextMock, times(1)).pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS); @@ -167,8 +168,8 @@ void handleModifyCloudToDeviceConfig_failure() { @Test void handleListConfigVersions_success() { - ListDeviceConfigVersionsResponse listDeviceConfigVersionsResponse = new ListDeviceConfigVersionsResponse(List.of(deviceConfig)); - MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); + final var listDeviceConfigVersionsResponse = new ListDeviceConfigVersionsResponse(List.of(deviceConfig)); + final MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); when(routingContextMock.queryParams()).thenReturn(queryParams); when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); @@ -178,7 +179,7 @@ void handleListConfigVersions_success() { "application/json")).thenReturn(httpServerResponseMock); when(configServiceMock.listAll(deviceID, tenantID, 10)).thenReturn(Future.succeededFuture(listDeviceConfigVersionsResponse)); - var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); + final var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); verify(routingContextMock, times(1)).queryParams(); @@ -192,7 +193,7 @@ void handleListConfigVersions_success() { @Test void handleListConfigVersions_failed() { - MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); + final MultiMap queryParams = MultiMap.caseInsensitiveMultiMap().add(DeviceConfigsConstants.NUM_VERSION_QUERY_PARAMS, String.valueOf(10)); when(routingContextMock.queryParams()).thenReturn(queryParams); when(routingContextMock.pathParam(DeviceConfigsConstants.TENANT_PATH_PARAMS)).thenReturn(tenantID); when(routingContextMock.pathParam(DeviceConfigsConstants.DEVICE_PATH_PARAMS)).thenReturn(deviceID); @@ -203,7 +204,7 @@ void handleListConfigVersions_failed() { "application/json")).thenReturn(httpServerResponseMock); when(configServiceMock.listAll(deviceID, tenantID, 10)).thenReturn(Future.failedFuture(illegalArgumentExceptionMock)); - var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); + final var results = deviceConfigsHandler.handleListConfigVersions(routingContextMock); verify(configServiceMock, times(1)).listAll(deviceID, tenantID, 10); verify(routingContextMock, times(1)).queryParams(); @@ -216,7 +217,7 @@ void handleListConfigVersions_failed() { } - void verifyErrorResponse(Future results) { + void verifyErrorResponse(final Future results) { verify(routingContextMock, times(1)).response(); verify(httpServerResponseMock).setStatusCode(400); verify(illegalArgumentExceptionMock).getMessage(); @@ -226,7 +227,7 @@ void verifyErrorResponse(Future results) { Assertions.assertTrue(results.failed()); } - void verifySuccessResponse(Future results, Object responseObj) { + void verifySuccessResponse(final Future results, final Object responseObj) { verify(routingContextMock, times(1)).response(); verify(httpServerResponseMock).setStatusCode(200); verify(httpServerResponseMock).putHeader("Content-Type", @@ -234,4 +235,4 @@ void verifySuccessResponse(Future results, Object responseObj) { verify(httpServerResponseMock).end(Json.encodePrettily(responseObj)); Assertions.assertTrue(results.succeeded()); } -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java index 8b818edc..24c9cb8d 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseSchemaCreatorImplTest.java @@ -16,17 +16,19 @@ package org.eclipse.hono.communication.api.service; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.file.FileSystem; import io.vertx.pgclient.PgPool; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; class DatabaseSchemaCreatorImplTest { @@ -88,4 +90,4 @@ void createDBTables_failed() { verify(pgPoolMock).withTransaction(any()); } -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java index 33025dbd..886c2259 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DatabaseServiceImplTest.java @@ -16,8 +16,8 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.core.Vertx; -import io.vertx.pgclient.PgPool; +import static org.mockito.Mockito.*; + import org.eclipse.hono.communication.core.app.DatabaseConfig; import org.eclipse.hono.communication.core.utils.DbUtils; import org.junit.jupiter.api.AfterEach; @@ -25,7 +25,9 @@ import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import static org.mockito.Mockito.*; +import io.vertx.core.Vertx; +import io.vertx.pgclient.PgPool; + class DatabaseServiceImplTest { @@ -35,7 +37,7 @@ class DatabaseServiceImplTest { private final PgPool pgPoolMock; private DatabaseService databaseService; - public DatabaseServiceImplTest() { + DatabaseServiceImplTest() { vertxMock = mock(Vertx.class); databaseConfigMock = mock(DatabaseConfig.class); pgPoolMock = mock(PgPool.class); @@ -56,7 +58,7 @@ void getDbClient() { dbUtilsMockedStatic.when(() -> DbUtils.createDbClient(vertxMock, databaseConfigMock)).thenReturn(pgPoolMock); databaseService = new DatabaseServiceImpl(databaseConfigMock, vertxMock); - var client = databaseService.getDbClient(); + final var client = databaseService.getDbClient(); Assertions.assertSame(client, pgPoolMock); @@ -78,4 +80,4 @@ void close() { dbUtilsMockedStatic.verifyNoMoreInteractions(); } } -} \ No newline at end of file +} diff --git a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java index a8099eb3..56b767fa 100644 --- a/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java +++ b/device-communication/src/test/java/org/eclipse/hono/communication/api/service/DeviceConfigServiceImplTest.java @@ -16,11 +16,11 @@ package org.eclipse.hono.communication.api.service; -import io.vertx.core.Future; -import io.vertx.pgclient.PgPool; +import static org.mockito.Mockito.*; + +import org.eclipse.hono.communication.api.data.DeviceConfig; import org.eclipse.hono.communication.api.data.DeviceConfigEntity; import org.eclipse.hono.communication.api.data.DeviceConfigRequest; -import org.eclipse.hono.communication.api.data.DeviceConfigResponse; import org.eclipse.hono.communication.api.data.ListDeviceConfigVersionsResponse; import org.eclipse.hono.communication.api.mapper.DeviceConfigMapper; import org.eclipse.hono.communication.api.repository.DeviceConfigsRepository; @@ -29,7 +29,9 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import static org.mockito.Mockito.*; +import io.vertx.core.Future; +import io.vertx.pgclient.PgPool; + class DeviceConfigServiceImplTest { @@ -42,7 +44,7 @@ class DeviceConfigServiceImplTest { private final String tenantId = "tenant_ID"; private final String deviceId = "device_ID"; - public DeviceConfigServiceImplTest() { + DeviceConfigServiceImplTest() { this.repositoryMock = mock(DeviceConfigsRepositoryImpl.class); this.dbMock = mock(DatabaseServiceImpl.class); this.mapperMock = mock(DeviceConfigMapper.class); @@ -58,19 +60,19 @@ void tearDown() { @Test void modifyCloudToDeviceConfig_success() { - var deviceConfigRequest = new DeviceConfigRequest(); - var deviceConfigEntity = new DeviceConfigEntity(); - var deviceConfigEntityResponse = new DeviceConfigResponse(); + final var deviceConfigRequest = new DeviceConfigRequest(); + final var deviceConfigEntity = new DeviceConfigEntity(); + final var deviceConfigEntityResponse = new DeviceConfig(); when(mapperMock.configRequestToDeviceConfigEntity(deviceConfigRequest)).thenReturn(deviceConfigEntity); - when(mapperMock.deviceConfigEntityToResponse(deviceConfigEntity)).thenReturn(deviceConfigEntityResponse); + when(mapperMock.deviceConfigEntityToConfig(deviceConfigEntity)).thenReturn(deviceConfigEntityResponse); when(dbMock.getDbClient()).thenReturn(poolMock); when(poolMock.withTransaction(any())).thenReturn(Future.succeededFuture(deviceConfigEntity)); - var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); + final var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); verify(mapperMock, times(1)).configRequestToDeviceConfigEntity(deviceConfigRequest); - verify(mapperMock, times(1)).deviceConfigEntityToResponse(deviceConfigEntity); + verify(mapperMock, times(1)).deviceConfigEntityToConfig(deviceConfigEntity); verify(dbMock, times(1)).getDbClient(); verify(poolMock, times(1)).withTransaction(any()); Assertions.assertTrue(results.succeeded()); @@ -78,14 +80,14 @@ void modifyCloudToDeviceConfig_success() { @Test void modifyCloudToDeviceConfig_failure() { - var deviceConfigRequest = new DeviceConfigRequest(); - var deviceConfigEntity = new DeviceConfigEntity(); + final var deviceConfigRequest = new DeviceConfigRequest(); + final var deviceConfigEntity = new DeviceConfigEntity(); when(mapperMock.configRequestToDeviceConfigEntity(deviceConfigRequest)).thenReturn(deviceConfigEntity); when(dbMock.getDbClient()).thenReturn(poolMock); when(poolMock.withTransaction(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); - var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); + final var results = deviceConfigService.modifyCloudToDeviceConfig(deviceConfigRequest, deviceId, tenantId); verify(mapperMock, times(1)).configRequestToDeviceConfigEntity(deviceConfigRequest); verify(dbMock, times(1)).getDbClient(); @@ -95,11 +97,11 @@ void modifyCloudToDeviceConfig_failure() { @Test void listAll_success() { - var deviceConfigVersions = new ListDeviceConfigVersionsResponse(); + final var deviceConfigVersions = new ListDeviceConfigVersionsResponse(); when(dbMock.getDbClient()).thenReturn(poolMock); when(poolMock.withConnection(any())).thenReturn(Future.succeededFuture(deviceConfigVersions)); - var results = deviceConfigService.listAll(deviceId, tenantId, 10); + final var results = deviceConfigService.listAll(deviceId, tenantId, 10); verify(dbMock, times(1)).getDbClient(); verify(poolMock, times(1)).withConnection(any()); @@ -114,7 +116,7 @@ void listAll_failed() { when(dbMock.getDbClient()).thenReturn(poolMock); when(poolMock.withConnection(any())).thenReturn(Future.failedFuture(new Throwable("test_error"))); - var results = deviceConfigService.listAll(deviceId, tenantId, 10); + final var results = deviceConfigService.listAll(deviceId, tenantId, 10); verify(dbMock, times(1)).getDbClient(); verify(poolMock, times(1)).withConnection(any()); @@ -122,4 +124,4 @@ void listAll_failed() { } -} \ No newline at end of file +} From 1edb541e78adee50538859934a0fbe86b5ed3d44 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Mon, 27 Feb 2023 13:41:41 +0100 Subject: [PATCH 07/18] replace System.exit with Quarkus.asyncExit --- .../org/eclipse/hono/communication/core/utils/DbUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java index 2d21812d..45568ce2 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/core/utils/DbUtils.java @@ -18,6 +18,7 @@ import org.eclipse.hono.communication.core.app.DatabaseConfig; +import io.quarkus.runtime.Quarkus; import io.vertx.core.Vertx; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; @@ -61,8 +62,7 @@ public static PgPool createDbClient(final Vertx vertx, final DatabaseConfig dbCo pool.getConnection(connection -> { if (connection.failed()) { log.error(String.format(connectionFailedMsg, connection.cause().getMessage())); - System.exit(-1); - + Quarkus.asyncExit(-1); } else { log.info(connectionSuccessMsg); } From 2629393616fc23aa32d1cec75879a7f170cc97b4 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Mon, 27 Feb 2023 13:50:26 +0100 Subject: [PATCH 08/18] set default quarkus.container-image.push to false --- device-communication/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-communication/src/main/resources/application.yaml b/device-communication/src/main/resources/application.yaml index fea04fc8..0503a293 100644 --- a/device-communication/src/main/resources/application.yaml +++ b/device-communication/src/main/resources/application.yaml @@ -32,5 +32,5 @@ quarkus: container-image: builder: docker build: true - push: true + push: false image: "gcr.io/sotec-iot-core-dev/hono-device-communication" From 2f72d02f3c03fa9460df9142a26f009daa079fbb Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 14 Mar 2023 15:07:48 +0100 Subject: [PATCH 09/18] remove class HttpServiceBase --- .../core/http/HttpServiceBase.java | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java b/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java deleted file mode 100644 index c0167c0a..00000000 --- a/device-communication/src/main/java/org/eclipse/hono/communication/core/http/HttpServiceBase.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * *********************************************************** - * Copyright (c) 2023 Contributors to the Eclipse Foundation - *

- * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - *

- * 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.eclipse.hono.communication.core.http; - -import java.util.HashMap; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -/** - * Base HTTP service class. - */ -public class HttpServiceBase { - - protected final Logger log = LoggerFactory.getLogger(getClass()); - private final Map endpoints = new HashMap<>(); - - -} From cdb2455a83411e1ddb4ed233114f6af416a71a01 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 14 Mar 2023 15:10:15 +0100 Subject: [PATCH 10/18] remove check style for auto generated mapper impl --- device-communication/pom.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/device-communication/pom.xml b/device-communication/pom.xml index 23fa49a4..608b7c64 100644 --- a/device-communication/pom.xml +++ b/device-communication/pom.xml @@ -196,6 +196,10 @@ checkstyle/default.xml checkstyle/suppressions.xml true + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + From 562b26d4b18c9a35c417dbc8255b16ad741cdadf Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 14 Mar 2023 15:14:17 +0100 Subject: [PATCH 11/18] fix typo Throws to throws --- .../hono/communication/api/DeviceCommunicationHttpServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index 24e78682..6269b463 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -204,7 +204,7 @@ void startVertxServer(final Router router) { * Adds status code 400 and sets the error message for the routingContext response. * * @param routingContext the routing context object - * @Throws: NullPointerException – if routingContext is {@code null}. + * @throws: NullPointerException – if routingContext is {@code null}. */ void addDefault400ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); @@ -217,7 +217,7 @@ void addDefault400ExceptionHandler(final RoutingContext routingContext) { * Adds status code 404 and sets the error message for the routingContext response. * * @param routingContext the routing context object - * @Throws: NullPointerException – if routingContext is {@code null}. + * @throws: NullPointerException – if routingContext is {@code null}. */ void addDefault404ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); From 74de95e99dbdcad654a9302af518abe9078aed2b Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Mon, 27 Mar 2023 09:25:10 +0200 Subject: [PATCH 12/18] remove colon from java docs --- .../hono/communication/api/DeviceCommunicationHttpServer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index 6269b463..aa7e004b 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -204,7 +204,7 @@ void startVertxServer(final Router router) { * Adds status code 400 and sets the error message for the routingContext response. * * @param routingContext the routing context object - * @throws: NullPointerException – if routingContext is {@code null}. + * @throws NullPointerException – if routingContext is {@code null}. */ void addDefault400ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); @@ -217,7 +217,7 @@ void addDefault400ExceptionHandler(final RoutingContext routingContext) { * Adds status code 404 and sets the error message for the routingContext response. * * @param routingContext the routing context object - * @throws: NullPointerException – if routingContext is {@code null}. + * @throws NullPointerException – if routingContext is {@code null}. */ void addDefault404ExceptionHandler(final RoutingContext routingContext) { Objects.requireNonNull(routingContext); From 6e92b86c610c0880208ece72cf6024745849025f Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Mon, 3 Apr 2023 10:33:18 +0200 Subject: [PATCH 13/18] exclude quarkus-ide-launcher --- device-communication/pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/device-communication/pom.xml b/device-communication/pom.xml index 608b7c64..16a757ab 100644 --- a/device-communication/pom.xml +++ b/device-communication/pom.xml @@ -34,6 +34,12 @@ io.quarkus quarkus-arc + + + io.quarkus + quarkus-ide-launcher + + io.quarkus From 2b93c9227236167f3d04ba50528b001d8fd3c4de Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 25 Apr 2023 09:30:00 +0200 Subject: [PATCH 14/18] remove leading slash from openapi file --- device-communication/src/main/resources/application.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-communication/src/main/resources/application.yaml b/device-communication/src/main/resources/application.yaml index 0503a293..1b15d64f 100644 --- a/device-communication/src/main/resources/application.yaml +++ b/device-communication/src/main/resources/application.yaml @@ -3,7 +3,7 @@ app: version: ${COM_APP_VERSION:"v1"} vertx: openapi: - file: ${COM_OPENAPI_FILE_PATH:/api/hono-device-communication-v1.yaml} + file: ${COM_OPENAPI_FILE_PATH:api/hono-device-communication-v1.yaml} server: url: ${COM_SERVER_HOST:0.0.0.0} From 21af4149305876c5d4ad86a94ca90d910688b18b Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 25 Apr 2023 14:26:46 +0200 Subject: [PATCH 15/18] fix log error --- .../hono/communication/api/DeviceCommunicationHttpServer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index aa7e004b..2e42fcd5 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -98,7 +98,7 @@ public void start() { }) .onFailure(error -> { if (error != null) { - log.error(error.getMessage()); + log.error("Can not create Router {}", error.getMessage()); } else { log.error("Can not create Router"); } From 7e3ce102f3cb4a680e4907b07108f90ca9fcb8b8 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 25 Apr 2023 14:38:50 +0200 Subject: [PATCH 16/18] remove router error handlers --- .../communication/api/DeviceCommunicationHttpServer.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java index 2e42fcd5..5cc13001 100644 --- a/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java +++ b/device-communication/src/main/java/org/eclipse/hono/communication/api/DeviceCommunicationHttpServer.java @@ -30,7 +30,6 @@ import org.eclipse.hono.communication.core.http.AbstractVertxHttpServer; import org.eclipse.hono.communication.core.http.HttpEndpointHandler; import org.eclipse.hono.communication.core.http.HttpServer; -import org.eclipse.hono.communication.core.utils.ResponseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -125,10 +124,6 @@ Router createRouterWithEndpoints(final RouterBuilder routerBuilder, final List - ResponseUtils.errorResponse(routingContext, routingContext.failure())); - apiRouter.errorHandler(404, routingContext -> - ResponseUtils.errorResponse(routingContext, routingContext.failure())); final var serverConfig = appConfigs.getServerConfig(); addHealthCheckHandlers(apiRouter, serverConfig); From 733ad6973e3eccd0a19fe80074f4a2533fa1e375 Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 25 Apr 2023 14:43:49 +0200 Subject: [PATCH 17/18] add hint for post command --- device-communication/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-communication/README.md b/device-communication/README.md index 681e5895..507b14ab 100644 --- a/device-communication/README.md +++ b/device-communication/README.md @@ -18,7 +18,7 @@ router. #### commands/{tenantId}/{deviceId} -- POST : post a command for a specific device +- POST : post a command for a specific device (NOT IMPLEMENTED YET)

From 8c87c8011a3812c6817ff26ea350a635afd9d95d Mon Sep 17 00:00:00 2001 From: "g.dimitropoulos" Date: Tue, 25 Apr 2023 15:01:13 +0200 Subject: [PATCH 18/18] make mvnw executable --- device-communication/mvnw | 1 + 1 file changed, 1 insertion(+) diff --git a/device-communication/mvnw b/device-communication/mvnw index eaa3d308..b69732dc 100644 --- a/device-communication/mvnw +++ b/device-communication/mvnw @@ -314,3 +314,4 @@ exec "$JAVACMD" \ "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +