diff --git a/.github/workflows/build-dev-release.yml b/.github/workflows/build-dev-release.yml new file mode 100644 index 0000000..86f34c1 --- /dev/null +++ b/.github/workflows/build-dev-release.yml @@ -0,0 +1,18 @@ +name: Build and publish dev release jar to sonatype repository + +on: workflow_dispatch + +jobs: + build: + uses: th2-net/.github/.github/workflows/compound-java.yml@main + with: + build-target: 'Sonatype,Docker' + devRelease: true + createTag: true + docker-username: ${{ github.actor }} + secrets: + docker-password: ${{ secrets.GITHUB_TOKEN }} + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000..a72a337 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,18 @@ +name: Build and publish release jar to sonatype repository + +on: workflow_dispatch + +jobs: + build: + uses: th2-net/.github/.github/workflows/compound-java.yml@main + with: + build-target: 'Sonatype,Docker' + devRelease: false + createTag: true + docker-username: ${{ github.actor }} + secrets: + docker-password: ${{ secrets.GITHUB_TOKEN }} + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/build-sanpshot.yml b/.github/workflows/build-sanpshot.yml new file mode 100644 index 0000000..46c237d --- /dev/null +++ b/.github/workflows/build-sanpshot.yml @@ -0,0 +1,23 @@ +name: Build and publish jar to sonatype snapshot repository + +on: + push: + branches-ignore: + - master + - version-* + - dependabot* + paths-ignore: + - README.md + +jobs: + build-job: + uses: th2-net/.github/.github/workflows/compound-java-dev.yml@main + with: + build-target: 'Sonatype,Docker' + docker-username: ${{ github.actor }} + secrets: + docker-password: ${{ secrets.GITHUB_TOKEN }} + sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + sonatypeSigningKey: ${{ secrets.SONATYPE_GPG_ARMORED_KEY }} + sonatypeSigningPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }} \ No newline at end of file diff --git a/.github/workflows/dev-docker-publish.yml b/.github/workflows/dev-docker-publish.yml deleted file mode 100644 index 10a95ab..0000000 --- a/.github/workflows/dev-docker-publish.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Dev build and publish Docker distributions to Github Container Registry ghcr.io - -on: - push: - branches-ignore: - - master - - version-* - - dependabot** - paths-ignore: - - README.md - - gradle.properties - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - # Prepare custom build version - - name: Get branch name - id: branch - run: echo ::set-output name=branch_name::${GITHUB_REF#refs/*/} - - name: Get release_version - id: ver - uses: christian-draeger/read-properties@1.1.1 - with: - path: gradle.properties - properties: release_version - - name: Get SHA of the commit - id: sha - run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Build custom release version - id: release_ver - run: echo value="${{ steps.ver.outputs.release_version }}-${{ steps.branch.outputs.branch_name }}-${{ github.run_id }}-${{ steps.sha.outputs.sha_short }}" >> $GITHUB_OUTPUT - - name: Show custom release version - run: echo ${{ steps.release_ver.outputs.value }} - # Build and publish image - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - run: echo "::set-output name=REPOSITORY_NAME::$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" - id: meta - - name: Build and push - id: docker_build - uses: docker/build-push-action@v3 - with: - push: true - tags: ghcr.io/${{ github.repository }}:${{ steps.release_ver.outputs.value }} - labels: com.exactpro.th2.${{ steps.meta.outputs.REPOSITORY_NAME }}=${{ steps.ver.outputs.value }} - build-args: | - release_version=${{ steps.read_property.outputs.value }} \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index 3d8706e..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build and publish Docker distributions to Github Container Registry ghcr.io - -on: - push: - branches: - - master - - version-* - paths: - - gradle.properties - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - run: echo "REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_OUTPUT - id: meta - - name: Read version from gradle.properties - id: read_property - uses: christian-draeger/read-properties@1.1.1 - with: - path: ./gradle.properties - properties: release_version - - name: Build and push - id: docker_build - uses: docker/build-push-action@v3 - with: - push: true - tags: ghcr.io/${{ github.repository }}:${{ steps.read_property.outputs.release_version }} - labels: com.exactpro.th2.${{ steps.meta.outputs.REPOSITORY_NAME }}=${{ steps.read_property.outputs.release_version }} - build-args: | - release_version=${{ steps.read_property.outputs.value }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4d80c26..e5ac521 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,9 +14,9 @@ RUN apk add --no-cache make git gcc musl-dev swig \ -I../src -L../src libwebp_java_wrap.c -lwebp -o libwebp.so FROM gradle:7.6-jdk11 AS build -ARG app_version=0.0.0 +ARG release_version=0.0.0 COPY ./ . -RUN gradle dockerPrepare -Prelease_version=${app_version} +RUN gradle dockerPrepare -Prelease_version=${release_version} FROM adoptopenjdk/openjdk11:alpine WORKDIR /home diff --git a/README.md b/README.md index 6708e6e..1d7070d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 hand (4.0.0) +# th2 hand (5.0.0) th2-hand is used to interpret and transmit commands from th2-act to Selenium or Windows Application Driver and vice versa. All incoming and outgoing data is stored in Cradle as messages. @@ -28,7 +28,15 @@ This project uses the Schema API to get its settings. For local run it needs `custom.json`, `grpc.json`, `rabbitMQ.json` and `mq.json` files. The `custom.json` file contains RemoteHand URLs map and has the following format: -- **session-alias == "th2-hand" by default** + +- **session-alias / sessionAlias == "th2-hand" by default** - hand publishes messages related to UI command under this session alias +- **screenshot-session-alias / screenshotSessionAlias = "th2-hand-screenshot" by default** - hand publishes messages with a screenshot under this session alias +- **session-group / sessionGroup = "th2-hand-group" by default** - hand publishes all messages under this session group. +- **message-batch-limit / messageBatchLimit = 1048576 by default** - limit size for batching messages. +- **drivers-mapping / driversMapping** - UI drivers settings. +- **rh-options / rhOptions** - remote hand options settings. +- **response-timeout-sec / responseTimeoutSec = 120 by default** - timeout for waiting result form remote hand. +- **use-transport / useTransport = true by default** - if true, hand used th2 transport protocol to publish messages via MQ. ``` { "session-alias": "aliasName", @@ -92,6 +100,24 @@ Example of `rabbitMQ.json`: ## Release Notes +### 5.0.0 + ++ Added th2 transport support + +#### Updated lib: ++ bom: `4.5.0` ++ common: `5.8.0-dev` ++ grpc-hand: `3.0.0-dev` + +#### Added lib: ++ common-utils: `2.2.2-dev` + +#### Added plugin: ++ org.owasp.dependencycheck: `9.0.9` ++ com.gorylenko.gradle-git-properties: `2.4.1` ++ com.github.jk1.dependency-license-report: `2.5` ++ de.undercouch.download: `5.4.0` + ### 4.0.0 + Migrated to Books & Pages concept @@ -106,9 +132,9 @@ Example of `rabbitMQ.json`: ### 3.1.0 -+ reads dictionaries from the /var/th2/config/dictionary folder -+ uses mq_router, grpc_router, cradle_manager optional JSON configs from the /var/th2/config folder -+ tries to load log4j.properties files from sources in order: '/var/th2/config', '/home/etc', configured path via cmd, default configuration ++ reads dictionaries from the /var/th2/context/dictionary folder ++ uses mq_router, grpc_router, cradle_manager optional JSON configs from the /var/th2/context folder ++ tries to load log4j.properties files from sources in order: '/var/th2/context', '/home/etc', configured path via cmd, default configuration + update Cradle version. Introduce async API for storing events + removed gRPC event loop handling + fixed dictionary reading \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0276079..0c4f2e8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /****************************************************************************** - * Copyright 2009-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2009-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,23 @@ * limitations under the License. ******************************************************************************/ +import com.github.jk1.license.filter.LicenseBundleNormalizer +import com.github.jk1.license.render.JsonReportRenderer + plugins { id 'java' id 'java-library' id 'application' - id 'com.palantir.docker' version '0.34.0' + id 'maven-publish' + id 'org.jetbrains.kotlin.jvm' version '1.8.22' + id "io.github.gradle-nexus.publish-plugin" version "1.3.0" + id 'com.palantir.docker' version '0.36.0' + id "org.owasp.dependencycheck" version "9.1.0" + id "com.gorylenko.gradle-git-properties" version "2.4.1" + id 'com.github.jk1.dependency-license-report' version '2.5' + id "de.undercouch.download" version "5.6.0" + id 'signing' + id "com.google.protobuf" version "0.9.4" } group 'com.exactpro.th2' @@ -45,20 +57,133 @@ repositories { } dependencies { - api platform('com.exactpro.th2:bom:4.1.0') + api platform('com.exactpro.th2:bom:4.6.0') - implementation('com.exactpro.remotehand:remotehand:1.7.3-TH2-4662-4046816762-SNAPSHOT') { - exclude group: "org.slf4j", module: "slf4j-log4j12" + implementation('com.exactpro.remotehand:remotehand:1.8.0-dev') + implementation("com.exactpro.th2:grpc-hand:3.0.0-dev") { + exclude group: "com.google.guava", module: "guava" // for compatibility with Selenium 3.141.59 } - implementation("com.exactpro.th2:grpc-hand:2.11.0-TH2-3884-2590730423-SNAPSHOT") { + implementation("com.exactpro.th2:common:5.10.0-dev") { exclude group: "com.google.guava", module: "guava" // for compatibility with Selenium 3.141.59 } - implementation("com.exactpro.th2:common:5.0.0-dev-version-5-3838510969-SNAPSHOT") { + implementation("com.exactpro.th2:common-utils:2.2.2-dev") { exclude group: "com.google.guava", module: "guava" // for compatibility with Selenium 3.141.59 } + implementation 'org.slf4j:slf4j-api' + + implementation "com.fasterxml.jackson.core:jackson-core" + implementation "com.fasterxml.jackson.core:jackson-databind" + implementation "com.fasterxml.jackson.core:jackson-annotations" + implementation 'org.apache.commons:commons-lang3' - implementation "org.apache.commons:commons-csv:1.9.0" + implementation "org.apache.commons:commons-csv:1.10.0" + + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1' + testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5' + testImplementation 'io.strikt:strikt-core:0.34.1' +} + +java { + withJavadocJar() + withSourcesJar() +} + +test { + useJUnitPlatform() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions.jvmTarget = "11" + kotlinOptions.freeCompilerArgs += "-Xjvm-default=all" +} + +// conditionals for publications +tasks.withType(PublishToMavenRepository).configureEach { + onlyIf { + (repository == publishing.repositories.nexusRepository && + project.hasProperty('nexus_user') && + project.hasProperty('nexus_password') && + project.hasProperty('nexus_url')) || + (repository == publishing.repositories.sonatype && + project.hasProperty('sonatypeUsername') && + project.hasProperty('sonatypePassword')) + } +} +tasks.withType(Sign).configureEach { + onlyIf { + project.hasProperty('signingKey') && + project.hasProperty('signingPassword') + } +} +// disable running task 'initializeSonatypeStagingRepository' on a gitlab +tasks.configureEach { task -> + if (task.name == 'initializeSonatypeStagingRepository' && + !(project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) + ) { + task.enabled = false + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + from(components.java) + pom { + name = rootProject.name + packaging = 'jar' + description = rootProject.description + url = vcs_url + scm { + url = vcs_url + } + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'developer' + name = 'developer' + email = 'developer@exactpro.com' + } + } + scm { + url = vcs_url + } + } + } + } + repositories { + //Nexus repo to publish from gitlab + maven { + name = 'nexusRepository' + credentials { + username = project.findProperty('nexus_user') + password = project.findProperty('nexus_password') + } + url = project.findProperty('nexus_url') + } + } +} + +nexusPublishing { + repositories { + sonatype { + nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) + snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + } + } +} + +signing { + String signingKey = findProperty("signingKey") + String signingPassword = findProperty("signingPassword") + useInMemoryPgpKeys(signingKey, signingPassword) + sign publishing.publications.mavenJava } applicationName = 'service' @@ -83,8 +208,6 @@ jar { archivesBaseName = applicationName manifest { attributes('Specification-Title': 'TH2 Hand') - attributes('Main-Class': 'com.exactpro.th2.hand.Application') - attributes("Class-Path": configurations.compileClasspath.collect { "lib/" + it.getName() }.join(' ')) attributes( 'Created-By': "${System.getProperty('java.version')} (${System.getProperty('java.vendor')})", 'Specification-Title': '', @@ -95,4 +218,36 @@ jar { 'Implementation-Version': project.version ) } +} + +dependencyCheck { + formats = ['SARIF', 'JSON', 'HTML'] + failBuildOnCVSS = 5 + + analyzers { + assemblyEnabled = false + nugetconfEnabled = false + nodeEnabled = false + } +} + +licenseReport { + def licenseNormalizerBundlePath = "$buildDir/license-normalizer-bundle.json" + + if (!file(licenseNormalizerBundlePath).exists()) { + download.run { + src 'https://raw.githubusercontent.com/th2-net/.github/main/license-compliance/gradle-license-report/license-normalizer-bundle.json' + dest "$buildDir/license-normalizer-bundle.json" + overwrite false + } + } + + filters = [ + new LicenseBundleNormalizer(licenseNormalizerBundlePath, false) + ] + renderers = [ + new JsonReportRenderer('licenses.json', false), + ] + excludeOwnGroup = false + allowedLicensesFile = new URL("https://raw.githubusercontent.com/th2-net/.github/main/license-compliance/gradle-license-report/allowed-licenses.json") } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index def873a..c8ad482 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,4 @@ -release_version = 4.0.0 -docker_image_name = th2-hand \ No newline at end of file +release_version = 5.0.0 +docker_image_name = th2-hand + +vcs_url=https://github.com/th2-net/th2-hand \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ad7440b..0671300 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index bd149a8..2201784 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -rootProject.name = 'th2-hand' +rootProject.name = 'hand' diff --git a/src/main/java/com/exactpro/th2/hand/Application.java b/src/main/java/com/exactpro/th2/hand/Application.java index 8e9e5a2..4a28d31 100644 --- a/src/main/java/com/exactpro/th2/hand/Application.java +++ b/src/main/java/com/exactpro/th2/hand/Application.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,10 @@ package com.exactpro.th2.hand; +import com.exactpro.th2.common.schema.factory.CommonFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.exactpro.th2.common.schema.factory.CommonFactory; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; - public class Application { private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); @@ -32,35 +28,30 @@ public static void main(String[] args) { new Application().run(args); } - public long getCurrentTime() { - Instant now = Instant.now(); - return TimeUnit.SECONDS.toNanos(now.getEpochSecond()) + now.getNano(); - } - public void run(String[] args) { try (CommonFactory factory = CommonFactory.createFromArguments(args)) { - Config config = getConfig(factory); - final HandServer handServer = new HandServer(config, getCurrentTime()); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - try { - LOGGER.info("Disposing Hand server"); - handServer.dispose(); - } - catch (Exception e) { - LOGGER.error("Error while disposing Hand server", e); - } - })); - handServer.start(); - handServer.blockUntilShutdown(); - } - catch (Exception e) { + Context context = getConfig(factory); + try (HandServer handServer = new HandServer(context)) { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOGGER.info("*** Closing hand server because JVM is shutting down"); + try { + handServer.close(); + } catch (InterruptedException e) { + LOGGER.warn("Server termination await was interrupted", e); + } + LOGGER.info("*** hand server closed"); + })); + handServer.start(); + handServer.blockUntilShutdown(); + } + } catch (Exception e) { LOGGER.error("Could not to start Hand server", e); closeApp(); } } - protected Config getConfig(CommonFactory factory) throws ConfigurationException { - return new Config(factory); + protected Context getConfig(CommonFactory factory) throws ConfigurationException { + return new Context(factory); } private static void closeApp() { diff --git a/src/main/java/com/exactpro/th2/hand/Config.java b/src/main/java/com/exactpro/th2/hand/Context.java similarity index 78% rename from src/main/java/com/exactpro/th2/hand/Config.java rename to src/main/java/com/exactpro/th2/hand/Context.java index 2b0f09e..867672f 100644 --- a/src/main/java/com/exactpro/th2/hand/Config.java +++ b/src/main/java/com/exactpro/th2/hand/Context.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,14 +24,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -public class Config { +public class Context { protected final CommonFactory factory; protected final CustomConfiguration customConfiguration; - public Config(CommonFactory factory) throws ConfigurationException { - this.factory = factory; - this.customConfiguration = factory.getCustomConfiguration(CustomConfiguration.class); + public Context(CommonFactory factory, CustomConfiguration customConfiguration) throws ConfigurationException { if (customConfiguration == null) { throw new ConfigurationException("Custom configuration is not found"); } @@ -39,12 +37,23 @@ public Config(CommonFactory factory) throws ConfigurationException { if (customConfiguration.getDriversMapping().isEmpty()) { throw new ConfigurationException("Drivers mapping should be provided in custom config."); } + + this.factory = factory; + this.customConfiguration = customConfiguration; + } + + public Context(CommonFactory factory) throws ConfigurationException { + this(factory, factory.getCustomConfiguration(CustomConfiguration.class)); } public CommonFactory getFactory() { return factory; } + public CustomConfiguration getCustomConfiguration() { + return customConfiguration; + } + public Map getDriversMapping() { return customConfiguration.getDriversMapping(); } @@ -65,10 +74,16 @@ public String getSessionGroup() { return customConfiguration.getSessionGroup(); } + public String getBook() { + return factory.getBoxConfiguration().getBookName(); + } + public String getScreenshotSessionAlias() { return customConfiguration.getScreenshotSessionAlias(); } + public boolean isUseTransport() { return customConfiguration.isUseTransport(); } + public static class DriverMapping { public final RemoteManagerType type; public final String url; diff --git a/src/main/java/com/exactpro/th2/hand/HandServer.java b/src/main/java/com/exactpro/th2/hand/HandServer.java index 290e69c..1576df1 100644 --- a/src/main/java/com/exactpro/th2/hand/HandServer.java +++ b/src/main/java/com/exactpro/th2/hand/HandServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,19 +27,20 @@ import java.util.List; import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; -public class HandServer { - private final Logger logger = LoggerFactory.getLogger(getClass()); +public class HandServer implements AutoCloseable { + private static final Logger LOGGER = LoggerFactory.getLogger(HandServer.class); - private final Config config; + private final Context context; private final MessageHandler messageHandler; private final Server server; private final List services; + private final AtomicReference watcher = new AtomicReference<>(); - public HandServer(Config config, long startSequences) throws Exception { - this.config = config; - this.messageHandler = new MessageHandler(config, new AtomicLong(startSequences)); + public HandServer(Context context) throws Exception { + this.context = context; + this.messageHandler = new MessageHandler(context); this.services = new ArrayList<>(); this.server = buildServer(); } @@ -48,38 +49,31 @@ protected Server buildServer() throws Exception { for (IHandService rhService : ServiceLoader.load(IHandService.class)) { services.add(rhService); rhService.init(messageHandler); - logger.info("Service '{}' loaded", rhService.getClass().getName()); + LOGGER.info("Service '{}' loaded", rhService.getClass().getName()); } - return config.getFactory().getGrpcRouter().startServer(services.toArray(new IHandService[0])); + return context.getFactory().getGrpcRouter().startServer(services.toArray(new IHandService[0])); } - /** Start serving requests. */ + /** + * Start serving requests. + * @throws IOException - if unable to bind + */ public void start() throws IOException { - new Thread(SessionWatcher.getWatcher()).start(); - server.start(); - logger.info("Server started, listening on port {}", server.getPort()); - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - logger.info("*** shutting down gRPC server because JVM is shutting down"); - try { - stop(); - } catch (InterruptedException e) { - logger.warn("Server termination await was interrupted", e); - } - logger.info("*** server shut down"); - })); - } - - /** Stop serving requests and shutdown resources. */ - public void stop() throws InterruptedException { - if (server != null) { - server.shutdown().awaitTermination(30, TimeUnit.SECONDS); + Thread thread = new Thread(SessionWatcher.getWatcher()); + if (watcher.compareAndSet(null, thread)) { + thread.start(); + server.start(); + LOGGER.info("Server started, listening on port {}", server.getPort()); + } else { + throw new IllegalStateException(getClass().getSimpleName() + " is already started"); } } /** * Await termination on the main thread since the grpc library uses daemon * threads. + * @throws InterruptedException - if current thread is interrupted */ public void blockUntilShutdown() throws InterruptedException { if (server != null) { @@ -87,9 +81,20 @@ public void blockUntilShutdown() throws InterruptedException { } } - public void dispose() { + @Override + public void close() throws InterruptedException { + if (!server.isShutdown()) { + server.shutdown().awaitTermination(30, TimeUnit.SECONDS); + } + for (IHandService service : this.services) { service.dispose(); } + + Thread thread = watcher.get(); + if (thread != null && !thread.isInterrupted()) { + thread.interrupt(); + thread.join(30_000); + } } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/RhConnectionManager.java b/src/main/java/com/exactpro/th2/hand/RhConnectionManager.java index a98c91c..2388040 100644 --- a/src/main/java/com/exactpro/th2/hand/RhConnectionManager.java +++ b/src/main/java/com/exactpro/th2/hand/RhConnectionManager.java @@ -33,12 +33,12 @@ public class RhConnectionManager { private final Map sessions = new ConcurrentHashMap<>(); private final GridRemoteHandManager gridRemoteHandManager; - private final Config config; + private final Context context; - public RhConnectionManager(Config config) { - this.config = config; + public RhConnectionManager(Context context) { + this.context = context; gridRemoteHandManager = new GridRemoteHandManager(); - gridRemoteHandManager.createConfigurations(null, config.getRhOptions()); + gridRemoteHandManager.createConfigurations(null, context.getRhOptions()); } public HandSessionHandler getSessionHandler(String sessionId) throws IllegalArgumentException { @@ -49,7 +49,7 @@ public HandSessionHandler getSessionHandler(String sessionId) throws IllegalArgu } public HandSessionHandler createSessionHandler(String targetServer) throws RhConfigurationException { - Config.DriverMapping driverSettings = config.getDriversMapping().get(targetServer); + Context.DriverMapping driverSettings = context.getDriversMapping().get(targetServer); if (driverSettings == null) { throw new RhConfigurationException("Unrecognized targetServer: " + targetServer); } diff --git a/src/main/java/com/exactpro/th2/hand/builders/events/DefaultEventBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/events/DefaultEventBuilder.java index 613a047..4060c7d 100644 --- a/src/main/java/com/exactpro/th2/hand/builders/events/DefaultEventBuilder.java +++ b/src/main/java/com/exactpro/th2/hand/builders/events/DefaultEventBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,104 +20,116 @@ import com.exactpro.th2.act.grpc.hand.ResultDetails; import com.exactpro.th2.act.grpc.hand.RhActionsBatch; import com.exactpro.th2.act.grpc.hand.RhBatchResponse; -import com.exactpro.th2.common.grpc.Event; -import com.exactpro.th2.common.grpc.EventID; -import com.exactpro.th2.common.grpc.EventStatus; +import com.exactpro.th2.common.event.bean.IRow; +import com.exactpro.th2.common.event.bean.Message; +import com.exactpro.th2.common.event.bean.Table; +import com.exactpro.th2.common.event.Event; +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration; import com.exactpro.th2.common.schema.factory.CommonFactory; import com.exactpro.th2.hand.messages.responseexecutor.ActionsBatchExecutorResponse; -import org.apache.commons.lang3.StringUtils; +import java.io.IOException; import java.time.Instant; -import java.util.List; -import java.util.Map; -import java.util.LinkedHashMap; +import java.util.ArrayList; import java.util.Iterator; -import java.util.UUID; +import java.util.List; import java.util.stream.Collectors; -import static com.exactpro.th2.hand.utils.Utils.getTimestamp; +import static com.exactpro.th2.common.event.Event.Status.FAILED; +import static com.exactpro.th2.common.event.Event.Status.PASSED; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; -public final class DefaultEventBuilder implements EventBuilder { +public final class DefaultEventBuilder implements EventBuilder { + public static final Message ACTION_MESSAGES_EVENT_MESSAGE = createMessage("Action messages"); + public static final Message RESULT_EVENT_MESSAGE = createMessage("Result"); - private final CommonFactory factory; + private final String book; + private final String scope; public DefaultEventBuilder(CommonFactory factory) { - this.factory = factory; + BoxConfiguration boxConfiguration = factory.getBoxConfiguration(); + this.book = boxConfiguration.getBookName(); + this.scope = boxConfiguration.getBoxName(); } @Override - public Event buildEvent(RhActionsBatch request, ActionsBatchExecutorResponse executorResponse) { - return buildEvent(Instant.now(), request, executorResponse); - } + public com.exactpro.th2.common.grpc.Event buildEvent(Instant startTime, RhActionsBatch request, ActionsBatchExecutorResponse executorResponse) throws IOException { + RhScriptResult scriptResult = executorResponse.getScriptResult(); + RhBatchResponse response = executorResponse.getHandResponse(); - @Override - public Event buildEvent(Instant startTime, RhActionsBatch request, ActionsBatchExecutorResponse executorResponse) { - EventID.Builder eventId = factory.newEventIDBuilder() - .setId(UUID.randomUUID().toString()) - .setStartTimestamp(getTimestamp(startTime)); + Event builder = Event.from(startTime) + .name(request.getEventName()) + .status(scriptResult.isSuccess() ? PASSED : FAILED); + + createAdditionalEventInfo(builder, request.getAdditionalEventInfo()); + createResultPayload(builder, scriptResult, response.getSessionId(), response.getScriptStatus()); + createActionMessagesPayload(builder, request.getStoreActionMessages(), response.getResultList()); - Event.Builder eventBuilder = Event.newBuilder().setName(request.getEventName()); + executorResponse.getMessageIds().forEach(builder::messageID); if (request.hasParentEventId()) { - eventId.setScope(request.getParentEventId().getScope()); - eventBuilder.setParentId(request.getParentEventId()); + return builder.toProto(request.getParentEventId()); + } else { + return builder.toProto(book, scope); } - - eventBuilder.setId(eventId); - - RhScriptResult scriptResult = executorResponse.getScriptResult(); - RhBatchResponse response = executorResponse.getHandResponse(); - eventBuilder.setStatus(scriptResult.isSuccess() ? EventStatus.SUCCESS : EventStatus.FAILED); - - EventPayloadBuilder payloadBuilder = new EventPayloadBuilder(); - createAdditionalEventInfo(payloadBuilder, request.getAdditionalEventInfo()); - createResultPayload(payloadBuilder, scriptResult, response.getSessionId(), response.getScriptStatus()); - createActionMessagesPayload(payloadBuilder, request.getStoreActionMessages(), response.getResultList()); - eventBuilder.setBody(payloadBuilder.toByteString()); - - eventBuilder.addAllAttachedMessageIds(executorResponse.getMessageIds()); - eventBuilder.setEndTimestamp(getTimestamp(Instant.now())); - - return eventBuilder.build(); } - private void createResultPayload(EventPayloadBuilder payloadBuilder, RhScriptResult scriptResult, String sessionId, - RhBatchResponse.ScriptExecutionStatus scriptStatus) { - Map responseMap = new LinkedHashMap<>(); - responseMap.put("Action status", scriptStatus.name()); - String errorMessage; - if (StringUtils.isNotEmpty(errorMessage = scriptResult.getErrorMessage())) - responseMap.put("Errors", errorMessage); - responseMap.put("SessionId", sessionId); + private void createResultPayload(com.exactpro.th2.common.event.Event builder, RhScriptResult scriptResult, String sessionId, + RhBatchResponse.ScriptExecutionStatus scriptStatus) { + List rows = new ArrayList<>(); + rows.add(new TableRow("Action status", scriptStatus.name())); - payloadBuilder.printTable("Result", responseMap); + String errorMessage = scriptResult.getErrorMessage(); + if (isNotEmpty(errorMessage)) { + rows.add(new TableRow("Errors", errorMessage)); + } + rows.add(new TableRow("SessionId", sessionId)); + + builder.bodyData(RESULT_EVENT_MESSAGE); + builder.bodyData(createTable(rows)); } - private void createActionMessagesPayload(EventPayloadBuilder payloadBuilder, boolean storeActionMessages, - List resultDetails) { + private void createActionMessagesPayload(com.exactpro.th2.common.event.Event builder, boolean storeActionMessages, + List resultDetails) { if (storeActionMessages && !resultDetails.isEmpty()) { - Map actionMessages = resultDetails.stream().collect( - Collectors.toMap(ResultDetails::getActionId, ResultDetails::getResult)); - payloadBuilder.printTable("Action messages", actionMessages); + List rows = resultDetails.stream() + .map(result -> new TableRow(result.getActionId(), result.getResult())) + .collect(Collectors.toList()); + builder.bodyData(ACTION_MESSAGES_EVENT_MESSAGE); + builder.bodyData(createTable(rows)); } } - private void createAdditionalEventInfo(EventPayloadBuilder payloadBuilder, RhActionsBatch.AdditionalEventInfo info) { + private void createAdditionalEventInfo(com.exactpro.th2.common.event.Event builder, RhActionsBatch.AdditionalEventInfo info) { String description = info.getDescription(); if (!description.isEmpty()) { - payloadBuilder.printText("Description: \n" + description); + builder.bodyData(createMessage("Description: \n" + description)); } if (info.getPrintTable()) { - Map table = new LinkedHashMap<>(info.getKeysCount()); + List rows = new ArrayList<>(); Iterator keys = info.getKeysList().iterator(); Iterator values = info.getValuesList().iterator(); while (keys.hasNext() && values.hasNext()) { - table.put(keys.next(), values.next()); + rows.add(new TableRow(keys.next(), values.next())); } - payloadBuilder.printTable(info.getRequestParamsTableTitle(), table); + builder.bodyData(createMessage(info.getRequestParamsTableTitle())); + builder.bodyData(createTable(rows)); } } + + private static Message createMessage(String text) { + Message message = new Message(); + message.setData(text); + return message; + } + + private static Table createTable(List rows) { + Table table = new Table(); + table.setFields(rows); + return table; + } + } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/builders/events/EventBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/events/EventBuilder.java index f6685de..6a3c500 100644 --- a/src/main/java/com/exactpro/th2/hand/builders/events/EventBuilder.java +++ b/src/main/java/com/exactpro/th2/hand/builders/events/EventBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,9 @@ import com.exactpro.th2.hand.messages.responseexecutor.BaseExecutorResponse; +import java.io.IOException; import java.time.Instant; public interface EventBuilder> { - T buildEvent(R request, E executorResponse); - - T buildEvent(Instant startTime, R request, E executorResponse); + T buildEvent(Instant startTime, R request, E executorResponse) throws IOException; } diff --git a/src/main/java/com/exactpro/th2/hand/builders/events/EventPayloadBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/events/EventPayloadBuilder.java deleted file mode 100644 index 3523f24..0000000 --- a/src/main/java/com/exactpro/th2/hand/builders/events/EventPayloadBuilder.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2021-2021 Exactpro (Exactpro Systems Limited) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.exactpro.th2.hand.builders.events; - -import com.exactpro.th2.hand.messages.eventpayload.EventPayloadMessage; -import com.exactpro.th2.hand.messages.eventpayload.EventPayloadTable; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.protobuf.ByteString; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class EventPayloadBuilder { - - private static final Logger logger = LoggerFactory.getLogger(EventPayloadBuilder.class); - - private final List data; - - public EventPayloadBuilder() { - this.data = new ArrayList<>(); - } - - public EventPayloadBuilder printText(String text) { - this.data.add(new EventPayloadMessage(text)); - return this; - } - - public EventPayloadBuilder printText(String title, String text) { - this.data.add(new EventPayloadMessage(title)); - this.data.add(new EventPayloadMessage(text)); - return this; - } - - public EventPayloadBuilder printTable(String tableHeader, Map table) { - this.data.add(new EventPayloadMessage(tableHeader)); - this.data.add(new EventPayloadTable(table, false)); - return this; - } - - - public byte[] toByteArray() { - try { - return new ObjectMapper().writeValueAsBytes(this.data); - } catch (JsonProcessingException e) { - logger.error("Error while creating body", e); - return e.getMessage().getBytes(StandardCharsets.UTF_8); - } - } - - public ByteString toByteString() { - return ByteString.copyFrom(toByteArray()); - } - -} diff --git a/src/main/java/com/exactpro/th2/hand/builders/mstore/MessageStoreBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/mstore/MessageStoreBuilder.java index 9113bbe..4c0835f 100644 --- a/src/main/java/com/exactpro/th2/hand/builders/mstore/MessageStoreBuilder.java +++ b/src/main/java/com/exactpro/th2/hand/builders/mstore/MessageStoreBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,14 @@ package com.exactpro.th2.hand.builders.mstore; import com.exactpro.th2.common.grpc.Direction; +import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Path; import java.util.Map; public interface MessageStoreBuilder { + ObjectMapper MAPPER = new ObjectMapper(); + T buildMessage(Map fields, Direction direction, String sessionId, String sessionGroup); T buildMessage(byte[] bytes, Direction direction, String sessionId, String sessionGroup); diff --git a/src/main/java/com/exactpro/th2/hand/builders/mstore/DefaultMessageStoreBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/mstore/ProtobufMessageStoreBuilder.java similarity index 85% rename from src/main/java/com/exactpro/th2/hand/builders/mstore/DefaultMessageStoreBuilder.java rename to src/main/java/com/exactpro/th2/hand/builders/mstore/ProtobufMessageStoreBuilder.java index 08849e1..1acc9bc 100644 --- a/src/main/java/com/exactpro/th2/hand/builders/mstore/DefaultMessageStoreBuilder.java +++ b/src/main/java/com/exactpro/th2/hand/builders/mstore/ProtobufMessageStoreBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,13 +18,12 @@ import com.exactpro.remotehand.Configuration; import com.exactpro.th2.common.grpc.ConnectionID; +import com.exactpro.th2.common.grpc.Direction; import com.exactpro.th2.common.grpc.MessageID; import com.exactpro.th2.common.grpc.RawMessage; import com.exactpro.th2.common.grpc.RawMessageMetadata; -import com.exactpro.th2.common.grpc.Direction; import com.exactpro.th2.common.schema.factory.CommonFactory; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.protobuf.ByteString; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,14 +38,13 @@ import static com.exactpro.th2.hand.utils.Utils.getTimestamp; -public final class DefaultMessageStoreBuilder implements MessageStoreBuilder { - private static final Logger logger = LoggerFactory.getLogger(DefaultMessageStoreBuilder.class); +public final class ProtobufMessageStoreBuilder implements MessageStoreBuilder { + private static final Logger LOGGER = LoggerFactory.getLogger(ProtobufMessageStoreBuilder.class); - private final ObjectMapper mapper = new ObjectMapper(); private final AtomicLong seqNum; private final CommonFactory factory; - public DefaultMessageStoreBuilder(CommonFactory factory, AtomicLong seqNum) { + public ProtobufMessageStoreBuilder(CommonFactory factory, AtomicLong seqNum) { this.factory = factory; this.seqNum = seqNum; } @@ -54,10 +52,10 @@ public DefaultMessageStoreBuilder(CommonFactory factory, AtomicLong seqNum) { @Override public RawMessage buildMessage(Map fields, Direction direction, String sessionId, String sessionGroup) { try { - byte[] bytes = mapper.writeValueAsBytes(fields); + byte[] bytes = MAPPER.writeValueAsBytes(fields); return buildMessage(bytes, direction, sessionId, sessionGroup); } catch (JsonProcessingException e) { - logger.error("Could not encode message as JSON", e); + LOGGER.error("Could not encode message as JSON", e); return null; } } @@ -76,7 +74,7 @@ public RawMessage buildMessageFromFile(Path path, Direction direction, String se try (InputStream is = Files.newInputStream(path)) { return RawMessage.newBuilder().setMetadata(messageMetadata).setBody(ByteString.readFrom(is, 0x1000)).build(); } catch (IOException e) { - logger.error("Cannot encode screenshot", e); + LOGGER.error("Cannot encode screenshot", e); return null; } } diff --git a/src/main/java/com/exactpro/th2/hand/builders/mstore/TransportMessageStoreBuilder.java b/src/main/java/com/exactpro/th2/hand/builders/mstore/TransportMessageStoreBuilder.java new file mode 100644 index 0000000..b616baf --- /dev/null +++ b/src/main/java/com/exactpro/th2/hand/builders/mstore/TransportMessageStoreBuilder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.builders.mstore; + +import com.exactpro.remotehand.Configuration; +import com.exactpro.th2.common.grpc.Direction; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.MessageId; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.RawMessage; +import com.fasterxml.jackson.core.JsonProcessingException; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import static com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.TransportUtilsKt.getTransport; + +public final class TransportMessageStoreBuilder implements MessageStoreBuilder { + private static final Logger LOGGER = LoggerFactory.getLogger(TransportMessageStoreBuilder.class); + + private final AtomicLong seqNum; + + public TransportMessageStoreBuilder(AtomicLong seqNum) { + this.seqNum = seqNum; + } + + @Override + public RawMessage buildMessage(Map fields, Direction direction, String sessionId, String sessionGroup) { + try { + byte[] bytes = MAPPER.writeValueAsBytes(fields); + return buildMessage(bytes, direction, sessionId, sessionGroup); + } catch (JsonProcessingException e) { + LOGGER.error("Could not encode message as JSON", e); + return null; + } + } + + @Override + public RawMessage buildMessage(byte[] bytes, Direction direction, String sessionId, String sessionGroup) { + RawMessage.Builder builder = RawMessage.builder() + .setId(MessageId.builder() + .setSessionAlias(sessionId) + .setDirection(getTransport(direction)) + .setSequence(seqNum.incrementAndGet()) + .setTimestamp(Instant.now()) + .build()) + .setBody(bytes); + return builder.build(); + } + + @Override + public RawMessage buildMessageFromFile(Path path, Direction direction, String sessionId, String sessionGroup) { + String protocol = "image/" + Configuration.getInstance().getDefaultScreenWriter().getScreenshotExtension(); + + try (InputStream is = Files.newInputStream(path)) { + int length = Math.toIntExact(path.toFile().length()); + ByteBuf buffer = Unpooled.buffer(length); + buffer.writeBytes(is, length); + + return RawMessage.builder() + .setId(MessageId.builder() + .setSessionAlias(sessionId) + .setDirection(getTransport(direction)) + .setSequence(seqNum.incrementAndGet()) + .setTimestamp(Instant.now()) + .build()) + .setProtocol(protocol) + .setBody(buffer) + .build(); + } catch (IOException e) { + LOGGER.error("Cannot encode screenshot", e); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/requestexecutors/ActionsBatchExecutor.java b/src/main/java/com/exactpro/th2/hand/requestexecutors/ActionsBatchExecutor.java index 21ae671..de29d03 100644 --- a/src/main/java/com/exactpro/th2/hand/requestexecutors/ActionsBatchExecutor.java +++ b/src/main/java/com/exactpro/th2/hand/requestexecutors/ActionsBatchExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -60,7 +61,7 @@ public ActionsBatchExecutor(MessageHandler messageHandler) { @Override - public ActionsBatchExecutorResponse execute(RhActionsBatch request) { + public ActionsBatchExecutorResponse execute(RhActionsBatch request) throws IOException { Instant executionStartTime = Instant.now(); RhScriptResult scriptResult; String sessionId = "th2_hand"; @@ -140,7 +141,7 @@ private List parseResultDetails(List actionData) { return details; } - private void buildAndSendEvent(Instant startTime, RhActionsBatch request, ActionsBatchExecutorResponse executorResponse) { + private void buildAndSendEvent(Instant startTime, RhActionsBatch request, ActionsBatchExecutorResponse executorResponse) throws IOException { EventStoreHandler eventStoreHandler = messageHandler.getEventStoreHandler(); Event event = eventStoreHandler.getEventBuilder().buildEvent(startTime, request, executorResponse); eventStoreHandler.getEventStoreSender().storeEvent(event); diff --git a/src/main/java/com/exactpro/th2/hand/requestexecutors/RequestExecutor.java b/src/main/java/com/exactpro/th2/hand/requestexecutors/RequestExecutor.java index 5d08c1d..6f1d5e2 100644 --- a/src/main/java/com/exactpro/th2/hand/requestexecutors/RequestExecutor.java +++ b/src/main/java/com/exactpro/th2/hand/requestexecutors/RequestExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ import com.exactpro.th2.hand.messages.responseexecutor.BaseExecutorResponse; +import java.io.IOException; + public interface RequestExecutor> { - Res execute(Req request); + Res execute(Req request) throws IOException; } diff --git a/src/main/java/com/exactpro/th2/hand/schema/CustomConfiguration.java b/src/main/java/com/exactpro/th2/hand/schema/CustomConfiguration.java index 5dc9258..f540ed4 100644 --- a/src/main/java/com/exactpro/th2/hand/schema/CustomConfiguration.java +++ b/src/main/java/com/exactpro/th2/hand/schema/CustomConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,35 +19,50 @@ import java.util.Collections; import java.util.Map; -import com.exactpro.th2.hand.Config; +import com.exactpro.th2.hand.Context; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class CustomConfiguration { + private static final String DEFAULT_SESSION_GROUP = "th2-hand-group"; private static final String DEFAULT_SESSION_ALIAS = "th2-hand"; + private static final String DEFAULT_SCREENSHOT_SESSION_ALIAS = "th2-hand-screenshot"; private static final int DEFAULT_RESPONSE_TIMEOUT = 120; private static final long DEFAULT_MESSAGE_BATCH_LIMIT = 1024 * 1024; // 1 MB @JsonProperty(value="session-alias", required = true, defaultValue = DEFAULT_SESSION_ALIAS) + @JsonAlias("sessionAlias") private String sessionAlias = DEFAULT_SESSION_ALIAS; - private String screenshotSessionAlias = null; + @JsonProperty(value="screenshot-session-alias", required = true, defaultValue = DEFAULT_SCREENSHOT_SESSION_ALIAS) + @JsonAlias("screenshotSessionAlias") + private String screenshotSessionAlias = DEFAULT_SCREENSHOT_SESSION_ALIAS; - @JsonProperty(value="sessionGroup") - private String sessionGroup = null; + @JsonProperty(value="session-group", defaultValue = DEFAULT_SESSION_GROUP) + @JsonAlias("sessionGroup") + private String sessionGroup = DEFAULT_SESSION_GROUP; @JsonProperty(value="message-batch-limit") + @JsonAlias("messageBatchLimit") private long messageBatchLimit = DEFAULT_MESSAGE_BATCH_LIMIT; - @JsonProperty(value="driversMapping", required = true) - private Map driversMapping; + @JsonProperty(value="drivers-mapping", required = true) + @JsonAlias("driversMapping") + private Map driversMapping; - @JsonProperty(value="rhOptions") - private Map rhOptions = Collections.emptyMap();; + @JsonProperty(value="rh-options") + @JsonAlias("rhOptions") + private Map rhOptions = Collections.emptyMap(); - @JsonProperty(value="responseTimeoutSec") + @JsonProperty(value="response-timeout-sec") + @JsonAlias("responseTimeoutSec") private int responseTimeout = DEFAULT_RESPONSE_TIMEOUT; - public Map getDriversMapping() { + @JsonProperty(value="use-transport") + @JsonAlias("useTransport") + private boolean useTransport = true; + + public Map getDriversMapping() { return driversMapping; } @@ -77,4 +92,8 @@ public String getScreenshotSessionAlias() { public long getMessageBatchLimit() { return messageBatchLimit; } + + public boolean isUseTransport() { + return useTransport; + } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/services/HandBaseService.java b/src/main/java/com/exactpro/th2/hand/services/HandBaseService.java index f644893..444a81b 100644 --- a/src/main/java/com/exactpro/th2/hand/services/HandBaseService.java +++ b/src/main/java/com/exactpro/th2/hand/services/HandBaseService.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,57 +16,71 @@ package com.exactpro.th2.hand.services; +import com.exactpro.remotehand.RhConfigurationException; import com.exactpro.th2.act.grpc.hand.RhActionsBatch; import com.exactpro.th2.act.grpc.hand.RhBatchGrpc.RhBatchImplBase; import com.exactpro.th2.act.grpc.hand.RhBatchResponse; +import com.exactpro.th2.act.grpc.hand.RhBatchService; import com.exactpro.th2.act.grpc.hand.RhSessionID; import com.exactpro.th2.act.grpc.hand.RhTargetServer; import com.exactpro.th2.hand.HandException; import com.exactpro.th2.hand.IHandService; import com.google.protobuf.Empty; -import com.google.protobuf.TextFormat; import io.grpc.stub.StreamObserver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class HandBaseService extends RhBatchImplBase implements IHandService { - private final Logger logger = LoggerFactory.getLogger(getClass()); +import java.io.IOException; +import java.util.Map; + +import static com.exactpro.th2.common.message.MessageUtils.toJson; + +public class HandBaseService extends RhBatchImplBase implements IHandService, RhBatchService { + private final static Logger LOGGER = LoggerFactory.getLogger(HandBaseService.class); public static final String RH_SESSION_PREFIX = "/Ses"; private MessageHandler messageHandler; @Override - public void init(MessageHandler messageHandler) throws Exception { + public void init(MessageHandler messageHandler) { this.messageHandler = messageHandler; } @Override public void register(RhTargetServer targetServer, StreamObserver responseObserver) { try { - String sessionId = messageHandler.getRhConnectionManager().createSessionHandler(targetServer.getTarget()).getId(); - RhSessionID result = RhSessionID.newBuilder().setId(sessionId).setSessionAlias(messageHandler.getConfig().getSessionAlias()).build(); - responseObserver.onNext(result); + responseObserver.onNext(register(targetServer)); + responseObserver.onCompleted(); } catch (Exception e) { - logger.error("Error while creating session", e); + LOGGER.error("Error while creating session", e); Exception responseException = new HandException("Error while creating session", e); responseObserver.onError(responseException); } - responseObserver.onCompleted(); } @Override public void unregister(RhSessionID request, StreamObserver responseObserver) { - messageHandler.getRhConnectionManager().closeSessionHandler(request.getId()); - responseObserver.onNext(Empty.getDefaultInstance()); - responseObserver.onCompleted(); + try { + responseObserver.onNext(unregister(request)); + responseObserver.onCompleted(); + } catch (Exception e) { + LOGGER.error("Action failure, request: '{}'", toJson(request), e); + responseObserver.onError(e); + } + } @Override public void executeRhActionsBatch(RhActionsBatch request, StreamObserver responseObserver) { - logger.trace("Action: '{}', request: '{}'", "executeRhActionsBatch", TextFormat.shortDebugString(request)); - responseObserver.onNext(messageHandler.handleActionsBatchRequest(request)); - responseObserver.onCompleted(); + LOGGER.trace("Action: 'executeRhActionsBatch', request: '{}'", toJson(request)); + try { + responseObserver.onNext(executeRhActionsBatch(request)); + responseObserver.onCompleted(); + } catch (Exception e) { + LOGGER.error("Action failure, request: '{}'", toJson(request), e); + responseObserver.onError(e); + } } @Override @@ -74,7 +88,47 @@ public void dispose() { try { this.messageHandler.close(); } catch (Exception e) { - logger.error("Error while disposing message handler", e); + LOGGER.error("Error while disposing message handler", e); } } + + @Override + public RhSessionID register(RhTargetServer targetServer) { + try { + String sessionId = messageHandler.getRhConnectionManager().createSessionHandler(targetServer.getTarget()).getId(); + return RhSessionID.newBuilder().setId(sessionId).setSessionAlias(messageHandler.getConfig().getSessionAlias()).build(); + } catch (RhConfigurationException e) { + throw new HandRuntimeException(e.getMessage(), e); + } + } + + @Override + public RhSessionID register(RhTargetServer input, Map properties) { + return register(input); + } + + @Override + public Empty unregister(RhSessionID request) { + messageHandler.getRhConnectionManager().closeSessionHandler(request.getId()); + return Empty.getDefaultInstance(); + } + + @Override + public Empty unregister(RhSessionID input, Map properties) { + return unregister(input); + } + + @Override + public RhBatchResponse executeRhActionsBatch(RhActionsBatch request) { + try { + return messageHandler.handleActionsBatchRequest(request); + } catch (IOException e) { + throw new HandRuntimeException(e.getMessage(), e); + } + } + + @Override + public RhBatchResponse executeRhActionsBatch(RhActionsBatch input, Map properties) { + return executeRhActionsBatch(input); + } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/services/MessageHandler.java b/src/main/java/com/exactpro/th2/hand/services/MessageHandler.java index 8ab605c..cc6e1ac 100644 --- a/src/main/java/com/exactpro/th2/hand/services/MessageHandler.java +++ b/src/main/java/com/exactpro/th2/hand/services/MessageHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,39 +19,58 @@ import com.exactpro.th2.act.grpc.hand.RhActionsBatch; import com.exactpro.th2.act.grpc.hand.RhBatchResponse; import com.exactpro.th2.common.schema.factory.CommonFactory; -import com.exactpro.th2.hand.Config; +import com.exactpro.th2.common.utils.message.transport.MessageUtilsKt; +import com.exactpro.th2.hand.Context; import com.exactpro.th2.hand.RhConnectionManager; import com.exactpro.th2.hand.builders.events.DefaultEventBuilder; -import com.exactpro.th2.hand.builders.mstore.DefaultMessageStoreBuilder; +import com.exactpro.th2.hand.builders.mstore.ProtobufMessageStoreBuilder; +import com.exactpro.th2.hand.builders.mstore.TransportMessageStoreBuilder; import com.exactpro.th2.hand.builders.script.ScriptBuilder; import com.exactpro.th2.hand.requestexecutors.ActionsBatchExecutor; import com.exactpro.th2.hand.services.estore.EventStoreHandler; import com.exactpro.th2.hand.services.estore.EventStoreSender; import com.exactpro.th2.hand.services.mstore.MessageStoreHandler; -import com.exactpro.th2.hand.services.mstore.MessageStoreSender; +import com.exactpro.th2.hand.services.mstore.ProtobufMessageStoreSender; +import com.exactpro.th2.hand.services.mstore.TransportMessageStoreSender; +import java.io.IOException; +import java.time.Instant; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; public class MessageHandler implements AutoCloseable { - private final Config config; - private final MessageStoreHandler messageStoreHandler; + private final Context context; + private final MessageStoreHandler messageStoreHandler; private final EventStoreHandler eventStoreHandler; private final RhConnectionManager rhConnectionManager; private final ScriptBuilder scriptBuilder = new ScriptBuilder(); - public MessageHandler(Config config, AtomicLong seqNum) { - this.config = config; - rhConnectionManager = new RhConnectionManager(config); - CommonFactory factory = config.getFactory(); - this.messageStoreHandler = new MessageStoreHandler( - config.getSessionGroup(), - new MessageStoreSender(factory), - new DefaultMessageStoreBuilder(config.getFactory(), seqNum) - ); + public MessageHandler(Context context) { + this.context = context; + rhConnectionManager = new RhConnectionManager(context); + CommonFactory factory = context.getFactory(); + AtomicLong seqNum = new AtomicLong(getCurrentTime()); + if (context.isUseTransport()) { + this.messageStoreHandler = new MessageStoreHandler<>( + context.getSessionGroup(), + new TransportMessageStoreSender(context), + new TransportMessageStoreBuilder(seqNum), + message -> MessageUtilsKt.toProto(message.getId(), context.getBook(), context.getSessionGroup()) + ); + } else { + this.messageStoreHandler = new MessageStoreHandler<>( + context.getSessionGroup(), + new ProtobufMessageStoreSender(context), + new ProtobufMessageStoreBuilder(context.getFactory(), seqNum), + message -> message.getMetadata().getId() + ); + } + + this.eventStoreHandler = new EventStoreHandler(new EventStoreSender(factory.getEventBatchRouter()), new DefaultEventBuilder(factory)); } - public MessageStoreHandler getMessageStoreHandler() { + public MessageStoreHandler getMessageStoreHandler() { return messageStoreHandler; } @@ -63,23 +82,26 @@ public ScriptBuilder getScriptBuilder() { return scriptBuilder; } - public Config getConfig() { - return config; + public Context getConfig() { + return context; } public RhConnectionManager getRhConnectionManager() { return rhConnectionManager; } - public RhBatchResponse handleActionsBatchRequest(RhActionsBatch request) { + public RhBatchResponse handleActionsBatchRequest(RhActionsBatch request) throws IOException { ActionsBatchExecutor actionsBatchExecutor = new ActionsBatchExecutor(this); return actionsBatchExecutor.execute(request).getHandResponse(); } @Override - public void close() throws Exception { + public void close() { rhConnectionManager.dispose(); - messageStoreHandler.close(); - eventStoreHandler.close(); + } + + private static long getCurrentTime() { + Instant now = Instant.now(); + return TimeUnit.SECONDS.toNanos(now.getEpochSecond()) + now.getNano(); } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreHandler.java b/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreHandler.java index 967366f..282bde2 100644 --- a/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreHandler.java +++ b/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import com.exactpro.th2.hand.builders.events.DefaultEventBuilder; -public class EventStoreHandler implements AutoCloseable { +public class EventStoreHandler { private final EventStoreSender eventStoreSender; private final DefaultEventBuilder eventBuilder; @@ -35,9 +35,4 @@ public EventStoreSender getEventStoreSender() { public DefaultEventBuilder getEventBuilder() { return eventBuilder; } - - @Override - public void close() throws Exception { - this.eventStoreSender.close(); - } } diff --git a/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreSender.java b/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreSender.java index 4868648..7c2d17e 100644 --- a/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreSender.java +++ b/src/main/java/com/exactpro/th2/hand/services/estore/EventStoreSender.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import java.io.IOException; -public class EventStoreSender implements AutoCloseable { +public class EventStoreSender { private static final Logger LOGGER = LoggerFactory.getLogger(EventStoreSender.class); private final MessageRouter eventBatchRouter; @@ -41,9 +41,4 @@ public void storeEvent(Event event) { LOGGER.error("Could not store event with id: " + event.getId(), e); } } - - @Override - public void close() throws Exception { - eventBatchRouter.close(); - } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreHandler.java b/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreHandler.java index f923ea3..67fa861 100644 --- a/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreHandler.java +++ b/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,7 @@ import com.exactpro.th2.act.grpc.hand.RhActionsBatch; import com.exactpro.th2.common.grpc.Direction; import com.exactpro.th2.common.grpc.MessageID; -import com.exactpro.th2.common.grpc.RawMessage; -import com.exactpro.th2.hand.builders.mstore.DefaultMessageStoreBuilder; +import com.exactpro.th2.hand.builders.mstore.MessageStoreBuilder; import com.exactpro.th2.hand.messages.RhResponseMessageBody; import com.google.protobuf.Descriptors; import com.google.protobuf.GeneratedMessageV3; @@ -36,23 +35,28 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.ArrayList; -import java.util.LinkedHashMap; -public class MessageStoreHandler implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(MessageStoreSender.class); +public class MessageStoreHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(MessageStoreHandler.class); private final String sessionGroup; - private final MessageStoreSender messageStoreSender; - private final DefaultMessageStoreBuilder messageStoreBuilder; - - public MessageStoreHandler(String sessionGroup, MessageStoreSender messageStoreSender, DefaultMessageStoreBuilder defaultMessageStoreBuilder) { + private final MessageStoreSender messageStoreSender; + private final MessageStoreBuilder messageStoreBuilder; + private final MessageIdExtractor messageIdExtractor; + + public MessageStoreHandler(String sessionGroup, + MessageStoreSender messageStoreSender, + MessageStoreBuilder defaultMessageStoreBuilder, + MessageIdExtractor messageIdExtractor) { this.sessionGroup = sessionGroup; this.messageStoreSender = messageStoreSender; this.messageStoreBuilder = defaultMessageStoreBuilder; + this.messageIdExtractor = messageIdExtractor; } private List getActionsList (RhActionsBatch actionsList) { @@ -63,7 +67,7 @@ private List getActionsList (RhActionsBatch action case WEB: return rhActionList.getWeb().getWebActionListList(); default: - logger.warn("Actions list is not set"); + LOGGER.warn("Actions list is not set"); return Collections.emptyList(); } } @@ -93,36 +97,36 @@ public List onRequest(RhActionsBatch actionsList, String sessionId) { } } - RawMessage message = messageStoreBuilder.buildMessage(Collections.singletonMap("messages", allMessages), + T message = messageStoreBuilder.buildMessage(Collections.singletonMap("messages", allMessages), Direction.SECOND, sessionId, sessionGroup); if (message != null) { messageStoreSender.sendMessages(message); - return Collections.singletonList(message.getMetadata().getId()); + return Collections.singletonList(messageIdExtractor.getId(message)); } else { - logger.debug("Nothing to store to mstore"); + LOGGER.debug("Nothing to store to mstore"); return Collections.emptyList(); } } public List storeScreenshots(List screenshotIds, String sessionAlias) { if (screenshotIds == null || screenshotIds.isEmpty()) { - logger.debug("No screenshots to store"); + LOGGER.debug("No screenshots to store"); return Collections.emptyList(); } List messageIDS = new ArrayList<>(); - List rawMessages = new ArrayList<>(); + List rawMessages = new ArrayList<>(); for (ActionResult screenshotId : screenshotIds) { - logger.debug("Storing screenshot id {}", screenshotId); + LOGGER.debug("Storing screenshot id {}", screenshotId); Path screenPath = Configuration.SCREENSHOTS_DIR_PATH.resolve(screenshotId.getData()); if (!Files.exists(screenPath)) { - logger.warn("Screenshot with id {} does not exists", screenshotId); + LOGGER.warn("Screenshot with id {} does not exists", screenshotId); continue; } - RawMessage rawMessage = messageStoreBuilder.buildMessageFromFile(screenPath, Direction.FIRST, sessionAlias, sessionGroup); + T rawMessage = messageStoreBuilder.buildMessageFromFile(screenPath, Direction.FIRST, sessionAlias, sessionGroup); if (rawMessage != null) { - messageIDS.add(rawMessage.getMetadata().getId()); + messageIDS.add(messageIdExtractor.getId(rawMessage)); rawMessages.add(rawMessage); } removeScreenshot(screenPath); @@ -135,11 +139,11 @@ public List storeScreenshots(List screenshotIds, String public MessageID onResponse(RhScriptResult response, String sessionId, String rhSessionId) { RhResponseMessageBody body = RhResponseMessageBody.fromRhScriptResult(response).setRhSessionId(rhSessionId); try { - RawMessage message = messageStoreBuilder.buildMessage(body.getFields(), Direction.FIRST, sessionId, sessionGroup); + T message = messageStoreBuilder.buildMessage(body.getFields(), Direction.FIRST, sessionId, sessionGroup); messageStoreSender.sendMessages(message); - return message.getMetadata().getId(); + return messageIdExtractor.getId(message); } catch (Exception e) { - logger.error("Cannot send message to message-storage", e); + LOGGER.error("Cannot send message to message-storage", e); } return null; @@ -149,7 +153,7 @@ private void removeScreenshot(Path file) { try { Files.delete(file); } catch (IOException e) { - logger.warn("Error deleting file: " + file.toAbsolutePath(), e); + LOGGER.warn("Error deleting file: " + file.toAbsolutePath(), e); } } @@ -179,8 +183,8 @@ private List> processList(List list) { return processed; } - @Override - public void close() throws Exception { - this.messageStoreSender.close(); + @FunctionalInterface + public interface MessageIdExtractor { + MessageID getId(T message); } } \ No newline at end of file diff --git a/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreSender.java b/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreSender.java index ff0c073..887b02a 100644 --- a/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreSender.java +++ b/src/main/java/com/exactpro/th2/hand/services/mstore/MessageStoreSender.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 Exactpro (Exactpro Systems Limited) + * Copyright 2024 Exactpro (Exactpro Systems Limited) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,120 +16,10 @@ package com.exactpro.th2.hand.services.mstore; -import com.exactpro.th2.common.grpc.AnyMessage; -import com.exactpro.th2.common.grpc.MessageGroup; -import com.exactpro.th2.common.grpc.MessageGroupBatch; -import com.exactpro.th2.common.grpc.RawMessage; -import com.exactpro.th2.common.schema.factory.CommonFactory; -import com.exactpro.th2.common.schema.message.MessageRouter; -import com.exactpro.th2.hand.schema.CustomConfiguration; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.Collection; -import java.util.Collections; - -public class MessageStoreSender implements AutoCloseable { - private static final Logger logger = LoggerFactory.getLogger(MessageStoreSender.class); - - public static final String RAW_MESSAGE_ATTRIBUTE = "raw"; - - private final MessageRouter messageRouterGroupBatch; - private final long batchLimit; - - - public MessageStoreSender(CommonFactory factory) { - messageRouterGroupBatch = factory.getMessageRouterMessageGroupBatch(); - CustomConfiguration customConfiguration = factory.getCustomConfiguration(CustomConfiguration.class); - this.batchLimit = customConfiguration.getMessageBatchLimit(); - writeToLogAboutConnection(factory); - } - - public void sendMessages(RawMessage messages) { - sendMessages(Collections.singleton(messages)); - } - - public void sendMessages(Collection messages) { - try { - sendRawMessages(messages); - } catch (Exception e) { - logger.error("Cannot store to mstore", e); - } - } - - @Override - public void close() throws Exception { - messageRouterGroupBatch.close(); - } - - - private void sendRawMessages(Collection messages) throws Exception { - MessageGroupBatch.Builder currentBatchBuilder = MessageGroupBatch.newBuilder(); - long currentBatchLength = 0; - long totalLength = 0; - int count = 0; - int batchesCount = 0; - for (RawMessage message : messages) { - if (message == null) - continue; - - long size = this.calculateSize(message); - - MessageGroup.Builder mgBuilder = MessageGroup.newBuilder() - .addMessages(AnyMessage.newBuilder().setRawMessage(message)); - //if batchlimit has incorrect value, sender should pack each message to batch - //if one message is bigger that batchLimit it is should send to mstore anyway and reject by it - if (currentBatchLength + size > batchLimit && currentBatchLength != 0) { - this.messageRouterGroupBatch.sendAll(currentBatchBuilder.build(), RAW_MESSAGE_ATTRIBUTE); - currentBatchBuilder = MessageGroupBatch.newBuilder(); - currentBatchLength = 0; - batchesCount++; - } - - currentBatchBuilder.addGroups(mgBuilder); - currentBatchLength += size; - totalLength += size; - count++; - } - - if (currentBatchLength != 0) { - this.messageRouterGroupBatch.sendAll(currentBatchBuilder.build(), RAW_MESSAGE_ATTRIBUTE); - batchesCount++; - } - - if (count == 0) { - logger.debug("There are no valid messages to send"); - return; - } - - logger.debug("Group with {} message(s) separated by {} batches to mstore was sent ({} bytes)", - count, batchesCount, totalLength); - } - private long calculateSize(RawMessage message) { - return message.getBody().size(); - } +public interface MessageStoreSender { + void sendMessages(T messages); - private void writeToLogAboutConnection(CommonFactory factory) { - if (!logger.isInfoEnabled()) - return; - StringBuilder connectionInfo = new StringBuilder("Connection to RabbitMQ with "); - connectionInfo.append(factory.getRabbitMqConfiguration()).append(" is established \n"); - connectionInfo.append("Queues: \n"); - factory.getMessageRouterConfiguration().getQueues().forEach((name, queue) -> { - connectionInfo.append(name).append(" : "); - try { - ObjectMapper mapper = new ObjectMapper(); - connectionInfo.append(mapper.writeValueAsString(queue)); - } - catch (JsonProcessingException e) { - logger.warn("Error occurs while convert QueueConfiguration to JSON string", e); - connectionInfo.append("QueueConfiguration is not available"); - } - connectionInfo.append('\n'); - }); - logger.info(connectionInfo.toString()); - } + void sendMessages(Collection messages); } diff --git a/src/main/java/com/exactpro/th2/hand/services/mstore/ProtobufMessageStoreSender.java b/src/main/java/com/exactpro/th2/hand/services/mstore/ProtobufMessageStoreSender.java new file mode 100644 index 0000000..7280286 --- /dev/null +++ b/src/main/java/com/exactpro/th2/hand/services/mstore/ProtobufMessageStoreSender.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.services.mstore; + +import com.exactpro.th2.common.grpc.AnyMessage; +import com.exactpro.th2.common.grpc.MessageGroup; +import com.exactpro.th2.common.grpc.MessageGroupBatch; +import com.exactpro.th2.common.grpc.RawMessage; +import com.exactpro.th2.common.schema.message.MessageRouter; +import com.exactpro.th2.hand.Context; +import com.exactpro.th2.hand.schema.CustomConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +public class ProtobufMessageStoreSender implements MessageStoreSender { + private static final String RAW_MESSAGE_ATTRIBUTE = "raw"; + private static final Logger LOGGER = LoggerFactory.getLogger(ProtobufMessageStoreSender.class); + private final MessageRouter messageRouterGroupBatch; + private final long batchLimit; + + public ProtobufMessageStoreSender(Context context) { + messageRouterGroupBatch = context.getFactory().getMessageRouterMessageGroupBatch(); + CustomConfiguration customConfiguration = context.getCustomConfiguration(); + this.batchLimit = customConfiguration.getMessageBatchLimit(); + } + + public void sendMessages(RawMessage messages) { + sendMessages(Collections.singleton(messages)); + } + + public void sendMessages(Collection messages) { + try { + sendRawMessages(messages); + } catch (Exception e) { + LOGGER.error("Cannot store to mstore", e); + } + } + + private void sendRawMessages(Collection messages) throws Exception { + MessageGroupBatch.Builder currentBatchBuilder = MessageGroupBatch.newBuilder(); + long currentBatchLength = 0; + long totalLength = 0; + int count = 0; + int batchesCount = 0; + for (RawMessage message : messages) { + if (message == null) + continue; + + long size = this.calculateSize(message); + + MessageGroup.Builder mgBuilder = MessageGroup.newBuilder() + .addMessages(AnyMessage.newBuilder().setRawMessage(message)); + //if batch limit has incorrect value, sender should pack each message to batch + //if one message is bigger that batchLimit it is should send to mstore anyway and reject by it + if (currentBatchLength + size > batchLimit && currentBatchLength != 0) { + this.messageRouterGroupBatch.sendAll(currentBatchBuilder.build(), RAW_MESSAGE_ATTRIBUTE); + currentBatchBuilder = MessageGroupBatch.newBuilder(); + currentBatchLength = 0; + batchesCount++; + } + + currentBatchBuilder.addGroups(mgBuilder); + currentBatchLength += size; + totalLength += size; + count++; + } + + if (currentBatchLength != 0) { + this.messageRouterGroupBatch.sendAll(currentBatchBuilder.build(), RAW_MESSAGE_ATTRIBUTE); + batchesCount++; + } + + if (count == 0) { + LOGGER.debug("There are no valid messages to send"); + return; + } + + LOGGER.debug("Group with {} message(s) separated by {} batches to mstore was sent ({} bytes)", + count, batchesCount, totalLength); + } + + private long calculateSize(RawMessage message) { + return message.getBody().size(); + } +} diff --git a/src/main/java/com/exactpro/th2/hand/services/mstore/TransportMessageStoreSender.java b/src/main/java/com/exactpro/th2/hand/services/mstore/TransportMessageStoreSender.java new file mode 100644 index 0000000..271a055 --- /dev/null +++ b/src/main/java/com/exactpro/th2/hand/services/mstore/TransportMessageStoreSender.java @@ -0,0 +1,104 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.services.mstore; + +import com.exactpro.th2.common.schema.message.MessageRouter; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.GroupBatch; +import com.exactpro.th2.common.schema.message.impl.rabbitmq.transport.RawMessage; +import com.exactpro.th2.common.utils.message.transport.MessageUtilsKt; +import com.exactpro.th2.hand.Context; +import com.exactpro.th2.hand.schema.CustomConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; + +public class TransportMessageStoreSender implements MessageStoreSender { + private static final Logger LOGGER = LoggerFactory.getLogger(TransportMessageStoreSender.class); + private final MessageRouter messageRouter; + private final long batchLimit; + private final String book; + private final String sessionGroup; + + public TransportMessageStoreSender(Context context) { + messageRouter = context.getFactory().getTransportGroupBatchRouter(); + CustomConfiguration customConfiguration = context.getCustomConfiguration(); + this.batchLimit = customConfiguration.getMessageBatchLimit(); + this.book = context.getFactory().getBoxConfiguration().getBookName(); + this.sessionGroup = customConfiguration.getSessionGroup(); + } + + public void sendMessages(RawMessage messages) { + sendMessages(Collections.singleton(messages)); + } + + public void sendMessages(Collection messages) { + try { + sendRawMessages(messages); + } catch (Exception e) { + LOGGER.error("Cannot store to mstore", e); + } + } + + private void sendRawMessages(Collection messages) throws Exception { + GroupBatch.Builder currentBatchBuilder = createBatchBuilder(); + long currentBatchLength = 0; + long totalLength = 0; + int count = 0; + int batchesCount = 0; + for (RawMessage message : messages) { + if (message == null) + continue; + + long size = message.getBody().readableBytes(); + + //if batch limit has incorrect value, sender should pack each message to batch + //if one message is bigger that batchLimit it is should send to mstore anyway and reject by it + if (currentBatchLength + size > batchLimit && currentBatchLength != 0) { + this.messageRouter.sendAll(currentBatchBuilder.build()); + currentBatchBuilder = createBatchBuilder(); + currentBatchLength = 0; + batchesCount++; + } + + currentBatchBuilder.addGroup(MessageUtilsKt.toGroup(message)); + currentBatchLength += size; + totalLength += size; + count++; + } + + if (currentBatchLength != 0) { + this.messageRouter.sendAll(currentBatchBuilder.build()); + batchesCount++; + } + + if (count == 0) { + LOGGER.debug("There are no valid messages to send"); + return; + } + + LOGGER.debug("Group with {} message(s) separated by {} batches to mstore was sent ({} bytes)", + count, batchesCount, totalLength); + } + + private GroupBatch.Builder createBatchBuilder() { + return GroupBatch.builder() + .setBook(book) + .setSessionGroup(sessionGroup); + } +} diff --git a/src/main/kotlin/com/exactpro/th2/hand/builders/events/TableRow.kt b/src/main/kotlin/com/exactpro/th2/hand/builders/events/TableRow.kt new file mode 100644 index 0000000..34ea018 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/hand/builders/events/TableRow.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.builders.events + +import com.exactpro.th2.common.event.bean.IRow +import com.fasterxml.jackson.annotation.JsonProperty + +class TableRow( + @JsonProperty("Name") + val name: String, + @JsonProperty("Value") + val value: String, +): IRow \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/hand/services/HandRuntimeException.kt b/src/main/kotlin/com/exactpro/th2/hand/services/HandRuntimeException.kt new file mode 100644 index 0000000..8d72729 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/hand/services/HandRuntimeException.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.services + +class HandRuntimeException(message: String, cause: Throwable) : RuntimeException(message, cause) \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/hand/builders/events/DefaultEventBuilderTest.kt b/src/test/kotlin/com/exactpro/th2/hand/builders/events/DefaultEventBuilderTest.kt new file mode 100644 index 0000000..e1c7769 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/hand/builders/events/DefaultEventBuilderTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright 2024 Exactpro (Exactpro Systems Limited) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.exactpro.th2.hand.builders.events + +import com.exactpro.remotehand.rhdata.RhResponseCode +import com.exactpro.remotehand.rhdata.RhScriptResult +import com.exactpro.th2.act.grpc.hand.RhActionsBatch +import com.exactpro.th2.act.grpc.hand.RhBatchResponse +import com.exactpro.th2.common.grpc.EventID +import com.exactpro.th2.common.grpc.EventStatus +import com.exactpro.th2.common.grpc.MessageID +import com.exactpro.th2.common.message.toJson +import com.exactpro.th2.common.schema.box.configuration.BoxConfiguration +import com.exactpro.th2.common.schema.factory.CommonFactory +import com.exactpro.th2.common.utils.message.toTimestamp +import com.exactpro.th2.hand.messages.responseexecutor.ActionsBatchExecutorResponse +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import strikt.api.expectThat +import strikt.api.expectThrows +import strikt.assertions.isEqualTo +import strikt.assertions.isNotBlank +import strikt.assertions.isSameInstanceAs +import strikt.assertions.isTrue +import java.time.Instant +import kotlin.test.assertEquals + +internal class DefaultEventBuilderTest { + + private val factory = mock { + on { newEventIDBuilder() }.thenAnswer { + EventID.newBuilder().setBookName(BOOK).setScope(SCOPE) + } + on { boxConfiguration }.thenReturn( + BoxConfiguration().apply { + bookName = BOOK + boxName = BOX_NAME + } + ) + } + private val builder = DefaultEventBuilder(factory) + + @BeforeEach + fun beforeEach() { + verify(factory).boxConfiguration + clearInvocations(factory) + } + + @AfterEach + fun afterEach() { + verifyNoMoreInteractions(factory) + } + + @Test + fun `build event test`() { + val now = Instant.now() + val rhActionsBatch = RhActionsBatch.newBuilder().apply { + eventName = "test-event-name" + parentEventIdBuilder.setBookName("$BOOK-1").setScope("$SCOPE-1") + additionalEventInfoBuilder + .setDescription("test-description") + .setPrintTable(true) + .addKeys("test-key") + .addValues("test-value") + .setRequestParamsTableTitle("test-title") + storeActionMessages = true + }.build() + val rhBatchResponse = RhBatchResponse.newBuilder().apply { + scriptStatus = RhBatchResponse.ScriptExecutionStatus.EXECUTION_ERROR + sessionId = "test-session-id" + addResultBuilder() + .setActionId("test-action-id") + .setResult("test-result") + }.build() + val scriptResult = RhScriptResult().apply { + code = RhResponseCode.TOOL_BUSY.code + errorMessage = "test-error-message" + } + val messageIDs = listOf( + MessageID.newBuilder().apply { + bookName = "$BOOK-1" + sequence = 1 + timestamp = Instant.now().toTimestamp() + connectionIdBuilder.setSessionAlias("test-session-alias") + }.build() + ) + val actionsBatchExecutorResponse = ActionsBatchExecutorResponse( + rhBatchResponse, + scriptResult, + messageIDs + ) + + val event = builder.buildEvent(now, rhActionsBatch, actionsBatchExecutorResponse) + + expectThat(event) { + get { id }.and { + get { id }.isNotBlank() + get { startTimestamp }.isEqualTo(now.toTimestamp()) + get { scope }.isSameInstanceAs(rhActionsBatch.parentEventId.scope) + get { bookName }.isSameInstanceAs(rhActionsBatch.parentEventId.bookName) + } + get { name }.isEqualTo(rhActionsBatch.eventName) + get { parentId }.isSameInstanceAs(rhActionsBatch.parentEventId) + get { status }.isEqualTo(EventStatus.FAILED) + get { attachedMessageIdsList }.isEqualTo(messageIDs) + get { hasEndTimestamp() }.isTrue() + @Suppress("SpellCheckingInspection") + get { body.toStringUtf8() }.isEqualTo(""" + |[ + |{"data":"Description: \ntest-description","type":"message"}, + |{"data":"test-title","type":"message"}, + |{ + |"type":"table", + |"rows": + |[ + |{ + |"Name":"test-key", + |"Value":"test-value" + |} + |] + |}, + |{"data":"Result","type":"message"}, + |{ + |"type":"table", + |"rows": + |[ + |{"Name":"Action status","Value":"EXECUTION_ERROR"}, + |{"Name":"Errors","Value":"test-error-message"}, + |{"Name":"SessionId","Value":"test-session-id"} + |] + |}, + |{"data":"Action messages","type":"message"}, + |{ + |"type":"table", + |"rows": + |[ + |{"Name":"test-action-id","Value":"test-result"} + |] + |} + |] + """.trimMargin().replace("\n", "")) + } + } + + @Test + fun `build event when message book doesn't mismatch to default book test`() { + val now = Instant.now() + + val messageId = MessageID.newBuilder().apply { + bookName = "$BOOK-2" + sequence = 1 + timestamp = Instant.now().toTimestamp() + connectionIdBuilder.setSessionAlias("test-session-alias") + }.build() + val messageIDs = listOf(messageId) + + expectThrows { + builder.buildEvent( + now, + RhActionsBatch.getDefaultInstance(), + ActionsBatchExecutorResponse( + RhBatchResponse.getDefaultInstance(), + RhScriptResult(), + messageIDs + ) + ) + }.assert("Message") { + assertEquals( + """ + |Build event failure, book: '$BOOK', scope: '$SCOPE', + |name: 'Unknown event name', type: 'Unknown event type', + |problems: [Book name mismatch in '${messageId.toJson()}' message id] + """.trimMargin().replace("\n", ""), + it.message) + } + } + + @Test + fun `build event when message book doesn't mismatch to parent event book test`() { + val now = Instant.now() + + val messageId = MessageID.newBuilder().apply { + bookName = "$BOOK-2" + sequence = 1 + timestamp = Instant.now().toTimestamp() + connectionIdBuilder.setSessionAlias("test-session-alias") + }.build() + val eventId = EventID.newBuilder().setBookName("$BOOK-1").setScope("$SCOPE-1").build() + val messageIDs = listOf(messageId) + + expectThrows { + builder.buildEvent( + now, + RhActionsBatch.newBuilder().setParentEventId(eventId).build(), + ActionsBatchExecutorResponse( + RhBatchResponse.getDefaultInstance(), + RhScriptResult(), + messageIDs + ) + ) + }.assert("Message") { + assertEquals( + """ + |Build event failure, book: '${eventId.bookName}', scope: '${eventId.scope}', + |name: 'Unknown event name', type: 'Unknown event type', + |problems: [Book name mismatch in '${messageId.toJson()}' message id] + """.trimMargin().replace("\n", ""), + it.message) + } + } + + companion object { + const val BOX_NAME = "test-box-name" + const val BOOK = "test-book" + const val SCOPE = BOX_NAME + } +} \ No newline at end of file