diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3fac85fb..11ce1529 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,3 +26,12 @@ updates: directory: "/" schedule: interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + labels: ["image"] + directory: "/" + schedule: + interval: "daily" diff --git a/interop/pom.xml b/.github/workflows/bintray-mirror-settings.xml similarity index 57% rename from interop/pom.xml rename to .github/workflows/bintray-mirror-settings.xml index 4fcfd648..db3e1262 100644 --- a/interop/pom.xml +++ b/.github/workflows/bintray-mirror-settings.xml @@ -1,6 +1,5 @@ - - - - parent - com.redhat.cli-java - 1.2.2-SNAPSHOT - ../parent/pom.xml - - 4.0.0 - - interop - - - + + + + + bintray-maven-central + Bintray Maven Central mirror + https://jcenter.bintray.com + central + + + diff --git a/.github/workflows/bintray-repository-settings.xml b/.github/workflows/bintray-repository-settings.xml new file mode 100644 index 00000000..b2b2f8f1 --- /dev/null +++ b/.github/workflows/bintray-repository-settings.xml @@ -0,0 +1,50 @@ + + + + + + + bintray + + + + false + + bintray + https://jcenter.bintray.com + + + + + + false + + bintray-plugins + https://jcenter.bintray.com + + + + + + + bintray + + diff --git a/.github/workflows/google-mirror-settings.xml b/.github/workflows/google-mirror-settings.xml new file mode 100644 index 00000000..7c996284 --- /dev/null +++ b/.github/workflows/google-mirror-settings.xml @@ -0,0 +1,29 @@ + + + + + + google-maven-central + GCS Maven Central mirror + https://maven-central.storage-download.googleapis.com/maven2/ + central + + + diff --git a/.github/workflows/google-repository-settings.xml b/.github/workflows/google-repository-settings.xml new file mode 100644 index 00000000..5c4a0323 --- /dev/null +++ b/.github/workflows/google-repository-settings.xml @@ -0,0 +1,48 @@ + + + + + + google-maven-central + + + + false + + google-maven-central + https://maven-central.storage-download.googleapis.com/maven2/ + + + + + + false + + google-maven-central-plugins + https://maven-central.storage-download.googleapis.com/maven2/ + + + + + + + google-maven-central + + diff --git a/.github/workflows/image_build.yml b/.github/workflows/image_build.yml new file mode 100644 index 00000000..ad6f461b --- /dev/null +++ b/.github/workflows/image_build.yml @@ -0,0 +1,44 @@ +name: Build and Push Image +on: + push: + branches: + - 'main' + +env: + IMAGE_NAME: cli-java + IMAGE_REGISTRY: quay.io + IMAGE_NAMESPACE: rhmessagingqe + +jobs: + build: + name: Build and push image + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Build Image + id: build-image + uses: redhat-actions/buildah-build@v2 + with: + image: ${{ env.IMAGE_NAME }} + tags: latest ${{ github.sha }} ${{ github.ref_name }} + archs: amd64, arm64, ppc64le, s390x + containerfiles: | + ./Dockerfile + + - name: Push To quay.io + id: push-to-quay + uses: redhat-actions/push-to-registry@v2 + with: + image: ${{ steps.build-image.outputs.image }} + tags: ${{ steps.build-image.outputs.tags }} + registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }} + username: ${{ secrets.QUAY_USER }} + password: ${{ secrets.QUAY_TOKEN }} + + - name: Print images URL + run: echo "Images pushed to ${{ steps.push-to-quay.outputs.registry-paths }}" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c8098889..03bc6963 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,9 +5,14 @@ name: Java CI with Maven on: push: - branches: [ main ] + branches: [ main, qpid-jms-1.x ] pull_request: - branches: [ main ] + branches: [ main, qpid-jms-1.x ] + workflow_dispatch: + +env: + # https://maven.apache.org/configure.html#maven_args-environment-variable + MAVEN_ARGS: "-Dmaven.artifact.threads=42 --no-transfer-progress --settings=.github/workflows/google-repository-settings.xml" jobs: build: @@ -15,18 +20,47 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v4 with: - java-version: 1.8 + distribution: 'adopt-openj9' + java-version: '11' - name: Build with Maven - run: mvn -B package --file pom.xml - + run: mvn -B package --file pom.xml ${{env.MAVEN_ARGS}} + - name: Install the qpid-jms subtree (needed for the next step) - run: mvn clean install -DskipTests -pl :cli-qpid-jms -am + run: mvn clean install -DskipTests -pl :cli-qpid-jms -am ${{env.MAVEN_ARGS}} - name: Run a single test (remember that JUnit5 issue) - run: mvn surefire:test -Dtest=QPIDJMS484Test -pl :cli-qpid-jms + run: mvn surefire:test -Dtest=QPIDJMS484Test -pl :cli-qpid-jms ${{env.MAVEN_ARGS}} + + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + distribution: 'adopt-openj9' + java-version: '11' + + - name: docker pull + run: docker pull quay.io/jdanek/docker-alpine-openjdk-artemis-snapshot:latest + + - name: docker run + run: docker run --rm -v`pwd`/scripts:/mnt -p 1883:1883 -p 5672:5672 -p 61616:61616 -p 5673:5673 -p 61617:61617 --entrypoint bash quay.io/jdanek/docker-alpine-openjdk-artemis-snapshot:latest /mnt/entrypoint.sh amq7-server & + + - name: sleep + run: sleep 10 + + - name: mvn package + run: mvn clean package -Ptests,coverage ${{env.MAVEN_ARGS}} + + - name: bash ./tests.sh + run: bash ./tests.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 08bafddf..00000000 --- a/.travis.yml +++ /dev/null @@ -1,43 +0,0 @@ -sudo: false -language: java -# oraclejdk8 is not available on releases after trusty -# https://travis-ci.community/t/install-of-oracle-jdk-8-failing/3038/8 -dist: trusty -jdk: - - openjdk8 - - oraclejdk8 - - openjdk11 - - oraclejdk11 - -services: - - docker - -env: - global: - # CC_TEST_REPORTER_ID - secure: "apzhT0ZU5sIR/ndjCtnHmzfmJePOXle0DD8Y4a9kTvFPidk8/RET4+YmWhE1ij4GOActLpbEwivA2wrM/YyxbRTRY4dZro0iK+O0DvPYQTsiI/fYe4chNOSO0+IQ1MA/KJbJgeZKgBCM/uXNw2Evb+KwBqeN72Lx1K9/uK8VkI2aiqbw9W3gEcshVMWthTFiPSSZfqU14y9KHP8HoEZvGed7kFJFslsfqwgVRMP4cnrff3CEmQNardjY/aWgVKzuqMVEvnw2ER6K0ULYoQyUdqqngvaqv7bEtQLkwdiVbbphVRyTfUkHtqbStp4vasegzDiEf7RhatqPNLCxb5V/jJFa6/pMccvDESwefLJW7ZqBObdUjk4pHbrQoY+psCs5Rs6Y9p/hA6jomZKO3ipHF/kSnKDcLCueR9UMh0/JTkbbr4cNAnxwOjX3btCCKeqSxD/0BjLWUINrMfCuXBBt29EOs02wLlURtv5HZrPzrHFnqXy4lja3JOzhiqLjTURnFwzT0gysKWGMKx4PPiZr/BY35pX/Z9InRhlXyuh8pQ3BJnJ4y8OXh/IISB4R9YfiTm42UlhGJUT7w/NR8uwgUBiTu/QgNf5FoZrH4QMsbNPchKFzhpB8ucp6V9Chjg7+86i3/BHmQM65r7H9mP9iGaYXOTRiJCKDetbrMa1W8Cs=" - -script: - # https://docs.travis-ci.com/user/docker/ - - docker pull quay.io/jdanek/docker-alpine-openjdk-artemis-snapshot:latest - - docker run --rm -v`pwd`/scripts:/mnt -p 1883:1883 -p 5672:5672 -p 61616:61616 -p 5673:5673 -p 61617:61617 --entrypoint bash quay.io/jdanek/docker-alpine-openjdk-artemis-snapshot:latest /mnt/entrypoint.sh amq7-server & - - sleep 10 - - mvn clean package -Ptests,coverage - - - java -jar cli-activemq/target/cli-activemq-1.2.2-SNAPSHOT-*.jar sender --address cli-activemq --log-msgs json --count 1 - - java -jar cli-activemq/target/cli-activemq-1.2.2-SNAPSHOT-*.jar receiver --address cli-activemq --log-msgs json --count 1 - - java -jar cli-activemq/target/cli-activemq-1.2.2-SNAPSHOT-*.jar sender --conn-username test --conn-ssl-verify-host false --conn-password test --msg-content msg no. %d --broker ssl://127.0.0.1:61617 --conn-auth-mechanisms PLAIN --timeout 30 --log-msgs json --log-lib trace --address message-basiccli_jms --count 10 --conn-ssl-trust-all true - - - java -jar cli-artemis-jms/target/cli-artemis-jms-1.2.2-SNAPSHOT-*.jar sender --address cli-artemis-jms --log-msgs json --count 1 - - java -jar cli-artemis-jms/target/cli-artemis-jms-1.2.2-SNAPSHOT-*.jar receiver --address cli-artemis-jms --log-msgs json --count 1 - - java -jar cli-artemis-jms/target/cli-artemis-jms-1.2.2-SNAPSHOT-*.jar sender --conn-username test --conn-ssl-verify-host false --conn-password test --msg-content msg no. %d --broker tcp://127.0.0.1:61617 --conn-auth-mechanisms PLAIN --timeout 30 --log-msgs json --log-lib trace --address message-basiccli_jms --count 10 --conn-ssl-trust-all true - - - cli_qpid_jms_jar=$(find cli-qpid-jms/target -name 'cli-qpid-jms-1.2.2-SNAPSHOT-*.jar' -not -name '*-tests.jar') - - java -jar ${cli_qpid_jms_jar} sender --address cli-qpid-jms --log-msgs json --count 1 - - java -jar ${cli_qpid_jms_jar} receiver --address cli-qpid-jms --log-msgs json --count 1 - - java -jar ${cli_qpid_jms_jar} sender --conn-username test --conn-ssl-verify-host false --conn-password test --msg-content msg no. %d --broker amqps://127.0.0.1:5673 --conn-auth-mechanisms PLAIN --timeout 30 --log-msgs json --log-lib trace --address message-basiccli_jms --count 10 --conn-ssl-trust-all true - - - java -jar cli-paho-java/target/cli-paho-java-1.2.2-SNAPSHOT-*.jar sender --address cli-paho-java --log-msgs json --count 1 - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4f640dba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +# Arguments for DEV's (comment static FROM and uncomnnet #DEV ones) +ARG UBI_VERSION=9 +ARG OPENJDK_VERSION=17 +ARG UBI_BUILD_TAG=latest +ARG UBI_RUNTIME_TAG=latest +ARG IMAGE_BUILD=registry.access.redhat.com/ubi${UBI_VERSION}/openjdk-${OPENJDK_VERSION}:${UBI_BUILD_TAG} +ARG IMAGE_BASE=registry.access.redhat.com/ubi${UBI_VERSION}/openjdk-${OPENJDK_VERSION}-runtime:${UBI_RUNTIME_TAG} + +#DEV FROM $IMAGE_BUILD AS build +FROM ${IMAGE_BUILD} AS build + +USER root +COPY . /app +WORKDIR /app + +RUN microdnf -y --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install \ + jq \ + && microdnf clean all -y + +ENV MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Dmaven.repo.local=/app/.m2 -Dmaven.artifact.threads=42" +RUN mvn clean && mvn -T 1C package -DskipTests=true --no-transfer-progress + +RUN mkdir targets && \ + cp cli-qpid-jms/target/cli-qpid-jms-*[0-9].jar targets/cli-qpid.jar && \ + cp cli-artemis-jms/target/cli-artemis-jms-*[0-9].jar targets/cli-artemis.jar && \ + cp cli-paho-java/target/cli-paho-java-*[0-9].jar targets/cli-paho.jar && \ + cp cli-activemq/target/cli-activemq-*[0-9].jar targets/cli-activemq.jar && \ + cp cli-protonj2/target/cli-protonj2-*[0-9].jar targets/cli-protonj2.jar && \ + ls -1 cli-*/target/cli-*.jar > VERSION.txt + +WORKDIR /tmp +RUN mkdir hivemq-mqtt && \ + cd hivemq-mqtt && \ + curl -sLO $(curl -s https://api.github.com/repos/hivemq/mqtt-cli/releases/latest | jq -r '.assets[] | select(.name | endswith(".jar")) | .browser_download_url') && \ + cp mqtt-cli-*[0-9].jar /app/targets/cli-hivemq-mqtt.jar && \ + ls -1 mqtt-cli-*[0-9].jar | sed -e 's/mqtt-cli/cli-hivemq-mqtt/'>> /app/VERSION.txt + +WORKDIR /app + +#DEV FROM $IMAGE_BASE +FROM ${IMAGE_BASE} + +LABEL name="Red Hat Messaging QE - Java CLI Image" \ + run="podman run --rm -ti /bin/bash cli-*" + +USER root + +# install fallocate for use by claire tests +RUN microdnf -y --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install \ + util-linux \ + && microdnf clean all -y + +RUN mkdir /licenses +COPY ./LICENSE /licenses/LICENSE.txt +COPY ./image/bin /usr/local/bin +COPY --from=build /app/targets/ /opt/cli-java +COPY --from=build /app/VERSION.txt /opt/cli-java + +RUN chmod 0755 /usr/local/bin/cli-* && \ + chmod +x /usr/local/bin/cli-* + +RUN mkdir /var/lib/cli-java && \ + chown -R 1001:0 /var/lib/cli-java && \ + chmod -R g=u /var/lib/cli-java + +USER 1001 + +VOLUME /var/lib/cli-java +WORKDIR /var/lib/cli-java + +CMD ["/bin/bash"] diff --git a/README.md b/README.md index 8df7f86c..0117a62b 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,42 @@ cli-java is a collection of commandline messaging clients suitable for interacting with Message Oriented Middleware. -## Getting started +## Requirements -When using IntelliJ IDEA Ultimate Edition, select "Open" (not "Import Project") option to open project and delete OSGi facets in File >> Project Structure >> Project Settings >> Facets. +* Java 11+ +* Maven 3 +* for [tcnative](https://netty.io/wiki/forked-tomcat-native.html) dynamic library, [outdated openssl 1.0 is required](https://github.com/netty/netty-tcnative/issues/551) + * on Fedora 37, execute `sudo dnf install -y apr https://kojipkgs.fedoraproject.org//packages/compat-openssl10/1.0.2o/11.fc33/x86_64/compat-openssl10-1.0.2o-11.fc33.x86_64.rpm` + * or, enable use of [BoringSSL](https://github.com/google/boringssl) static library instead of the dynamic one, with `-P tcnative-boringssl-static` profile to maven + +## Getting started mvn clean package # compile without executing external tests (tests that require broker) java -jar cli-qpid-jms/target/cli-qpid-jms-*.jar sender -b amqp://127.0.0.1:5672 -a myQ --log-msgs dict +### IntelliJ notes + +[Open an existing Maven project](https://www.jetbrains.com/help/idea/maven-support.html#maven_import_project_start). + +#### Common issues + +* OSGi problems (don't remember what those actually were) + +When using IntelliJ IDEA Ultimate Edition, select "Open" (not "Import Project") option to open project and delete OSGi facets in File >> Project Structure >> Project Settings >> Facets. + +* `Unresolved reference: DaggerFakeClient`, or anything else with `Dagger` in it + +The class is generated by the [Dagger](https://github.com/google/dagger) annotation processor. +Run `mvn compile` on the command line so that Maven generates what is needed. + +The IDE action on the Maven tab to generate sources does not actually generate what is needed for Kotlin tests, because of a [missing feature](https://youtrack.jetbrains.com/issue/KT-15040). + +* `Error: Unable to access jarfile [...]/target/cli-qpid-jms-1.2.2-SNAPSHOT-2.2.0.jar` + +IntelliJ is happy to run [failsafe](https://maven.apache.org/surefire/maven-failsafe-plugin/) tests without doing the (equivalent of) `mvn package` first. +This means that the jars used in the `*ITCase` tests may be nonexistent (or out of date). +Run `mvn package -DskipTests` yourself to fix this. + ### Run tests mvn test -Ptests @@ -33,11 +62,54 @@ When using IntelliJ IDEA Ultimate Edition, select "Open" (not "Import Project") mvn versions:set -DgenerateBackupPoms=false -DnewVersion=2017.07 +## Build docker + +Uses `podman`. Needs `sudo` to hook qemu. + +```shell +mvn clean +bash build_java.sh +bash build_docker.sh +``` + +Date-based versioning of image tags, use + +```shell +bash build_docker.sh $(date '+%Y-%m-%d') +``` + ## List of Java clis -* qpid-jms (AMQP 1.0) -* activemq-client (OpenWire) -* artemis-jms-client (Artemis Core) +| maven module | messaging library | protocol (JMS version) | notes | +|------------------|--------------------------------------------------------------------------------------------------------|------------------------|------------------| +| cli-activemq | [activemq-client](https://deps.dev/maven/org.apache.activemq%3Aactivemq-client) | OpenWire (JMS v1.1) | javax.jms API | +| cli-activemq-jmx | [artemis-core-client](https://deps.dev/maven/org.apache.activemq%3Aartemis-core-client) | JMX management | | +| cli-artemis-jms | [artemis-jms-client](https://deps.dev/maven/org.apache.activemq%3Aartemis-jms-client) | Artemis Core | javax.jms API | +| cli-paho-java | [eclipse.paho.client.mqttv3](https://deps.dev/maven/org.eclipse.paho%3Aorg.eclipse.paho.client.mqttv3) | MQTT v3 | | +| cli-protonj2 | [protonj2-client](https://deps.dev/maven/org.apache.qpid%3Aprotonj2-client) | AMQP 1.0 | "imperative API" | +| cli-qpid-jms | [qpid-jms-client](https://deps.dev/maven/org.apache.qpid%3Aqpid-jms-client) | AMQP 1.0 (JMS v2.0) | jakarta.jms API | + +## Additional maven modules + +| maven module | | +|--------------|-----------------------------------------------------------------------------| +| parent | common maven configuration for child modules, parent of all other modules | +| bom | contains dependencyManagement pom section with dependency versions | +| broker | embedded artemis-server broker for use in selftests | +| tests | test dependency of cli-* projects, contains shared test code | +| lib | shared code that does not depend on JMS | +| jmslib | shared code that depends on javax.jms API | +| jakartalib | shared code that depends on jakarta.jmx API | +| cli | the ClientListener interface for use in client selftests (messages as Maps) | + +## Directories + +| directory | | +|-----------|------------------------------------------------------------| +| .github | GitHub Actions CI configurations, dependabot.yml file | +| image | helper scripts for Dockerfile/Containerfile to build image | +| scripts | helper scripts for CI jobs | + ## Related projects diff --git a/bom/pom.xml b/bom/pom.xml index a8d11e4e..9f171ee6 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -33,39 +33,53 @@ pom - 1.7.30 + 2.0.13 + 2.23.1 5.0.4 - 2.36 - 2.12.3 + 2.51.1 + 2.17.1 - 1.5.10 - 5.7.2 - 1.4.1 - 1.1.3 - 3.10.0 - 4.1.0 + 2.3.10 + 5.10.2 + 2.2.0 + 1.4.2 + 5.12.0 + 4.2.1 - 2.17.0 + 2.35.0 - 3.8.1 + 3.13.0 3.1.0 - 3.0.0-M3 - 3.2.0 - 0.8.7 - 3.2.4 - 3.0.0-M5 + 3.5.0 + 3.4.1 + 0.8.12 + 3.6.0 + 3.5.0 2.7 + 1.0.15.Final - 21.0.1 - 3.13.0 + 24.1.0 + 3.44.0 + 2.0.9 org.slf4j - slf4j-log4j12 - ${slfj.version} + slf4j-api + ${slf4j-api.version} + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j-slf4j2-impl.version} + + + org.apache.logging.log4j + log4j-core + ${log4j-slf4j2-impl.version} + + net.sf.jopt-simple jopt-simple @@ -87,7 +101,13 @@ ${annotations.version} - + + + jakarta.jms + jakarta.jms-api + 3.1.0 + + org.apache.geronimo.specs geronimo-jms_1.1_spec @@ -110,6 +130,11 @@ jmslib 1.2.2-SNAPSHOT + + com.redhat.cli-java + jakartalib + 1.2.2-SNAPSHOT + com.redhat.cli-java lib @@ -122,6 +147,20 @@ test-jar test + + com.redhat.cli-java + jmslib + 1.2.2-SNAPSHOT + test-jar + test + + + com.redhat.cli-java + jakartalib + 1.2.2-SNAPSHOT + test-jar + test + @@ -205,6 +244,11 @@ jacoco-maven-plugin ${plugin.jacoco.version} + + org.powermock + powermock-reflect + ${powermock-reflect.version} + diff --git a/broker/src/test/java/util/Broker.java b/broker/src/test/java/util/Broker.java index f0fbbb33..0bc71e4e 100644 --- a/broker/src/test/java/util/Broker.java +++ b/broker/src/test/java/util/Broker.java @@ -20,18 +20,21 @@ package util; import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.api.core.management.AddressControl; +import org.apache.activemq.artemis.api.core.management.ObjectNameBuilder; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.ConfigurationUtils; import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ; import org.apache.activemq.artemis.spi.core.remoting.Acceptor; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.LogManager; -import org.apache.log4j.SimpleLayout; import org.junit.jupiter.api.extension.ExtensionContext; +import javax.management.MBeanServer; +import javax.management.MBeanServerInvocationHandler; +import javax.management.ObjectName; import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.file.Files; @@ -41,9 +44,11 @@ // https://activemq.apache.org/artemis/docs/latest/embedding-activemq.html public class Broker implements AutoCloseable, ExtensionContext.Store.CloseableResource { - public Path tempDir; - public EmbeddedActiveMQ embeddedBroker = new EmbeddedActiveMQ(); - public Configuration configuration = new ConfigurationImpl(); + // Use same MBeanServer instance that broker is using (don't create new) + private final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + public final Path tempDir; + public final EmbeddedActiveMQ embeddedBroker = new EmbeddedActiveMQ(); + public final Configuration configuration = new ConfigurationImpl(); public Broker() { this(null); @@ -89,11 +94,7 @@ public void close() { * Configures a log4j appender if there isn't any, so that log messages flood the stdout */ public static void configureLogging() { - if (LogManager.getRootLogger().getAllAppenders().hasMoreElements()) { - return; - } - ConsoleAppender consoleAppender = new ConsoleAppender(new SimpleLayout(), ConsoleAppender.SYSTEM_OUT); - LogManager.getRootLogger().addAppender(consoleAppender); + throw new UnsupportedOperationException("Not implemented yet."); } /** @@ -176,4 +177,24 @@ private int findRandomAvailablePortOnAllLocalInterfaces() throws IOException { return socket.getLocalPort(); } } + + protected Object createProxy(final ObjectName objectName, + final Class mbeanInterface, + final MBeanServer mbeanServer) { + return MBeanServerInvocationHandler.newProxyInstance(mbeanServer, objectName, mbeanInterface, false); + } + + /** + * The resulting AddressControl operations will throw java.lang.reflect.UndeclaredThrowableException + * if the queried object does not exist yet. Use awaitilly to deal with that. + */ + public AddressControl makeAddressControl(String queueName) { + SimpleString address = SimpleString.toSimpleString(queueName); + try { + AddressControl addressControl = (AddressControl) createProxy(ObjectNameBuilder.DEFAULT.getAddressObjectName(address), AddressControl.class, mBeanServer); + return addressControl; + } catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/build_java.sh b/build_java.sh new file mode 100755 index 00000000..55cc8001 --- /dev/null +++ b/build_java.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +set -x + +mvn -B package --file pom.xml -DskipTests diff --git a/cli-activemq-jmx/pom.xml b/cli-activemq-jmx/pom.xml index afa8c8c0..8e26721f 100644 --- a/cli-activemq-jmx/pom.xml +++ b/cli-activemq-jmx/pom.xml @@ -16,25 +16,25 @@ com.redhat.amqx.main.Main - 5.14.0 + 6.2.1 LATEST - 5.11.0.redhat-630187 + 5.11.1.redhat-00001 - 2.9.0.redhat-00005 + 2.53.0.redhat-00003 /opt/jboss-amq-7 amqx - 1.6.6 - 20160810 - 1.4 + 2.0.13 + 20231013.0.0.redhat-00001 + 1.5.0.redhat-00001 @@ -53,9 +53,8 @@ - org.slf4j - slf4j-log4j12 - ${slf4j.version} + org.apache.logging.log4j + log4j-core @@ -69,6 +68,11 @@ commons-cli ${commons-cli.version} + + + com.redhat.cli-java + lib + @@ -103,6 +107,10 @@ JAMQ7 + + true + + ${jamq7.version} @@ -186,137 +194,15 @@ - - local - - false - - - 2.0.0-SNAPSHOT - - - - org.apache.activemq - artemis-jms-client - ${library.version} - - - - org.apache.activemq - artemis-core-client - ${library.version} - - - - org.apache.activemq - artemis-commons - ${library.version} - - - - - org.apache.activemq - activemq-broker - ${activemq.version} - - - - org.apache.activemq - activemq-runtime-config - ${activemq.version} - - - - - - org.apache.maven.plugins - maven-install-plugin - ${plugin.install.version} - - - install-external-artemis-jms-client - clean - - ${project.basedir}/libs/artemis-jms-client-2.0.0-SNAPSHOT.jar - default - org.apache.activemq - artemis-jms-client - ${library.version} - jar - true - - - install-file - - - - install-external-artemis-core-client - clean - - ${project.basedir}/libs/artemis-core-client-2.0.0-SNAPSHOT.jar - default - org.apache.activemq - artemis-core-client - ${library.version} - jar - true - - - install-file - - - - install-external-artemis-commons - clean - - ${project.basedir}/libs/artemis-commons-2.0.0-SNAPSHOT.jar - default - org.apache.activemq - artemis-commons - ${library.version} - jar - true - - - install-file - - - - - - - + + + + + org.apache.maven.plugins + maven-shade-plugin + + + - - - Delivery - - false - - - - - maven-assembly-plugin - - - src/main/assembly/dist-assembly.xml - - true - - - - - dist-assembly - package - - single - - - - - - - - diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/Formatter.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/Formatter.java index 7a246775..dbc84d64 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/Formatter.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/Formatter.java @@ -11,4 +11,5 @@ public interface Formatter { * @return string format of given type */ String convertJSON(String string); + void printConvertedJson(String json); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/PythonFormatter.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/PythonFormatter.java index 7a42479e..544ea8d0 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/PythonFormatter.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/formatters/PythonFormatter.java @@ -14,7 +14,7 @@ public class PythonFormatter implements Formatter { */ public String convertJSON(String json) { // TODO for serialization from JSON to java object use jackson json library - json = json.replaceAll("\"", "\'"); + json = json.replaceAll("\"", "'"); json = json.replaceAll(":", ": "); json = json.replaceAll(",'", ", '"); json = json.replaceAll("'?[Nn]one'?", "None"); @@ -22,7 +22,9 @@ public String convertJSON(String json) { json = json.replaceAll("'?[Ff]alse'?", "False"); return json; } - + public void printConvertedJson(String json) { + System.out.println(convertJSON(json)); + } } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/BrokerType.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/BrokerType.java index 61e17aee..5d05ef96 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/BrokerType.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/BrokerType.java @@ -8,9 +8,9 @@ public enum BrokerType { ARTEMIS("service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi", "amq", "artemis"), ACTIVEMQ("service:jmx:rmi:///jndi/rmi://localhost:1099/karaf-root", "amq", "activemq"); - private String defaultJMXURL; - private String defaultBrokerName; - private String defaultUpstreamName; + private final String defaultJMXURL; + private final String defaultBrokerName; + private final String defaultUpstreamName; BrokerType(String defaultJMXURL, String defaultBrokerName, String upstreamName) { this.defaultJMXURL = defaultJMXURL; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/LogConfigurator.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/LogConfigurator.java deleted file mode 100644 index 377c0acb..00000000 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/LogConfigurator.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.redhat.amqx.main; - -import org.apache.log4j.PropertyConfigurator; - -import java.util.Properties; - -/** - * Utility class to configure log messages - */ -public class LogConfigurator { - /** - * Restricted constructor - */ - private LogConfigurator() { - } - - - private static void configureCommon(Properties properties) { - properties.setProperty("log4j.appender.stdout", "org.apache.log4j.ConsoleAppender"); - properties.setProperty("log4j.appender.stdout.Target", "System.out"); - properties.setProperty("log4j.appender.stdout.layout", "org.apache.log4j.PatternLayout"); - properties.setProperty("log4j.appender.stdout.layout.ConversionPattern", "%m%n"); - } - - private static void configureTrace(Properties properties) { - properties.setProperty("log4j.rootLogger", "TRACE, stdout"); - } - - private static void configureDebug(Properties properties) { - properties.setProperty("log4j.rootLogger", "DEBUG, stdout"); - } - - private static void configureInfo(Properties properties) { - properties.setProperty("log4j.rootLogger", "INFO, stdout"); - } - - private static void configureError(Properties properties) { - properties.setProperty("log4j.rootLogger", "ERROR, stdout"); - } - - - /** - * Configure the output to be at trace level - */ - public static void trace() { - Properties properties = new Properties(); - - configureCommon(properties); - configureTrace(properties); - - PropertyConfigurator.configure(properties); - } - - /** - * Configure the output to be at debug level - */ - public static void debug() { - Properties properties = new Properties(); - - configureCommon(properties); - configureDebug(properties); - PropertyConfigurator.configure(properties); - } - - /** - * Configure the output to be at info (info) level - */ - public static void info() { - Properties properties = new Properties(); - - configureCommon(properties); - configureInfo(properties); - PropertyConfigurator.configure(properties); - } - - /** - * Configure the output to be as error as possible - */ - public static void error() { - Properties properties = new Properties(); - - configureCommon(properties); - configureError(properties); - - PropertyConfigurator.configure(properties); - } -} diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/Main.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/Main.java index eae10302..f9683ddc 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/Main.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/Main.java @@ -19,7 +19,7 @@ public class Main { private static void help(int code) { System.out.println("Usage: amqx --host : --action [--name ]\n"); - System.out.printf("Objects: {%s, %s, %s, %s}\n", DESTINATION, JMS_TOPIC, ADDRESS, BROKER, DIVERT); + System.out.printf("Objects: {%s, %s, %s, %s, %s}\n", DESTINATION, JMS_TOPIC, ADDRESS, BROKER, DIVERT); System.out.printf("Actions: Destination {%s, %s, %s, %s}\n", QueueAction.ADD_ACTION, QueueAction.REMOVE_ACTION, QueueAction.LIST_ACTION, QueueAction.PROPERTIES_ACTION); // TODO finish broker actions diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/NodeType.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/NodeType.java index af47de6b..2cca2f27 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/NodeType.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/NodeType.java @@ -9,7 +9,7 @@ public enum NodeType { QUEUE("Queue"), DIVERT("Divert"); - private String nodeTypeString; + private final String nodeTypeString; NodeType(final String nodeTypeString) { this.nodeTypeString = nodeTypeString; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AbstractAction.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AbstractAction.java index 44021ad6..5739e461 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AbstractAction.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AbstractAction.java @@ -1,6 +1,6 @@ package com.redhat.amqx.main.actions; -import com.redhat.amqx.main.LogConfigurator; +import com.redhat.mqe.lib.LogConfigurator; import com.redhat.amqx.management.Credentials; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.HelpFormatter; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AddressAction.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AddressAction.java index 2778b4f2..37d7f628 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AddressAction.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/AddressAction.java @@ -64,7 +64,7 @@ protected void processCommand(String[] args) { setCredentials(cmdLine); setLogLevel(cmdLine.getOptionValue("log-level", DEFAULT_LOGGING_LEVEL)); - if (routingType.toLowerCase().equals("multicast")) { + if (routingType.equalsIgnoreCase("multicast")) { routingType = RoutingType.MULTICAST.toString(); } else { routingType = RoutingType.ANYCAST.toString(); diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/DivertAction.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/DivertAction.java index e8d6ccfd..9e901cc7 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/DivertAction.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/main/actions/DivertAction.java @@ -3,15 +3,12 @@ import com.redhat.amqx.main.BrokerType; import com.redhat.amqx.management.ManagerFactory; import com.redhat.amqx.management.artemis.DivertArtemisManager; -import com.redhat.amqx.management.exception.DestinationException; -import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.ParseException; -import javax.print.attribute.standard.Destination; import java.io.IOException; /** diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/AbstractConnectionManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/AbstractConnectionManager.java index 51d1369a..9e5bf7b2 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/AbstractConnectionManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/AbstractConnectionManager.java @@ -19,8 +19,8 @@ */ public abstract class AbstractConnectionManager { private static final Logger logger = LoggerFactory.getLogger(AbstractConnectionManager.class); - private Resolver resolver; - private String brokerName; + private final Resolver resolver; + private final String brokerName; private JMXServiceURL jmxServiceURL; protected MBeanServerConnection mBeanServerConnection; @@ -34,7 +34,7 @@ public AbstractConnectionManager(String url, final Credentials credentials, Stri // else { use default url}; jmxServiceURL = new JMXServiceURL(url); - if (brokerName == null || brokerName.equals("")) { + if (brokerName == null || brokerName.isEmpty()) { this.brokerName = brokerType.getDefaultBrokerName(); } else { this.brokerName = brokerName; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Credentials.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Credentials.java index ba4b4550..10134a1e 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Credentials.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Credentials.java @@ -4,8 +4,8 @@ * Implements basic credentials used for authentication */ public class Credentials { - private String username; - private String password; + private final String username; + private final String password; public Credentials(String username, String password) { this.username = username; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ManagerFactory.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ManagerFactory.java index 074b2b23..3b967232 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ManagerFactory.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ManagerFactory.java @@ -49,7 +49,7 @@ public BrokerManager createBrokerManager(String jmxURL, Credentials credentials, } private String checkBrokerName(String brokerName, BrokerType brokerType) { - if (brokerName == null || brokerName.equals("")) { + if (brokerName == null || brokerName.isEmpty()) { brokerName = brokerType.getDefaultBrokerName(); } return brokerName; @@ -62,7 +62,7 @@ private String checkBrokerName(String brokerName, BrokerType brokerType) { * @return brokerType to be used from parameters */ private BrokerType resolveBrokerType(String brokerTypeName) { - if (brokerTypeName == null || brokerTypeName.equals("")) { + if (brokerTypeName == null || brokerTypeName.isEmpty()) { return BrokerType.ARTEMIS; } else { brokerTypeName = brokerTypeName.toUpperCase().trim(); diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ObjectReader.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ObjectReader.java index b97cfc8b..3014d783 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ObjectReader.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/ObjectReader.java @@ -6,6 +6,14 @@ import org.slf4j.LoggerFactory; import javax.management.AttributeNotFoundException; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanException; +import javax.management.MBeanInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.ReflectionException; +import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -18,7 +26,7 @@ public class ObjectReader { protected static final Logger logger = LoggerFactory.getLogger(ObjectReader.class); - private static List explicitMethodExcludeList; + private static final List explicitMethodExcludeList; static { explicitMethodExcludeList = Arrays.asList("getClass", "getProxyClass", "isProxyClass", "getInvocationHandler"); @@ -100,7 +108,10 @@ private Map extractMethodProperty(Method method, Object object, throw new DestinationException( String.format("Unable to access '%s' of '%s' object!", object.getClass(), method.getName())); } catch (Throwable e) { - logger.debug(e.getCause().toString()); + logger.debug(e.toString()); + if (e.getCause() != null) { + logger.debug(e.getCause().toString()); + } } return methodPropertyMap; } @@ -163,17 +174,48 @@ public Map getObjectProperties(Object object, List exclu for (Method method : object.getClass().getMethods()) { tmpMap = extractMethodProperty(method, object, excludeMethodList); - if (tmpMap != null && tmpMap.size() != 0) { + if (tmpMap != null && !tmpMap.isEmpty()) { propertiesMap.putAll(tmpMap); } } return propertiesMap; } + + public Map getRawObjectProperties(MBeanServerConnection mBeanServerConnection, ObjectName objectName, List excludeMethodList) throws DestinationException { + Map propertiesMap = new HashMap<>(); + + MBeanInfo info; + try { + info = mBeanServerConnection.getMBeanInfo(objectName); + } catch (Throwable e) { + throw new RuntimeException(e); + } + for (MBeanAttributeInfo attributeInfo : info.getAttributes()) { + String attributeName = attributeInfo.getName(); + + // get rid of get/is+lowercase first letter + String propertyName = getPropertyNameByMethod(attributeName, 0); + + Object value = null; + try { + value = mBeanServerConnection.getAttribute(objectName, attributeName); + propertiesMap.put(propertyName, value); + } catch (MBeanException | AttributeNotFoundException | InstanceNotFoundException | ReflectionException | + IOException e) { + + e.printStackTrace(); + } catch (Throwable e) { + logger.debug(e.toString()); + } + } + + return propertiesMap; + } } class ProxyHandler implements InvocationHandler { - private Object delegate; + private final Object delegate; public ProxyHandler(Object delegate) { this.delegate = delegate; diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Resolver.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Resolver.java index 07b60c21..f14436a8 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Resolver.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/Resolver.java @@ -1,5 +1,7 @@ package com.redhat.amqx.management; +import javax.management.ObjectName; + /** * Interface for resolving view management * objects like queues, topics, broker information. @@ -18,6 +20,8 @@ public interface Resolver { U getAddressView(String addressName) throws Exception; + ObjectName getAddressObjectName(String addressName) throws Exception; + V getDivertView(String addressName, String divertName) throws Exception; } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/AbstractActiveMQManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/AbstractActiveMQManager.java index 905fca59..091ff677 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/AbstractActiveMQManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/AbstractActiveMQManager.java @@ -28,7 +28,7 @@ abstract class AbstractActiveMQManager extends AbstractConnectionManager { private static final Logger logger = LoggerFactory.getLogger(AbstractActiveMQManager.class); private static final ObjectReader objectReader = new ObjectReader(); - protected Formatter formatter = new PythonFormatter(); + protected final Formatter formatter = new PythonFormatter(); public AbstractActiveMQManager(String url, final Credentials credentials, String brokerName, String hostname) throws IOException { super(url, credentials, brokerName, BrokerType.ACTIVEMQ, hostname); diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/ActiveMQResolver.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/ActiveMQResolver.java index 900870f8..39b6fb1c 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/ActiveMQResolver.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/ActiveMQResolver.java @@ -14,9 +14,9 @@ */ public class ActiveMQResolver implements Resolver { private static final String DEFAULT_DOMAIN = "org.apache.activemq"; - private String brokerDomain; - private String brokerName; - private MBeanServerConnection mBeanServerConnection; + private final String brokerDomain; + private final String brokerName; + private final MBeanServerConnection mBeanServerConnection; public ActiveMQResolver(MBeanServerConnection mBeanServerConnection, String brokerName) { this.mBeanServerConnection = mBeanServerConnection; @@ -57,6 +57,12 @@ public QueueViewMBean getAddressView(String queueName) throws Exception { return null; } + @Override + public ObjectName getAddressObjectName(String addressName) throws Exception { + System.err.println("Does not exist for AMQ!"); + return null; + } + @Override public QueueViewMBean getDivertView(String addressName, String divertName) throws Exception { System.err.println("Does not exist for AMQ!"); diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/QueueActiveMQManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/QueueActiveMQManager.java index 81c94623..8176886f 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/QueueActiveMQManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/QueueActiveMQManager.java @@ -1,6 +1,5 @@ package com.redhat.amqx.management.activemq; -import com.redhat.amqx.formatters.PythonFormatter; import com.redhat.amqx.management.Credentials; import com.redhat.amqx.management.DestinationManager; import com.redhat.amqx.management.exception.DestinationException; @@ -9,7 +8,6 @@ import org.apache.activemq.broker.jmx.BrokerViewMBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.reflect.generics.reflectiveObjects.NotImplementedException; import javax.management.ObjectName; import java.io.IOException; @@ -38,9 +36,9 @@ public boolean destinationExists(String name) throws Exception { @Override public void listDestinations(boolean isVerbose) throws Exception { if (isVerbose) { - logger.info(formatter.convertJSON(new JSONObject(listDestinationsWithProperties()).toString())); + formatter.printConvertedJson(new JSONObject(listDestinationsWithProperties()).toString()); } else { - logger.info(formatter.convertJSON(new JSONArray(listDestinationsWithoutProperties()).toString())); + formatter.printConvertedJson(new JSONArray(listDestinationsWithoutProperties()).toString()); } } @@ -64,7 +62,7 @@ public void removeDestination(final String destinationName, String addressName) @Override public void removeMessages(String destinationName, String addressName) throws Exception { - throw new NotImplementedException(); + throw new UnsupportedOperationException("Not implemented yet"); } @Override @@ -80,7 +78,7 @@ public void addDestination(final String destinationName, boolean durable, String @Override public void addDestination(String destinationName, boolean durable, String addressName, String selector, int maxConsumers, boolean deleteOnNoConsumers) throws Exception { - throw new NotImplementedException(); + throw new UnsupportedOperationException("Not implemented yet"); } protected String getFormattedDestinationProperties(String destinationName) throws Exception { @@ -93,7 +91,7 @@ protected String getFormattedDestinationProperties(String destinationName) throw @Override public void getDestinationProperties(final String addressName, final String queueName) throws Exception { if (destinationExists(queueName)) { - logger.info(formatter.convertJSON(getFormattedDestinationProperties(queueName))); + formatter.printConvertedJson(getFormattedDestinationProperties(queueName)); } else { throw new DestinationException("Queue '" + queueName + "' does not exist"); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/TopicActiveMQManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/TopicActiveMQManager.java index 1a499e77..1189dda7 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/TopicActiveMQManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/activemq/TopicActiveMQManager.java @@ -1,6 +1,5 @@ package com.redhat.amqx.management.activemq; -import com.redhat.amqx.formatters.PythonFormatter; import com.redhat.amqx.main.NodeType; import com.redhat.amqx.management.Credentials; import com.redhat.amqx.management.DestinationManager; @@ -11,7 +10,6 @@ import org.apache.activemq.broker.jmx.BrokerViewMBean; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.reflect.generics.reflectiveObjects.NotImplementedException; import javax.management.ObjectName; import java.io.IOException; @@ -23,7 +21,7 @@ */ public class TopicActiveMQManager extends AbstractActiveMQManager implements DestinationManager { private static final Logger logger = LoggerFactory.getLogger(TopicActiveMQManager.class); - private NodeType topicType = NodeType.TOPIC; + private final NodeType topicType = NodeType.TOPIC; public TopicActiveMQManager(final String url, final Credentials credentials, final String brokerName, final String host) throws IOException { @@ -52,9 +50,9 @@ protected ObjectName[] getObjectNames() throws Exception { @Override public void listDestinations(boolean isVerbose) throws Exception { if (isVerbose) { - logger.info(formatter.convertJSON(new JSONObject(listDestinationsWithProperties()).toString())); + formatter.printConvertedJson(new JSONObject(listDestinationsWithProperties()).toString()); } else { - logger.info(formatter.convertJSON(new JSONArray(listDestinationsWithoutProperties()).toString())); + formatter.printConvertedJson(new JSONArray(listDestinationsWithoutProperties()).toString()); } } @@ -72,7 +70,7 @@ public void removeDestination(final String destinationName, String addressName) @Override public void removeMessages(String destinationName, String addressName) throws Exception { - throw new NotImplementedException(); + throw new UnsupportedOperationException("Not implemented yet"); } @Override @@ -88,13 +86,13 @@ public void addDestination(final String destinationName, boolean durable, String @Override public void addDestination(String destinationName, boolean durable, String addressName, String selector, int maxConsumers, boolean deleteOnNoConsumers) throws Exception { - throw new NotImplementedException(); + throw new UnsupportedOperationException("Not implemented yet"); } @Override public void getDestinationProperties(final String addressName, final String topicName) throws Exception { if (destinationExists(topicName)) { - logger.info(formatter.convertJSON(getFormattedDestinationProperties(topicName))); + formatter.printConvertedJson(getFormattedDestinationProperties(topicName)); } else { throw new DestinationException("Topic '" + topicName + "' does not exist"); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AbstractArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AbstractArtemisManager.java index 12dfe71a..68fcb380 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AbstractArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AbstractArtemisManager.java @@ -8,7 +8,6 @@ import com.redhat.amqx.management.AbstractConnectionManager; import com.redhat.amqx.management.Credentials; import com.redhat.amqx.management.Resolver; -import org.apache.activemq.artemis.api.core.ActiveMQAddressDoesNotExistException; import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl; import org.apache.activemq.artemis.api.core.management.AddressControl; import org.apache.activemq.artemis.api.core.management.DivertControl; @@ -20,6 +19,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.management.ObjectName; import java.io.IOException; import java.util.*; @@ -30,7 +30,7 @@ public class AbstractArtemisManager extends AbstractConnectionManager { protected static final Logger logger = LoggerFactory.getLogger(AbstractArtemisManager.class); private static final ObjectReader objectReader = new ArtemisObjectReader(); - protected Formatter formatter = new PythonFormatter(); + protected final Formatter formatter = new PythonFormatter(); public AbstractArtemisManager(String url, Credentials credentials, String brokerName, String hostname) throws IOException { super(url, credentials, brokerName, BrokerType.ARTEMIS, hostname); @@ -66,7 +66,7 @@ protected QueueControl getTopicControlMBean(String addressName, String queueName /** * Get topics MULTICAST routing type "queues" from Artemis broker * - * @return List of multicast addresses + * @return Map of multicast addresses * @throws Exception */ protected Map getTopics() throws Exception { @@ -74,7 +74,7 @@ protected Map getTopics() throws Exception { for (String addr : getAddresses()) { AddressControl ac = (AddressControl) getResolver().getAddressView(addr); - for (String queue : Arrays.asList(ac.getQueueNames())) { + for (String queue : ac.getQueueNames()) { if (getServerControlMBean().getAddressInfo(addr).contains(RoutingType.MULTICAST.toString())) { topics.put(queue, addr); } @@ -94,7 +94,7 @@ protected Map getQueues() throws Exception { for (String addr : getAddresses()) { AddressControl ac = (AddressControl) getResolver().getAddressView(addr); - for (String queue : Arrays.asList(ac.getQueueNames())) { + for (String queue : ac.getQueueNames()) { if (getServerControlMBean().getAddressInfo(addr).contains(RoutingType.ANYCAST.toString())) { queues.put(queue, addr); } @@ -176,8 +176,8 @@ public Map getDestinationProperties(String addressName, String d } } - AddressControl addressControl = (AddressControl) getResolver().getAddressView(addressName); - propertiesMap.putAll(objectReader.getObjectProperties(addressControl, excludeMethods)); + ObjectName addressObjectName = getResolver().getAddressObjectName(addressName); + propertiesMap.putAll(objectReader.getRawObjectProperties(mBeanServerConnection, addressObjectName, excludeMethods)); propertiesMap.put("address-settings", new JSONObject(getServerControlMBean().getAddressSettingsAsJSON(addressName))); return propertiesMap; } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AddressArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AddressArtemisManager.java index 468e19fa..475c6f12 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AddressArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/AddressArtemisManager.java @@ -5,7 +5,6 @@ import com.redhat.amqx.management.DestinationManager; import com.redhat.amqx.management.exception.DestinationException; import org.apache.activemq.artemis.api.core.ActiveMQAddressDoesNotExistException; -import org.apache.activemq.artemis.api.core.RoutingType; import org.json.JSONArray; import org.json.JSONObject; @@ -43,7 +42,7 @@ public void listDestinations(boolean isVerbose) throws Exception { } else { reportJson = new JSONArray(addressNames).toString(); } - logger.info(formatter.convertJSON(reportJson)); + formatter.printConvertedJson(reportJson); } /** @@ -102,7 +101,7 @@ public void addDestination(String destinationName, boolean durable, String addre @Override public void getDestinationProperties(String addressName, String unused) throws Exception { if (destinationExists(addressName)) { - logger.info(formatter.convertJSON(new JSONObject(getDestinationProperties(addressName, null, NodeType.ADDRESS)).toString())); + formatter.printConvertedJson(new JSONObject(getDestinationProperties(addressName, null, NodeType.ADDRESS)).toString()); } else { throw new DestinationException(String.format("Address '%s' does not exist!", addressName)); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/ArtemisResolver.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/ArtemisResolver.java index c359c091..e33b0c56 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/ArtemisResolver.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/ArtemisResolver.java @@ -4,6 +4,7 @@ import org.apache.activemq.artemis.api.core.SimpleString; import org.apache.activemq.artemis.api.core.management.*; import org.apache.activemq.artemis.api.core.RoutingType; +import org.jetbrains.annotations.NotNull; import javax.management.MBeanServerConnection; import javax.management.MBeanServerInvocationHandler; @@ -14,12 +15,14 @@ */ public class ArtemisResolver implements Resolver { private final String DEFAULT_DOMAIN = "org.apache.activemq.artemis"; - private MBeanServerConnection mBeanServerConnection; - private String brokerName; + private final MBeanServerConnection mBeanServerConnection; + private final String brokerName; + private final ObjectNameBuilder objectNameBuilder; public ArtemisResolver(MBeanServerConnection mBeanServerConnection, String brokerName) { this.mBeanServerConnection = mBeanServerConnection; this.brokerName = brokerName; + this.objectNameBuilder = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true); } @Override @@ -29,40 +32,44 @@ public String getBrokerName() { @Override public ActiveMQServerControl getBrokerView() throws Exception { - ObjectName objectName = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getActiveMQServerObjectName(); // 1.0 org.apache.activemq.artemis:module=Core,type=Server // 1.2 org.apache.activemq.artemis:type=Broker,brokerName="",module=Core,ServerType=Server"; // 1.5.1 org.apache.activemq.artemis:type=Broker,brokerName="amq",serviceType=Address,name="queue-anycast2" // 2.0 org.apache.activemq.artemis:broker="",component=addresses,address="",subcomponent=queues,routing-type="",queue="" - objectName = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getActiveMQServerObjectName(); + ObjectName objectName = objectNameBuilder.getActiveMQServerObjectName(); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectName, ActiveMQServerControl.class, false); } @Override public QueueControl getQueueView(String addressName, String queueName) throws Exception { - ObjectName objectName = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getQueueObjectName(new SimpleString(addressName), new SimpleString(queueName), RoutingType.ANYCAST); + ObjectName objectName = objectNameBuilder.getQueueObjectName(new SimpleString(addressName), new SimpleString(queueName), RoutingType.ANYCAST); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectName, QueueControl.class, false); } @Override public QueueControl getTopicView(String addressName, String topicName) throws Exception { // if address doesn't add RoutingType.MULTICAST, it does not have any type - ObjectName objectName = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getQueueObjectName(new SimpleString(addressName), new SimpleString(topicName), RoutingType.MULTICAST); + ObjectName objectName = objectNameBuilder.getQueueObjectName(new SimpleString(addressName), new SimpleString(topicName), RoutingType.MULTICAST); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectName, QueueControl.class, false); } public AcceptorControl getAcceptorView(String acceptorName) throws Exception { - ObjectName objectname = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getAcceptorObjectName(acceptorName); + ObjectName objectname = objectNameBuilder.getAcceptorObjectName(acceptorName); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectname, AcceptorControl.class, false); } public AddressControl getAddressView(String addressName) throws Exception { - ObjectName objectname = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getAddressObjectName(new SimpleString(addressName)); + ObjectName objectname = getAddressObjectName(addressName); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectname, AddressControl.class, false); } + @NotNull + public ObjectName getAddressObjectName(String addressName) throws Exception { + return objectNameBuilder.getAddressObjectName(new SimpleString(addressName)); + } + public DivertControl getDivertView(String addressName, String divertName) throws Exception { - ObjectName objectname = ObjectNameBuilder.create(DEFAULT_DOMAIN, brokerName, true).getDivertObjectName(divertName, addressName); + ObjectName objectname = objectNameBuilder.getDivertObjectName(divertName, addressName); return MBeanServerInvocationHandler.newProxyInstance(mBeanServerConnection, objectname, DivertControl.class, false); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/BrokerArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/BrokerArtemisManager.java index e79a7ba0..3c222b3f 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/BrokerArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/BrokerArtemisManager.java @@ -43,24 +43,24 @@ public void reload() throws MalformedObjectNameException { @Override public void getTransportConnectors() throws Exception { ActiveMQServerControl serverControl = getResolver(ArtemisResolver.class).getBrokerView(); - logger.info(formatter.convertJSON(serverControl.getConnectorsAsJSON())); - logger.info(formatter.convertJSON(new JSONObject(serverControl.getConnectors()).toString())); - logger.info(formatter.convertJSON(new JSONArray(serverControl.getAddressNames()).toString())); + formatter.printConvertedJson(serverControl.getConnectorsAsJSON()); + formatter.printConvertedJson(new JSONObject(serverControl.getConnectors()).toString()); + formatter.printConvertedJson(new JSONArray(serverControl.getAddressNames()).toString()); } @Override public void getNetworkTopology() throws Exception { ActiveMQServerControl serverControl = getResolver(ArtemisResolver.class).getBrokerView(); - logger.info(formatter.convertJSON(serverControl.listNetworkTopology())); + formatter.printConvertedJson(serverControl.listNetworkTopology()); } @Override public void getSessions(String connectionId) throws Exception { ActiveMQServerControl serverControl = getResolver(ArtemisResolver.class).getBrokerView(); if (connectionId == null) { - logger.info(formatter.convertJSON(serverControl.listAllSessionsAsJSON())); + formatter.printConvertedJson(serverControl.listAllSessionsAsJSON()); } else { - logger.info(formatter.convertJSON(serverControl.listSessionsAsJSON(connectionId))); + formatter.printConvertedJson(serverControl.listSessionsAsJSON(connectionId)); } } @@ -75,7 +75,7 @@ public void getAllBrokerDestinations() throws Exception { allDestinationsMap.put("address", new JSONArray(getAddresses())); allDestinationsMap.put("queue", new JSONArray(getQueues().keySet())); allDestinationsMap.put("topic", new JSONArray(getTopics().keySet())); - logger.info(formatter.convertJSON(new JSONObject(allDestinationsMap).toString())); + formatter.printConvertedJson(new JSONObject(allDestinationsMap).toString()); } public void getAllBrokerDestinationsProperties() throws Exception { @@ -94,7 +94,7 @@ public void getAllBrokerDestinationsProperties() throws Exception { allDestinationsMap.put(address, getDestinationProperties(address, null, NodeType.ADDRESS)); } - logger.info(formatter.convertJSON(new JSONObject(allDestinationsMap).toString())); + formatter.printConvertedJson(new JSONObject(allDestinationsMap).toString()); } } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DestinationArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DestinationArtemisManager.java index 61ca05b8..77e083f0 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DestinationArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DestinationArtemisManager.java @@ -43,7 +43,7 @@ public void listDestinations(boolean isVerbose) throws Exception { } else { reportJson = new JSONArray(queueNames.keySet()).toString(); } - logger.info(formatter.convertJSON(reportJson)); + formatter.printConvertedJson(reportJson); } @Override @@ -135,7 +135,7 @@ public void getDestinationProperties(String addressName, String destinationName) addressName = destinationName; } if (destinationExists(destinationName)) { - logger.info(formatter.convertJSON(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.QUEUE)).toString())); + formatter.printConvertedJson(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.QUEUE)).toString()); } else { throw new DestinationException(String.format("Queue '%s' does not exist!", destinationName)); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DivertArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DivertArtemisManager.java index 5513c2a7..773f2554 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DivertArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/DivertArtemisManager.java @@ -2,22 +2,14 @@ import com.redhat.amqx.main.NodeType; import com.redhat.amqx.management.Credentials; -import com.redhat.amqx.management.DestinationManager; import com.redhat.amqx.management.exception.DestinationException; -import org.apache.activemq.artemis.api.core.ActiveMQAddressDoesNotExistException; -import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException; -import org.apache.activemq.artemis.api.core.RoutingType; import org.apache.activemq.artemis.api.core.management.AddressControl; -import org.apache.activemq.artemis.api.core.management.DivertControl; import org.json.JSONArray; import org.json.JSONObject; -import sun.reflect.generics.reflectiveObjects.NotImplementedException; import java.io.IOException; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** * Destination object management for Apache Artemis/AMQ7 broker. @@ -57,7 +49,7 @@ public void listDiverts(boolean isVerbose) throws Exception { } else { reportJson = new JSONArray(divertNames).toString(); } - logger.info(formatter.convertJSON(reportJson)); + formatter.printConvertedJson(reportJson); } @@ -109,7 +101,7 @@ public void getDestinationProperties(String addressName, String destinationName) addressName = destinationName; } if (divertExists(destinationName)) { - logger.info(formatter.convertJSON(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.DIVERT)).toString())); + formatter.printConvertedJson(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.DIVERT)).toString()); } else { throw new DestinationException(String.format("Divert '%s' does not exist!", destinationName)); } diff --git a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/TopicArtemisManager.java b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/TopicArtemisManager.java index 6881a3e5..ae462941 100644 --- a/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/TopicArtemisManager.java +++ b/cli-activemq-jmx/src/main/java/com/redhat/amqx/management/artemis/TopicArtemisManager.java @@ -42,7 +42,7 @@ public void listDestinations(boolean isVerbose) throws Exception { } else { reportJson = new JSONArray(topicNames.keySet()).toString(); } - logger.info(formatter.convertJSON(reportJson)); + formatter.printConvertedJson(reportJson); } @Override @@ -124,7 +124,7 @@ public void getDestinationProperties(String addressName, String destinationName) addressName = destinationName; } if (destinationExists(destinationName)) { - logger.info(formatter.convertJSON(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.TOPIC)).toString())); + formatter.printConvertedJson(new JSONObject(getDestinationProperties(addressName, destinationName, NodeType.TOPIC)).toString()); } else { throw new DestinationException(String.format("Topic '%s' does not exist!", destinationName)); } diff --git a/cli-activemq-jmx/src/test/kotlin/MainTest.kt b/cli-activemq-jmx/src/test/kotlin/MainTest.kt index b6df0fcd..07d9b9c6 100644 --- a/cli-activemq-jmx/src/test/kotlin/MainTest.kt +++ b/cli-activemq-jmx/src/test/kotlin/MainTest.kt @@ -1,6 +1,8 @@ +import org.junit.jupiter.api.Tag import org.junit.jupiter.api.Test class AmqxMain { + @Tag("external") @Test fun `queue properties`() { com.redhat.amqx.main.Main.main( diff --git a/cli-activemq/pom.xml b/cli-activemq/pom.xml index e1b24729..91e5fa97 100644 --- a/cli-activemq/pom.xml +++ b/cli-activemq/pom.xml @@ -35,7 +35,7 @@ aoc com.redhat.mqe.aoc.Main - 5.16.2 + 6.1.3 ${activemq.client.version} @@ -60,11 +60,22 @@ test-jar test + + com.redhat.cli-java + jmslib + test-jar + test + com.google.truth truth test + + org.apache.activemq + artemis-server + test + diff --git a/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocClientOptionManager.java b/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocClientOptionManager.java index 8791bf73..f676ddff 100644 --- a/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocClientOptionManager.java +++ b/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocClientOptionManager.java @@ -205,7 +205,7 @@ private String relativize(String p) { /** * Does not do any checking. Trusts all certificates. */ - private class TrustingTrustManager implements X509TrustManager { + private static class TrustingTrustManager implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { // trust anything diff --git a/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocConnectionManager.java b/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocConnectionManager.java index c085e75d..a8405353 100644 --- a/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocConnectionManager.java +++ b/cli-activemq/src/main/java/com/redhat/mqe/aoc/AocConnectionManager.java @@ -36,10 +36,10 @@ class AocConnectionManager extends ConnectionManager { private static final String EXTERNAL_JNDI_PROPERTY = "aoc7.jndi"; protected InitialContext initialContext; - private String queueOrTopic = "amqQueue"; - private String amqConnectionFactoryJNDI = "amqFactory"; + private final String queueOrTopic = "amqQueue"; + private final String amqConnectionFactoryJNDI = "amqFactory"; private Properties properties; - private Logger LOG = LoggerFactory.getLogger(AocConnectionManager.class.getName()); + private final Logger LOG = LoggerFactory.getLogger(AocConnectionManager.class.getName()); AocConnectionManager(ClientOptions clientOptions, String connectionFactory) { if (clientOptions.getOption(ClientOptions.USERNAME).hasParsedValue()) { diff --git a/cli-activemq/src/main/resources/log4j.properties b/cli-activemq/src/main/resources/log4j2.properties similarity index 55% rename from cli-activemq/src/main/resources/log4j.properties rename to cli-activemq/src/main/resources/log4j2.properties index 968f060d..a91e958c 100644 --- a/cli-activemq/src/main/resources/log4j.properties +++ b/cli-activemq/src/main/resources/log4j2.properties @@ -17,20 +17,15 @@ # limitations under the License. # -# Set root logger level to DEBUG and its only appender to console -# This sets ALL the logs to given level. -log4j.rootLogger=WARN, console +rootLogger=WARN, console -log4j.logger.com.redhat.mqe.lib=INFO, console -log4j.additivity.com.redhat.mqe.lib=false +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO -log4j.logger.io.netty=WARN, console -log4j.logger.netty=WARN, console +logger.netty.name=io.netty +logger.netty.level=WARN -# Appender "console" settings -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.target=System.err -log4j.appender.console.Threshold=all -log4j.appender.console.layout=org.apache.log4j.PatternLayout -#log4j.appender.console.layout.ConversionPattern=%t %d %p [%c{4}] %m%n -log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %m%n +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-artemis-jms/pom.xml b/cli-artemis-jms/pom.xml index 143cb364..4a25bfe4 100644 --- a/cli-artemis-jms/pom.xml +++ b/cli-artemis-jms/pom.xml @@ -36,7 +36,7 @@ acc com.redhat.mqe.acc.Main false - 2.17.0 + 2.53.0 ${artemis.jms.client.version} @@ -68,7 +68,7 @@ com.google.guava guava - 30.1.1-android + 33.2.1-android provided diff --git a/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccClientOptionManager.java b/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccClientOptionManager.java index e7fc0c0e..592082a4 100644 --- a/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccClientOptionManager.java +++ b/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccClientOptionManager.java @@ -27,7 +27,7 @@ import java.util.Map; public class AccClientOptionManager extends ClientOptionManager { - private Map CTL_SSL_OPTIONS = new HashMap<>(); + private final Map CTL_SSL_OPTIONS = new HashMap<>(); { // Core diff --git a/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccConnectionManager.java b/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccConnectionManager.java index 188720ea..61bd6bdb 100644 --- a/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccConnectionManager.java +++ b/cli-artemis-jms/src/main/java/com/redhat/mqe/acc/AccConnectionManager.java @@ -37,7 +37,7 @@ public class AccConnectionManager extends ConnectionManager { // TODO remove initialcontext from the superclass - private Logger LOG = LoggerFactory.getLogger(AccConnectionManager.class.getName()); + private final Logger LOG = LoggerFactory.getLogger(AccConnectionManager.class.getName()); AccConnectionManager(final ClientOptions clientOptions, String brokerUrl) { super(); diff --git a/cli-qpid-jms/src/test/resources/log4j.properties b/cli-artemis-jms/src/main/resources/log4j2.properties similarity index 73% rename from cli-qpid-jms/src/test/resources/log4j.properties rename to cli-artemis-jms/src/main/resources/log4j2.properties index 128ef5e8..be82b864 100644 --- a/cli-qpid-jms/src/test/resources/log4j.properties +++ b/cli-artemis-jms/src/main/resources/log4j2.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Red Hat, Inc. +# Copyright (c) 2017 Red Hat, Inc. # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with @@ -16,3 +16,15 @@ # See the License for the specific language governing permissions and # limitations under the License. # +rootLogger=WARN, console + +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO + +logger.netty.name=io.netty +logger.netty.level=WARN + +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Client.java b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Client.java index 830d7305..b235fdf0 100644 --- a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Client.java +++ b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Client.java @@ -23,8 +23,9 @@ import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; @@ -36,9 +37,9 @@ import static java.util.Arrays.asList; abstract class Client { - private Logger log = setUpLogger("Client"); + private final Logger log = setUpLogger("Client"); - OptionParser parser = new OptionParser(); + final OptionParser parser = new OptionParser(); OptionSpec destination; OptionSpec clientId; @@ -76,7 +77,7 @@ abstract class Client { Integer cliKeepAlive; Boolean cliReconnect; - AmcMessageFormatter messageFormatter = new AmcMessageFormatter(); + final AmcMessageFormatter messageFormatter = new AmcMessageFormatter(); Client(String[] args) { populateOptionParser(parser); @@ -238,7 +239,7 @@ static void closeClient(MqttClient client) throws MqttException { } protected Logger setUpLogger(String name) { - Logger log = Logger.getLogger(name); + org.apache.logging.log4j.core.Logger log = (org.apache.logging.log4j.core.Logger) LogManager.getLogger(name); log.setLevel(Level.WARN); return log; } diff --git a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Receiver.java b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Receiver.java index e66f16fe..f8cb1332 100644 --- a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Receiver.java +++ b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Receiver.java @@ -20,12 +20,12 @@ package com.redhat.mqe.amc; import joptsimple.OptionParser; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.*; public class Receiver extends Client implements MqttCallback { - private Logger log = setUpLogger("Receiver"); + private final Logger log = setUpLogger("Receiver"); private MqttClient receiver = null; diff --git a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Sender.java b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Sender.java index 00c88e0c..24c32189 100644 --- a/cli-paho-java/src/main/java/com/redhat/mqe/amc/Sender.java +++ b/cli-paho-java/src/main/java/com/redhat/mqe/amc/Sender.java @@ -28,7 +28,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; -import org.apache.log4j.Logger; +import org.apache.logging.log4j.Logger; /** @@ -38,8 +38,8 @@ */ public class Sender extends Client { OptionSpec content; - MemoryPersistence persistence = new MemoryPersistence(); - private Logger log = setUpLogger("Sender"); + final MemoryPersistence persistence = new MemoryPersistence(); + private final Logger log = setUpLogger("Sender"); private MqttClient sender = null; diff --git a/cli-paho-java/src/main/resources/log4j.properties b/cli-paho-java/src/main/resources/log4j2.properties similarity index 53% rename from cli-paho-java/src/main/resources/log4j.properties rename to cli-paho-java/src/main/resources/log4j2.properties index 532b055b..55ea0f46 100644 --- a/cli-paho-java/src/main/resources/log4j.properties +++ b/cli-paho-java/src/main/resources/log4j2.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018 Red Hat, Inc. +# Copyright (c) 2022 Red Hat, Inc. # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with @@ -16,17 +16,19 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Set root logger level to DEBUG and its only appender to console -# This sets ALL the logs to given level. -log4j.rootLogger=ALL, console -log4j.logger.com.redhat.mqe.lib=ALL, console -log4j.additivity.com.redhat.mqe.lib=false -log4j.logger.io.netty=ALL, console -log4j.logger.netty=ALL, console -# Appender "console" settings -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.target=System.err -log4j.appender.console.Threshold=all -log4j.appender.console.layout=org.apache.log4j.PatternLayout -#log4j.appender.console.layout.ConversionPattern=%t %d %p [%c{4}] %m%n -log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %m%n +rootLogger=ALL, console + +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO + +logger.netty.name=io.netty +logger.netty.level=WARN + +# Log transport.traceBytes=true messages +logger.NettyTcpTransport.name=org.apache.qpid.jms.transports.netty.NettyTcpTransport +logger.NettyTcpTransport.level=DEBUG + +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-protonj2/pom.xml b/cli-protonj2/pom.xml new file mode 100644 index 00000000..0d8600db --- /dev/null +++ b/cli-protonj2/pom.xml @@ -0,0 +1,157 @@ + + + + + 4.0.0 + + + com.redhat.cli-java + parent + 1.2.2-SNAPSHOT + ../parent/pom.xml + + + cli-protonj2 + jar + + + jms + 1.0.0 + com.redhat.mqe.Main + ${protonj2.version} + 2.0.70.Final + linux-x86_64-fedora + 1.6.0 + + + + + + + org.junit-pioneer + junit-pioneer + test + + + org.awaitility + awaitility + test + + + + com.redhat.cli-java + jmslib + + + + org.apache.qpid + protonj2-client + ${protonj2.version} + + + + info.picocli + picocli + 4.7.6 + + + + com.redhat.cli-java + cli + test + + + com.redhat.cli-java + tests + test-jar + test + + + com.google.truth + truth + test + + + + org.apache.activemq + artemis-amqp-protocol + test + + + com.redhat.cli-java + broker + 1.2.2-SNAPSHOT + test-jar + test + + + org.powermock + powermock-reflect + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Connector.java b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Connector.java new file mode 100644 index 00000000..4dda3213 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Connector.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.Connection; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import picocli.CommandLine; + +import java.net.URI; +import java.util.concurrent.Callable; + +@CommandLine.Command( + name = "connector", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Opens AMQP connections" +) +class CliProtonJ2Connector extends CliProtonJ2SenderReceiverConnector implements Callable { + @CommandLine.Option(names = {"-b", "--broker"}, description = "") + private String broker = ""; + + @CommandLine.Option(names = {"-a", "--address"}, description = "") + private String address = ""; + + @CommandLine.Option(names = {"--count"}, description = "") + private int count = 1; + + @Override + public Integer call() throws Exception { + configureLogging(); + + String prefix = ""; + if (!broker.startsWith("amqp://") && !broker.startsWith("amqps://")) { + prefix = "amqp://"; + } + final URI url = new URI(prefix + broker); + final String serverHost = url.getHost(); + int serverPort = url.getPort(); + serverPort = (serverPort == -1) ? 5672 : serverPort; + + final Client client = Client.create(); + + final ConnectionOptions options = getConnectionOptions(); + + try (Connection connection = client.connect(serverHost, serverPort, options)) { + } + + client.close(); + + return 0; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Receiver.java b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Receiver.java new file mode 100644 index 00000000..a7021ebd --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Receiver.java @@ -0,0 +1,342 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import com.redhat.mqe.lib.Utils; +import org.apache.qpid.protonj2.client.Client; +import org.apache.qpid.protonj2.client.ClientOptions; +import org.apache.qpid.protonj2.client.Connection; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import org.apache.qpid.protonj2.client.Delivery; +import org.apache.qpid.protonj2.client.DistributionMode; +import org.apache.qpid.protonj2.client.DurabilityMode; +import org.apache.qpid.protonj2.client.Message; +import org.apache.qpid.protonj2.client.Receiver; +import org.apache.qpid.protonj2.client.ReceiverOptions; +import org.apache.qpid.protonj2.client.Sender; +import org.apache.qpid.protonj2.client.Session; +import org.apache.qpid.protonj2.client.exceptions.ClientException; +import org.apache.qpid.protonj2.types.DescribedType; +import org.apache.qpid.protonj2.types.Symbol; +import org.apache.qpid.protonj2.types.UnknownDescribedType; +import picocli.CommandLine; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static com.redhat.mqe.lib.ClientOptionManager.QUEUE_PREFIX; +import static com.redhat.mqe.lib.ClientOptionManager.TOPIC_PREFIX; + +@CommandLine.Command( + name = "receiver", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Opens AMQP connections" +) +public class CliProtonJ2Receiver extends CliProtonJ2SenderReceiver implements Callable { + + @CommandLine.Option(names = {"-b", "--broker"}, description = "MD5, SHA-1, SHA-256, ...") + private String broker = "MD5"; + + @CommandLine.Option(names = {"--conn-clientid"}) + private String connClientId; + + @CommandLine.Option(names = {"--durable-subscriber"}) + private String durableSubscriberString = "false"; + + @CommandLine.Option(names = {"--durable-subscriber-name"}) + private String durableSubscriberName; + + @CommandLine.Option(names = {"--subscriber-unsubscribe"}) + private String subscriberUnsubscribeString = "false"; + + @CommandLine.Option(names = {"--recv-browse"}, description = "browse queued messages instead of receiving them") + private String recvBrowseString = "false"; + + @CommandLine.Option(names = {"--msg-selector"}, description = "receive only messages matching a server-side selector") + private String selector; + + @CommandLine.Option(names = {"--count"}, description = "MD5, SHA-1, SHA-256, ...") + private int count = 1; + + @CommandLine.Option(names = {"--timeout"}, description = "Timeout in seconds to wait before exiting, it is reset after every successful send/receive/connect") + private int timeout = -1; + + @CommandLine.Option(names = {"--process-reply-to"}) + private boolean processReplyTo = false; + + @CommandLine.Option(names = {"--duration"}) + private Float duration = 0.0f; + + @CommandLine.Option(names = {"--duration-mode"}) + private DurationModeReceiver durationMode = DurationModeReceiver.afterReceive; + + @CommandLine.Option(names = {"--ssn-ack-mode"}) + private SsnAckMode ssnAckMode; + + @CommandLine.Option(names = {"--tx-size"}) + private Integer txSize; + + @CommandLine.Option(names = {"--tx-endloop-action"}) + private TxAction txEndloopAction; + + @CommandLine.Option(names = {"--tx-action"}) + private TxAction txAction; + + @CommandLine.Option(names = {"--msg-content-to-file"}) + private String msgContentToFile; + + @CommandLine.Option(names = {"--conn-prefetch"}) + private Integer connPrefetch; + + public CliProtonJ2Receiver() { + super(); + } + + public CliProtonJ2Receiver(ProtonJ2MessageFormatter messageFormatter) { + super(messageFormatter); + } + + /** + * This is the main function of the client, as called by the cli options handling library. + */ + @Override + public Integer call() throws Exception { + configureLogging(); + + duration *= 1000; // convert to milliseconds + + String prefix = ""; + if (!broker.startsWith("amqp://") && !broker.startsWith("amqps://")) { + prefix = "amqp://"; + } + final URI url = new URI(prefix + broker); + final String serverHost = url.getHost(); + int serverPort = url.getPort(); + serverPort = (serverPort == -1) ? 5672 : serverPort; + + String destinationCapability = "queue"; + if (address.startsWith(TOPIC_PREFIX)) { + address = address.substring((TOPIC_PREFIX.length())); + destinationCapability = "topic"; + } + if (address.startsWith(QUEUE_PREFIX)) { + address = address.substring((QUEUE_PREFIX.length())); + } + + ClientOptions clientOptions = new ClientOptions(); + // TODO api usability I had to hunt for this a bit; the idea is to have durable subscription: need specify connection id and subscriber name + if (connClientId != null) { + clientOptions.id(connClientId); + } + + // TODO api usability; If I use the w/ clientOptions variant of Client.create, then .id defaults to null, and I get exception; + // ok, that just cannot be true ^^^; but it looks to be true; what!?! + // aha, right; constructor does not check, factory method does check for null + // proposed solution: allow null there, and let it mean autoassign; or tell us method to generate ID ourselves if we don't care + Client client; + if (clientOptions.id() != null) { + client = Client.create(clientOptions); + } else { + client = Client.create(); + } + + + final ConnectionOptions options = getConnectionOptions(); + + + + /* + TODO API usability, hard to ask for queue when dealing with broker that likes to autocreate topics + */ + final boolean durableSubscription = stringToBool(durableSubscriberString) || stringToBool(subscriberUnsubscribeString); + + ReceiverOptions receiverOptions = new ReceiverOptions(); + // is it target or source? target. + receiverOptions.sourceOptions().capabilities(destinationCapability); + // TODO: huh, did not know that this is configurable; and it was very hard to find in relation to durable receivers + if (durableSubscription) { + receiverOptions.sourceOptions().durabilityMode(DurabilityMode.UNSETTLED_STATE); + // proton cpp cli does also this +// receiverOptions.sourceOptions().expiryPolicy(ExpiryPolicy.NEVER); // but that seems to happen automatically here + } + + // Selectors are not part of core AMQP, Artemis supports `apache.org:selector-filter:string` extension + // https://www.amqp.org/specification/1.0/filters + if (selector != null && !selector.isEmpty()) { // other java clis ignore empty selector + DescribedType describedType = new UnknownDescribedType(Symbol.getSymbol("apache.org:selector-filter:string"), selector); + receiverOptions.sourceOptions().filters(Map.of("selector", describedType)); + } + + // todo: another usability, little hard to figure out this is analogue of jms to browse queues + if (stringToBool(recvBrowseString)) { + receiverOptions.sourceOptions().distributionMode(DistributionMode.COPY); + } + + // In AMQP, it is one credit means one message, so this matches the semantics + if (connPrefetch != null) { + receiverOptions.creditWindow(connPrefetch); + } + + // TODO: API question: what is difference between autoSettle and autoAccept? why I want one but not the other? + if (ssnAckMode != null) { + if (ssnAckMode == SsnAckMode.client) { + receiverOptions.autoAccept(false); + receiverOptions.autoSettle(false); + } + } + + // TODO: these are constants in client lib + // TODO: These are symbols, not strings + // todo, no, these go to link Attach, I need opts for Open frame +// receiverOptions.desiredCapabilities( +// "sole-connection-for-container", "DELAYED_DELIVERY", "SHARED-SUBS", "ANONYMOUS-RELAY"); +// ClientConstants.SOLE_CONNECTION_CAPABILITY, +// ClientConstants.DELAYED_DELIVERY, +// ClientConstants.SHARED_SUBS, +// ClientConstants.ANONYMOUS_RELAY); +// "sole-connection-for-container", "DELAYED_DELIVERY", "SHARED-SUBS", "ANONYMOUS-RELAY"); + + boolean transacted = txSize != null || txAction != null || txEndloopAction != null; + + try (Connection connection = client.connect(serverHost, serverPort, options); + Session session = connection.openSession()) { + Receiver receiver; + if (durableSubscription) { + receiver = session.openDurableReceiver(address, durableSubscriberName, receiverOptions); + } else { + receiver = session.openReceiver(address, receiverOptions); + } + + if (stringToBool(subscriberUnsubscribeString)) { + receiver.openFuture().get(); // force client to perform attach, so it is forced to send detach afterwards + receiver.close(); + return 0; + } + + if (transacted) { + session.beginTransaction(); + } + + int i = 0; + double initialTimestamp = Utils.getTime(); + while (true) { + + if (durationMode == DurationModeReceiver.beforeReceive) { + Utils.sleepUntilNextIteration(initialTimestamp, count, duration, i + 1); + } + + final Delivery delivery; + delivery = receiver.receive(timeout, TimeUnit.SECONDS); + + if (delivery == null) { + break; + } + + if (durationMode == DurationModeReceiver.afterReceive) { + Utils.sleepUntilNextIteration(initialTimestamp, count, duration, i + 1); + } + + if (processReplyTo && delivery.message().replyTo() != null) { + String replyTo = delivery.message().replyTo(); + Message message = delivery.message(); + message.replyTo(null); + try (Sender sender = connection.openSender(replyTo)) { + sender.send(message); + } + } + + // todo, is this what we mean? + if (ssnAckMode != null && ssnAckMode == SsnAckMode.client) { + delivery.accept(); + } + + outputReceivedMessage(i, delivery); + i++; + + if (txSize != null && txSize != 0) { + if (i % txSize == 0) { + if (txAction != null) { + switch (txAction) { + case commit: + session.commitTransaction(); + break; + case rollback: + session.rollbackTransaction(); + break; + } + + session.beginTransaction(); + + if (durationMode == DurationModeReceiver.afterReceiveTxAction) { + Utils.sleepUntilNextIteration(initialTimestamp, i, duration, i + 1); + } + } + } + } + + if (i == count) { // not i > count; --count=0 needs to disable the break + break; + } + } + + if (txEndloopAction != null) { + switch (txEndloopAction) { + case commit: + session.commitTransaction(); + break; + case rollback: + session.rollbackTransaction(); + break; + } + } else if (transacted) { + session.rollbackTransaction(); + } + + if (durableSubscription) { + receiver.detach(); + } else { + receiver.close(); // TODO want to do autoclosable, need helper func, that's all + } + } + + client.close(); + + return 0; + } + + private void outputReceivedMessage(int i, Delivery delivery) throws ClientException, IOException { + Message message = delivery.message(); + int messageFormat = delivery.messageFormat(); + if (msgContentToFile != null) { + // todo? + Path file = Paths.get(msgContentToFile + "_" + i); + Files.write(file, message.body().toString().getBytes(StandardCharsets.UTF_8)); + } + printMessage(message); + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Sender.java b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Sender.java new file mode 100644 index 00000000..c1df734a --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/CliProtonJ2Sender.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import com.redhat.mqe.lib.Content; +import com.redhat.mqe.lib.Utils; +import org.apache.qpid.protonj2.client.*; +import org.apache.qpid.protonj2.client.exceptions.ClientException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import picocli.CommandLine; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import static com.redhat.mqe.lib.ClientOptionManager.QUEUE_PREFIX; +import static com.redhat.mqe.lib.ClientOptionManager.TOPIC_PREFIX; + +@CommandLine.Command( + name = "sender", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Opens AMQP connections" +) +public class CliProtonJ2Sender extends CliProtonJ2SenderReceiver implements Callable { + + @CommandLine.Option(names = {"-b", "--broker"}, description = "") + private String broker = "MD5"; + + @CommandLine.Option(names = {"--count"}, description = "") + private int count = 1; + + @CommandLine.Option(names = {"--timeout"}, description = "") + private int timeout; + + @CommandLine.Option(names = {"--duration"}) + private Float duration = 0.0f; + + @CommandLine.Option(names = {"--msg-property"}) // picocli Map options works for this, sounds like + private List msgProperties = new ArrayList<>(); + + @CommandLine.Option(names = {"--msg-content"}) + private String msgContent; + + @CommandLine.Option(names = {"--msg-content-from-file"}) + private String msgContentFromFile; + + @CommandLine.Option(names = {"--content-type"}) + private ContentType contentType = ContentType.STRING; + + @CommandLine.Option(names = {"--property-type"}) + private PropertyType propertyType = PropertyType.String; + + @CommandLine.Option(names = {"--msg-durable"}) + private String msgDurableString = "false"; + + @CommandLine.Option(names = {"--msg-ttl"}) + private Long msgTtl; + + // e.g. `--msg-content-list-item --msg-content-list-item "String"` + @CommandLine.Option(names = {"--msg-content-list-item"}, arity = "0..1", fallbackValue = CommandLine.Option.NULL_VALUE) + private List msgContentListItem; + + @CommandLine.Option(names = {"--msg-content-map-item"}) + private List msgContentMapItems; + + @CommandLine.Option(names = {"--msg-content-binary"}) + private String msgContentBinaryString = "false"; + + @CommandLine.Option(names = {"--msg-correlation-id"}) + private String msgCorrelationId; + + @CommandLine.Option(names = {"--msg-group-id"}) + private String msgGroupId; + + @CommandLine.Option(names = {"--msg-id"}) + private String msgId; // todo, not just string is an option + + @CommandLine.Option(names = {"--msg-reply-to"}) + private String msgReplyTo; + + @CommandLine.Option(names = {"--msg-subject"}) + private String msgSubject; + + @CommandLine.Option(names = {"--msg-user-id"}) + private String msgUserId; + + @CommandLine.Option(names = {"--msg-priority"}) + private Short msgPriority; // TODO unsigned byte, actually + + // jms.populateJMSXUserID opt in qpid-jms + // TODO: does not seem to have equivalent; what is the threat model for "prevent spoofing" in JMS docs? + @CommandLine.Option(names = {"--conn-populate-user-id"}) + private String connPopulateUserIdString = "false"; + + @CommandLine.Option(names = {"--msg-group-seq"}) + private Integer msgGroupSeq; + + @CommandLine.Option(names = {"--msg-reply-to-group-id"}) + private String msgReplyToGroupId; + + @CommandLine.Option(names = {"--ssn-ack-mode"}) + private SsnAckMode ssnAckMode; + + @CommandLine.Option(names = {"--tx-size"}) + private Integer txSize; + + @CommandLine.Option(names = {"--tx-endloop-action"}) + private TxAction txEndloopAction; + + @CommandLine.Option(names = {"--tx-action"}) + private TxAction txAction; + + @CommandLine.Option(names = {"--sync-mode"}) + private SyncMode syncMode; + + @CommandLine.Option(names = {"--duration-mode"}) + private DurationModeSender durationMode = DurationModeSender.afterSend; + + public CliProtonJ2Sender() { + super(); + } + + public CliProtonJ2Sender(ProtonJ2MessageFormatter messageFormatter) { + super(messageFormatter); + } + + /** + * This is the main function of the client, as called by the cli options handling library. + */ + @Override + public Integer call() throws Exception { + configureLogging(); + + duration *= 1000; // convert to milliseconds + + String prefix = ""; + if (!broker.startsWith("amqp://") && !broker.startsWith("amqps://")) { + prefix = "amqp://"; + } + final URI url = new URI(prefix + broker); + final String serverHost = url.getHost(); + int serverPort = url.getPort(); + serverPort = (serverPort == -1) ? 5672 : serverPort; + + String destinationCapability = "queue"; + if (address.startsWith(TOPIC_PREFIX)) { + address = address.substring((TOPIC_PREFIX.length())); + destinationCapability = "topic"; + } + if (address.startsWith(QUEUE_PREFIX)) { + address = address.substring((QUEUE_PREFIX.length())); + } + + final Client client = Client.create(); + + final ConnectionOptions options = getConnectionOptions(); + + /* + TODO API usablility, hard to ask for queue when dealing with broker that likes to autocreate topics + */ + SenderOptions senderOptions = new SenderOptions(); + // is it target or source? target. // TODO API explain which is which + senderOptions.targetOptions().capabilities(destinationCapability); + + boolean transacted = txSize != null || txAction != null || txEndloopAction != null; + + // do simple and also complex (with session) loop, depending on if we have transactions + if (transacted) { + // TODO API, when I use session and when not? Add note to session that it is optional. and that it provides transactions? + // "Session object used to create Sender and Receiver instances." + try (Connection connection = client.connect(serverHost, serverPort, options); + Session session = connection.openSession(); + Sender sender = session.openSender(address, senderOptions)) { + performMessageSending(transacted, sender, session); + } + } else { + try (Connection connection = client.connect(serverHost, serverPort, options); + Sender sender = connection.openSender(address, senderOptions)) { + performMessageSending(transacted, sender, null); + } + } + + client.close(); + + return 0; + } + + private void performMessageSending(boolean transacted, @NotNull Sender sender, @Nullable Session session) throws IOException, ClientException { + // ensure we have a transaction; JMS begins a transaction automatically + if (transacted) { + assert session != null; + // TODO: Typo in javadoc: transaction they user must commit + // also, thought beginning transaction twice is noop, but got + // ClientIllegalStateException("A transaction is already active in this Session"); + session.beginTransaction(); + } + + int i = 0; + double initialTimestamp = Utils.getTime(); + while (true) { + + if (durationMode == DurationModeSender.beforeSend) { + Utils.sleepUntilNextIteration(initialTimestamp, count, duration, i + 1); + } + + Message message = createNewMessage(); + // TODO what's timeout for in a sender? + Tracker tracker = sender.send(message); + tracker.awaitSettlement(); + // NB: This is not a busy loop involving the network, because sooner or later the peer will drain credit, + // if it intends to keep blocking. And sender.send() blocks upon running out of credit. + while (!tracker.remoteState().isAccepted()) { + // NB: Transacted session gives a special state that is not considered "accepted" even though it is e.g. + // DeliveryState.ClientTransactional{ + // TransactionalState{txnId=ea51ffc4-4896-11ed-924a-d6bdd75e6e2e, outcome=Accepted{}} + // Since we don't test reconnect with transactions (GAP! :shocked face:) let's bail out + if (tracker.remoteState().getType() == DeliveryState.Type.TRANSACTIONAL) { + break; + } + // TODO: am I supposed to increment `delivery-count` of the message if I got rejected before? + // as per http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-rejected + tracker = sender.send(message); // resend the message + tracker.awaitSettlement(); + } + + printMessage(message); + i++; // TODO: looks like all have the sleeps wrong, then (the + 1 in the calls) + + if (durationMode == DurationModeSender.afterSend) { + Utils.sleepUntilNextIteration(initialTimestamp, count, duration, i + 1); + } + + if (txSize != null && txSize != 0) { + if (i % txSize == 0) { + // Do transaction action + if (txAction != null) { + assert transacted && session != null; + switch (txAction) { + case commit: + session.commitTransaction(); + break; + case rollback: + session.rollbackTransaction(); + break; + } + + session.beginTransaction(); + + if (durationMode == DurationModeSender.afterSendTxAction) { + Utils.sleepUntilNextIteration(initialTimestamp, count, duration, i + 1); + } + } + } + } + if (count == 0) continue; + if (i == count) break; + } + + if (txEndloopAction != null) { + assert transacted && session != null; + switch (txEndloopAction) { + case commit: + session.commitTransaction(); + break; + case rollback: + session.rollbackTransaction(); + break; + } + } else if (transacted) { + session.rollbackTransaction(); + } + } + + @NotNull + private Message createNewMessage() throws IOException, ClientException { + Message message; + if (msgContentListItem != null && !msgContentListItem.isEmpty()) { // TODO check only one of these is specified + List list = new ArrayList<>(); + for (String item : msgContentListItem) { + Content content = new Content(contentType.toString(), item, false); // TODO do this in args parsing? + list.add(content.getValue()); + } + message = Message.create((Object) list); + } else if (msgContentMapItems != null) { + Map map = new HashMap<>(); + for (String item : msgContentMapItems) { + Content content = new Content(contentType.toString(), item, true); // TODO do this in args parsing? + map.put(content.getKey(), content.getValue()); + } + message = Message.create((Object) map); + } else if (msgContentFromFile != null) { + if (stringToBool(msgContentBinaryString)) { + message = Message.create(Files.readAllBytes(Paths.get(msgContentFromFile))); // todo maybe param type as Path? check exists + } else { + message = Message.create(Files.readString(Paths.get(msgContentFromFile))); // todo maybe param type as Path? check exists + } + } else { + message = Message.create(msgContent); + } + if (msgProperties != null) { + for (String item : msgProperties) { + Content content = new Content(propertyType.toString(), item, true); // TODO do this in args parsing? + message.property(content.getKey(), content.getValue()); + } + } + if (msgId != null) { + message.messageId(msgId); + } + if (msgCorrelationId != null) { + message.correlationId(msgCorrelationId); + } + if (msgTtl != null) { + message.timeToLive(msgTtl); + } + if (stringToBool(msgDurableString)) { + message.durable(true); + } + if (msgGroupId != null) { + message.groupId(msgGroupId); + } + if (msgGroupSeq != null) { + message.groupSequence(msgGroupSeq); + } + if (msgReplyTo != null) { + message.replyTo(msgReplyTo); + } + if (msgReplyToGroupId != null) { + message.replyToGroupId(msgReplyToGroupId); + } + if (contentType != null) { + message.contentType(contentType.toString()); // TODO: maybe should do more with it? don't bother with enum? + } + if (stringToBool(connPopulateUserIdString)) { + message.userId(msgUserId.getBytes()); + } + if (msgSubject != null) { + message.subject(msgSubject); + } + if (msgPriority != null) { + message.priority((byte) (int) msgPriority); + } + return message; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/ContentType.java b/cli-protonj2/src/main/java/com/redhat/mqe/ContentType.java new file mode 100644 index 00000000..e5d5dd35 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/ContentType.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum ContentType { + INT("int"), STRING("string"); + + private final String value; + + ContentType(String s) { + this.value = s; + } + + @Override + public String toString() { + return value; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeReceiver.java b/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeReceiver.java new file mode 100644 index 00000000..2ae7c186 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeReceiver.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum DurationModeReceiver { + beforeReceive("before-receive"), + afterReceive("after-receive"), + afterReceiveTxAction("after-receive-action-tx-action"); + + private final String value; + + @Override + public String toString() { + return value; + } + + DurationModeReceiver(String s) { + this.value = s; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeSender.java b/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeSender.java new file mode 100644 index 00000000..799f2949 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/DurationModeSender.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum DurationModeSender { + beforeSend("before-send"), + afterSend("after-send"), + afterSendTxAction("after-send-tx-action"); + + private final String value; + + @Override + public String toString() { + return value; + } + + DurationModeSender(String s) { + this.value = s; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/LogLib.java b/cli-protonj2/src/main/java/com/redhat/mqe/LogLib.java new file mode 100644 index 00000000..e42ab620 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/LogLib.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum LogLib { + off, trace +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/LogMsgs.java b/cli-protonj2/src/main/java/com/redhat/mqe/LogMsgs.java new file mode 100644 index 00000000..6ad506fe --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/LogMsgs.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum LogMsgs { + dict, interop, json +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/Main.java b/cli-protonj2/src/main/java/com/redhat/mqe/Main.java new file mode 100644 index 00000000..8f15aed1 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/Main.java @@ -0,0 +1,202 @@ +package com.redhat.mqe; + +import com.redhat.mqe.lib.LogConfigurator; +import org.apache.qpid.protonj2.client.ConnectionOptions; +import org.apache.qpid.protonj2.client.Message; +import org.apache.qpid.protonj2.client.exceptions.ClientException; +import org.jetbrains.annotations.NotNull; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +@Command( + name = "cli-protonj2", + mixinStandardHelpOptions = true, + version = "1.0.0", + description = "Sends and receives messages using Qpid Proton-J2 AMQP library.", + subcommands = { + CliProtonJ2Connector.class, + CliProtonJ2Sender.class, + CliProtonJ2Receiver.class + } +) +class Main implements Callable { + @Override + public Integer call() throws Exception { // your business logic goes here... + return 0; + } + + // this example implements Callable, so parsing, error handling and handling user + // requests for usage help or version help can be done with one line of code. + public static void main(String... args) { + int exitCode = new CommandLine(new Main()).execute(args); + System.exit(exitCode); + } +} + +class CliProtonJ2SenderReceiverConnector { + @CommandLine.Option(names = {"--log-lib"}) + private LogLib logLib = LogLib.off; + @CommandLine.Option(names = {"--conn-username"}, description = "") + private String connUsername = "MD5"; + @CommandLine.Option(names = {"--conn-password"}, description = "") + private String connPassword = "MD5"; + @CommandLine.Option(names = {"--conn-auth-mechanisms"}, description = "MD5, SHA-1, SHA-256, ...") + // todo, want to accept comma-separated lists; there is https://picocli.info/#_split_regex + private List connAuthMechanisms = new ArrayList<>(); + @CommandLine.Option(names = {"--conn-reconnect"}) + private String reconnectString = "false"; + @CommandLine.Option(names = {"--conn-heartbeat"}) + private Long connHeartbeat; + @CommandLine.Option(names = {"--conn-ssl"}, arity = "0..1") + private Boolean connSsl = false; + @CommandLine.Option(names = {"--conn-ssl-certificate"}, arity = "0..1") + private String connSslCertificate; + @CommandLine.Option(names = {"--conn-ssl-password"}, arity = "0..1") + private String connSslPassword; + @CommandLine.Option(names = {"--conn-ssl-verify-peer"}, arity = "0..1") + private Boolean connSslVerifyPeer; + @CommandLine.Option(names = {"--conn-ssl-verify-peer-skip-trust-check"}, arity = "0..1") + private Boolean connSslTrustCheck; + @CommandLine.Option(names = {"--conn-ssl-verify-peer-name"}, arity = "0..1") + private Boolean connSslCheckName; + + protected boolean stringToBool(String string) { + boolean bool = string.equalsIgnoreCase("true") || string.equalsIgnoreCase("yes"); + return bool; + } + + @NotNull + protected ConnectionOptions getConnectionOptions() { + final ConnectionOptions options = new ConnectionOptions(); + // TODO typo in javadoc: This option enables or disables reconnection to a remote remote peer after IO errors. To control + // TODO API: unclear if reconnect is on or off by default (public static final boolean DEFAULT_RECONNECT_ENABLED = false;) + if (stringToBool(reconnectString)) { + options.reconnectEnabled(true); + } + if (connHeartbeat != null) { + // TODO finish that 2x investigation for heartbeats and document it somewhere (jira?) + options.idleTimeout(2 * connHeartbeat, TimeUnit.SECONDS); + } + options.user(connUsername); + options.password(connPassword); + for (AuthMechanism mech : connAuthMechanisms) { + options.saslOptions().addAllowedMechanism(mech.name()); + } + + // TODO: why is there both `options.sslEnabled and options.sslOptions().sslEnabled()`? + boolean anyTlsOptionSet = Stream.of( + connSslCertificate, + connSslTrustCheck, + connSslCheckName, + connSslPassword, + connSslVerifyPeer + ).anyMatch(Objects::nonNull); + if (connSsl || anyTlsOptionSet) { + options.sslEnabled(true); + } + + if (connSslCertificate != null) { + options.sslOptions().keyStoreLocation(connSslCertificate); + } + if (connSslPassword != null) { + options.sslOptions().keyStorePassword(connSslPassword); + } + + if (connSslTrustCheck != null) { + options.sslOptions().verifyHost(connSslTrustCheck); + options.sslOptions().trustAll(!connSslTrustCheck); + } + if (connSslVerifyPeer != null) { + options.sslOptions().verifyHost(connSslVerifyPeer); + } + if (connSslCheckName != null) { + options.sslOptions().trustAll(!connSslCheckName); + } + + // TODO: what do I actually need/want here? + // TODO, same problem, lib has Symbols in ClientConstants class + // cli proton cpp does not do this, btw +// options.desiredCapabilities( +// "sole-connection-for-container", "DELAYED_DELIVERY", "SHARED-SUBS", "ANONYMOUS-RELAY" +// ); + return options; + } + + protected void configureLogging() { + switch (logLib) { + case trace: + LogConfigurator.trace(); + break; + case off: + break; + } + } +} + +class CliProtonJ2SenderReceiver extends CliProtonJ2SenderReceiverConnector { + protected final ProtonJ2MessageFormatter messageFormatter; + + // todo: what does --out=python --log-msgs=json mean? + @CommandLine.Option(names = {"--out"}, description = "") + protected Out out = Out.python; + + @CommandLine.Option(names = {"--log-msgs"}, description = "message reporting style") + protected LogMsgs logMsgs = LogMsgs.dict; + + @CommandLine.Option(names = {"--msg-content-hashed"}, arity = "0..1") + protected boolean msgContentHashed = false; + + @CommandLine.Option(names = {"-a", "--address"}, description = "") + protected String address = ""; + + public CliProtonJ2SenderReceiver() { + this.messageFormatter = new ProtonJ2MessageFormatter(); + } + + public CliProtonJ2SenderReceiver(ProtonJ2MessageFormatter messageFormatter) { + this.messageFormatter = messageFormatter; + } + + protected void printMessage(Message message) throws ClientException { + Map messageDict = messageFormatter.formatMessage(address, message, msgContentHashed); + switch (out) { + case python: + switch (logMsgs) { + case dict: + messageFormatter.printMessageAsPython(messageDict); + break; + case interop: + messageFormatter.printMessageAsPython(messageDict); + break; + } + break; + case json: + switch (logMsgs) { + case dict: + messageFormatter.printMessageAsJson(messageDict); + break; + case interop: + messageFormatter.printMessageAsJson(messageDict); + break; + } + break; + } + } +} + +enum AuthMechanism { + PLAIN, anonymous +} + +// todo list of features in general; supports kerberos, io-uring, epoll, websockets, +// does it support opening listening sockets? listening websocket? NO +// does support all JMS 2.0 capabilities? (in some way, assuming broker cooperates?) It should + diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/Out.java b/cli-protonj2/src/main/java/com/redhat/mqe/Out.java new file mode 100644 index 00000000..8f59bc5c --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/Out.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum Out { + python, json +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/PropertyType.java b/cli-protonj2/src/main/java/com/redhat/mqe/PropertyType.java new file mode 100644 index 00000000..06b439f7 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/PropertyType.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum PropertyType { + String("string"); + + private final String value; + + PropertyType(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/ProtonJ2MessageFormatter.java b/cli-protonj2/src/main/java/com/redhat/mqe/ProtonJ2MessageFormatter.java new file mode 100644 index 00000000..681498ba --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/ProtonJ2MessageFormatter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import com.redhat.mqe.lib.MessageFormatter; +import org.apache.qpid.protonj2.client.Message; +import org.apache.qpid.protonj2.client.exceptions.ClientException; + +import java.util.HashMap; +import java.util.Map; + +public class ProtonJ2MessageFormatter extends MessageFormatter { + + public Map formatMessage(String address, Message message, boolean msgContentHashed) throws ClientException { + Map map = new HashMap<>(); + map.put("address", address); + map.put("group-id", message.groupId()); + map.put("subject", message.subject()); + map.put("user-id", message.userId()); + map.put("correlation-id", message.correlationId()); + map.put("content-encoding", message.contentEncoding()); + map.put("priority", (int) message.priority()); // TODO(ENTMQCL-2973) remove cast + map.put("type", message.contentType()); +// map.put("ttl", message.timeToLive()); // todo, why do we do the weird thing below instead of this? + map.put("ttl", getTtl(message)); + map.put("absolute-expiry-time", message.absoluteExpiryTime()); + if (msgContentHashed) { + map.put("content", MessageFormatter.hash(message.body())); + } else { + map.put("content", message.body()); + } + map.put("redelivered", message.deliveryCount() > 1); + map.put("reply-to-group-id", message.replyToGroupId()); + map.put("durable", message.durable()); + map.put("group-sequence", (long) message.groupSequence()); // TODO(ENTMQCL-2973) remove cast + map.put("creation-time", message.creationTime()); + map.put("content-type", message.contentType()); + map.put("id", message.messageId()); + map.put("reply-to", message.replyTo()); + map.put("delivery-count", message.deliveryCount()); + + // getPropertyNames? from JMS missing? + Map propertyMap = new HashMap<>(); + message.forEachProperty((s, o) -> { + propertyMap.put((String) s, o); // this wanted to cast to string when I removed message generic type; what??? TODO + }); + map.put("properties", propertyMap); + + return map; + } + + + /** + * Calculate TTL of given message from message + * expiration time and message timestamp. + *

+ * Returns the time the message expires, which is the sum of the time-to-live value + * specified by the client and the GMT at the time of the send + * EXP_TIME = CLIENT_SEND+TTL (CLIENT_SEND??) + * CLIENT_SEND time is approximately getJMSTimestamp() (time value between send()/publish() and return) + * TODO - check for correctness + * + * @param message calculate TTL for this message + * @return positive long number if TTL was calculated. Long.MIN_VALUE if error. + */ + public static long getTtl(Message message) { + long ttl = 0; + try { + long expiration = message.absoluteExpiryTime(); + long timestamp = message.creationTime(); + if (expiration != 0 && timestamp != 0) { + ttl = expiration - timestamp; + } + } catch (ClientException jmse) { +// LOG.error("Error while calculating TTL value.\n" + jmse.getMessage()); + jmse.printStackTrace(); + System.exit(1); + } + return ttl; + } +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/SsnAckMode.java b/cli-protonj2/src/main/java/com/redhat/mqe/SsnAckMode.java new file mode 100644 index 00000000..c09ec49a --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/SsnAckMode.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum SsnAckMode { + auto, + client, +// dups_ok, // todo we probably don't have equivalent of this here // https://www.ibm.com/docs/en/ibm-mq/8.0?topic=session-acknowledgement-modes +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/SyncMode.java b/cli-protonj2/src/main/java/com/redhat/mqe/SyncMode.java new file mode 100644 index 00000000..c845afea --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/SyncMode.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum SyncMode { + session, +} diff --git a/cli-protonj2/src/main/java/com/redhat/mqe/TxAction.java b/cli-protonj2/src/main/java/com/redhat/mqe/TxAction.java new file mode 100644 index 00000000..02141024 --- /dev/null +++ b/cli-protonj2/src/main/java/com/redhat/mqe/TxAction.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +public enum TxAction { + commit, + rollback, + // recover, // TODO: how do I do JMS Recover here? +} diff --git a/cli-artemis-jms/src/main/resources/log4j.properties b/cli-protonj2/src/main/resources/log4j2.properties similarity index 53% rename from cli-artemis-jms/src/main/resources/log4j.properties rename to cli-protonj2/src/main/resources/log4j2.properties index 968f060d..e6c0e82b 100644 --- a/cli-artemis-jms/src/main/resources/log4j.properties +++ b/cli-protonj2/src/main/resources/log4j2.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2017 Red Hat, Inc. +# Copyright (c) 2022 Red Hat, Inc. # # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with @@ -16,21 +16,19 @@ # See the License for the specific language governing permissions and # limitations under the License. # +rootLogger=WARN, console -# Set root logger level to DEBUG and its only appender to console -# This sets ALL the logs to given level. -log4j.rootLogger=WARN, console +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO -log4j.logger.com.redhat.mqe.lib=INFO, console -log4j.additivity.com.redhat.mqe.lib=false +logger.netty.name=io.netty +logger.netty.level=WARN -log4j.logger.io.netty=WARN, console -log4j.logger.netty=WARN, console +# Log transport.traceBytes=true messages +logger.NettyTcpTransport.name=org.apache.qpid.jms.transports.netty.NettyTcpTransport +logger.NettyTcpTransport.level=DEBUG -# Appender "console" settings -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.target=System.err -log4j.appender.console.Threshold=all -log4j.appender.console.layout=org.apache.log4j.PatternLayout -#log4j.appender.console.layout.ConversionPattern=%t %d %p [%c{4}] %m%n -log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %m%n +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-protonj2/src/test/java/com/redhat/mqe/CliProtonJ2SenderTest.java b/cli-protonj2/src/test/java/com/redhat/mqe/CliProtonJ2SenderTest.java new file mode 100644 index 00000000..6a5aa8ab --- /dev/null +++ b/cli-protonj2/src/test/java/com/redhat/mqe/CliProtonJ2SenderTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2022 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.powermock.reflect.Whitebox; +import picocli.CommandLine; + +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; + +class CliProtonJ2SenderTest { + + // https://stackoverflow.com/questions/74273941/how-do-i-get-picocli-to-parse-item-item-foo-as-null-foo-listst + @Nested + class MsgContentListItem { + final CliProtonJ2Sender sender = new CliProtonJ2Sender(); + final CommandLine commandLine = new CommandLine(sender); + + @Test + void test_msgContentListItem__null() { + commandLine.parseArgs("--msg-content-list-item", "--msg-content-list-item", "pepa"); + + // https://github.com/powermock/powermock/wiki/Bypass-Encapsulation + List v = Whitebox.getInternalState(sender, "msgContentListItem", sender.getClass()); + assertThat(v).containsExactly(null, "pepa"); + } + + @Test + void test_msgContentListItem__empty_equals() { + commandLine.parseArgs("--msg-content-list-item=", "--msg-content-list-item", "pepa"); + + List v = Whitebox.getInternalState(sender, "msgContentListItem", sender.getClass()); + assertThat(v).containsExactly("", "pepa"); + } + } +} diff --git a/cli-protonj2/src/test/java/com/redhat/mqe/MainTest.java b/cli-protonj2/src/test/java/com/redhat/mqe/MainTest.java new file mode 100644 index 00000000..f5ee8c84 --- /dev/null +++ b/cli-protonj2/src/test/java/com/redhat/mqe/MainTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import com.google.common.truth.Truth; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.internal.Intrinsics; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; +import util.Broker; +import util.BrokerFixture; + +import java.security.Permission; +import java.util.concurrent.TimeUnit; + +class SystemExitingWithStatus extends RuntimeException { + public final int status; + + public SystemExitingWithStatus(int status) { + this.status = status; + } +} + +class NoExitSecurityManager extends SecurityManager { + private final SecurityManager parentManager; + + @Override + public void checkPermission(Permission perm) { + // allow all + } + + @Override + public void checkPermission(Permission perm, Object context) { + // allow all + } + + @Override + public void checkExit(int status) { + throw new SystemExitingWithStatus(status); + } + + NoExitSecurityManager(SecurityManager parentManager) { + this.parentManager = parentManager; + } + + public static void assertSystemExit(int status, @NotNull Executable executable) throws Throwable { + Intrinsics.checkNotNullParameter(executable, "executable"); + SecurityManager previousManager = System.getSecurityManager(); + + try { + NoExitSecurityManager manager = new NoExitSecurityManager(previousManager); + System.setSecurityManager(manager); + executable.execute(); + Assertions.fail("expected exception"); + } catch (SystemExitingWithStatus exception) { + Truth.assertThat(exception.status).isEqualTo(status); + } finally { + System.setSecurityManager(previousManager); + } + } + + public static void assertNoSystemExit(@NotNull Function0 executable) { + Intrinsics.checkNotNullParameter(executable, "executable"); + SecurityManager previousManager = System.getSecurityManager(); + + try { + NoExitSecurityManager manager = new NoExitSecurityManager(previousManager); + System.setSecurityManager((SecurityManager) manager); + executable.invoke(); + } catch (SystemExitingWithStatus var5) { + Assertions.fail("System.exit has been called"); + } finally { + System.setSecurityManager(previousManager); + } + } +} + +class MainTest { + @BeforeAll + static void configureLogging() { + // turn on extra verbose logging + //Broker.configureLogging(); + } + + void checkMainInvocation(String cmd) throws Throwable { + String[] args = cmd.split(" "); + NoExitSecurityManager.assertSystemExit(0, () -> { + Main.main(args); + }); + } + + @Test + @Timeout(value = 60, unit = TimeUnit.SECONDS) + @ExtendWith(BrokerFixture.class) + void testAckWithSessionClose(@BrokerFixture.TempBroker Broker broker) throws Throwable { + broker.configuration.setSecurityEnabled(false); + broker.configuration.setPersistenceEnabled(false); // this, or tmpdir, otherwise test runs interact + broker.startBroker(); + + // todo: have to handle amqp:// prefix + String brokerUrl = "localhost:" + broker.addAMQPAcceptor(); + + NoExitSecurityManager.assertSystemExit(0, () -> { + Main.main( + "sender", "--log-msgs=dict", "--broker=" + brokerUrl, "--conn-username=tckuser", "--conn-password=tckuser", "--address=test_default_username_right_password_right", "--count=1" + ); + }); + + NoExitSecurityManager.assertSystemExit(0, () -> { + Main.main( + "sender", "--timeout=2", "--log-msgs=dict", "--broker=" + brokerUrl, "--conn-auth-mechanisms=PLAIN", "--conn-username=admin", "--conn-password=admin", "--address=test_direct_transient_empty_message_with_string_property", "--count=10", "--msg-property=key1=value1" + ); + }); + + NoExitSecurityManager.assertSystemExit(0, () -> { + Main.main( + "receiver", "--timeout=2", "--log-msgs=dict", "--broker=" + brokerUrl, "--conn-auth-mechanisms=PLAIN", "--conn-username=admin", "--conn-password=admin", "--address=test_direct_transient_empty_message_with_string_property", "--count=10" + ); + }); + + NoExitSecurityManager.assertSystemExit(0, () -> { + Main.main( + "sender", "--log-msgs=dict", "--broker=" + brokerUrl, "--conn-auth-mechanisms=PLAIN", "--conn-username=admin", "--conn-password=admin", "--address=test_direct_transient_text_message", "--count=1", "--msg-content=Simple Text Message", "--msg-correlation-id=corr-id-JWXoIk" + ); + }); + + // test_direct_transient_text_message + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_text_message --count 1 --msg-content SimpleTextMessage --msg-correlation-id corr-id-eqa9vp"); + checkMainInvocation("receiver --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_text_message --count 1"); + + // test_publish_subscribe_string + Thread t = new Thread(() -> { + try { + checkMainInvocation("receiver --timeout 100 --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address topic://test_publish_subscribe_string --count 3"); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + }); + Thread.sleep(100); // do I really want to do things like this? need better check I have a subscriber on broker + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address topic://test_publish_subscribe_string --count 3 --msg-content ABC --msg-correlation-id some-corr-id"); + t.join(); + } + + @Test + @Disabled("These commands take way too long to execute, now that --duration works how it should") + @Timeout(value = 60, unit = TimeUnit.SECONDS) + @ExtendWith(BrokerFixture.class) + void test2(@BrokerFixture.TempBroker Broker broker) throws Throwable { + broker.configuration.setSecurityEnabled(false); + broker.configuration.setPersistenceEnabled(false); // this, or tmpdir, otherwise test runs interact + broker.startBroker(); + + // todo: have to handle amqp:// prefix + String brokerUrl = "localhost:" + broker.addAMQPAcceptor(); + + + checkMainInvocation("receiver --timeout 2 --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address JAMQMsgPatterns111Tests_test_browse_messages --recv-browse true --count 20"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_direct_transient_list_message + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_list_message --count 1 --msg-content-list-item --msg-content-list-item String --msg-content-list-item ~1 --msg-content-list-item ~1.0 --msg-content-list-item 1 --msg-content-list-item 1.0 --msg-content-list-item ~-1 --msg-content-list-item ~-1.3 --msg-content-list-item -1 --msg-content-list-item ~~1 --msg-correlation-id corr-id-Mee2YQ"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_direct_transient_map_message + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_map_message --count 1 --msg-content-map-item empty_string= --msg-content-map-item string=String --msg-content-map-item int~1 --msg-content-map-item float~1.0 --msg-content-map-item string_int=1 --msg-content-map-item string_float=1.0 --msg-content-map-item negative_int~-1 --msg-content-map-item negative_float~-1.3 --msg-content-map-item string_negative_int=-1 --msg-content-map-item string_retype_operator=~1 --msg-correlation-id corr-id-JQt4JM"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_dead_letter_queue_with_expired_messages + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_dead_letter_queue_with_expired_messages --count 3 --msg-content ABC --msg-durable yes --msg-ttl 1000"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_message_group_simple + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_message_group_simple --count 1 --msg-content B-0 --msg-group-id B"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_reply_to_address + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_reply_to_address --count 1 --msg-reply-to test_reply_to_address-replyQ --msg-content-map-item text=replyQ_SLjFkenkBi"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_publish_subscribe_int + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address topic://test_publish_subscribe_int --count 3 --msg-content 12345 --content-type int --msg-correlation-id some-corr-id"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_publish_subscribe_map_list + // --msg-content-map-item "key3~[123, 3.14]" + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_message_group_consumer_disconnect + // TODO msgcontent %d feature + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_message_group_consumer_disconnect --count 40 --msg-content A-%d --duration 20 --msg-group-id A"); + + // tests.JAMQMsgPatterns000Tests.JAMQMsgPatternsTests.test_reply_to_address + checkMainInvocation("receiver --timeout 5 --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_reply_to_address --count 1 --process-reply-to"); + + // tests.JAMQNode000Tests.JAMQNodeTests.test_address_full_policy_block + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address JAMQNode111Tests_test_address_full_policy_block --count 1 --msg-content-from-file /etc/passwd"); + + // tests.JAMQNode000Tests.JAMQNodeTests.test_direct_transient_large_string_message_size_1mb + // ditto for receiver + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_large_string_message_size_1mb --count 1 --msg-content-from-file /etc/passwd --msg-content-hashed True"); + + // tests.JAMQNode000Tests.JAMQNodeTests.test_max_consumers_queue + // TODO: client is not started at all in the test; some issues with params mapping? + + // tests.JAMQMessage000Tests.JAMQMessageTests.test_amqp_bare_message_consistency + // Unknown options: '--msg-subject', 'amqp_bare_message_test', '--msg-user-id', 'admin', '--msg-priority', '7', '--conn-populate-user-id', 'True', '--msg-group-seq', '1', '--msg-reply-to-group-id', 'group-a' + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_amqp_bare_message_consistency --count 1 --msg-subject amqp_bare_message_test --msg-reply-to ExpiryQueue --msg-property PI=~3.141592 --msg-property color=red --msg-property mapKey=mapValue --msg-content amqp_bare_msg-CBJJIY --msg-durable True --msg-ttl 300000 --msg-correlation-id amqp_bare_msg-CBJJIY --msg-user-id admin --msg-priority 7 --conn-populate-user-id True --msg-group-id group-a --msg-group-seq 1 --msg-reply-to-group-id group-a"); + + // tests.JAMQMessage000Tests.JAMQMessageTests.test_populate_validated_user_option + // TODO FAIL dtestlib.Test:levels.py:61 Checking properties keys for validated user with expected '_AMQ_VALIDATED_USER' or 'JMSXUserID': dict_keys([]) # result:False (exp. True), dur.:-1.00 err_cnt:1 + + // tests.JAMQMessage000Tests.JAMQMessageTests.test_scheduled_message_zero_timestamp + // Unknown options: '--msg-id' + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address JAMQMessage111Tests_test_scheduled_message_zero_timestamp --count 2 --msg-id ReferenceMessage_JAMQMessage111Tests_test_scheduled_message_zero_timestamp --msg-property _AMQ_SCHED_DELIVERY~0"); + + // tests.JAMQMessage000Tests.JAMQMessageTests.test_client_acknowledge_inactivity_exception + // --ssn-ack-mode + // --duration --duration-mode --ssn-ack-mode + checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_client_acknowledge_inactivity_exception --count 20 --msg-durable True --ssn-ack-mode client"); + checkMainInvocation("receiver --timeout 10 --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_client_acknowledge_inactivity_exception --count 20 --duration 100 --duration-mode after-receive --ssn-ack-mode client"); + + +// +// checkMainInvocation("sender --log-msgs dict --broker " + brokerUrl + " --conn-auth-mechanisms PLAIN --conn-username admin --conn-password admin --address test_direct_transient_text_message --count 1 --msg-content SimpleTextMessage --msg-correlation-id corr-id-eqa9vp"); + } + +// void testMessageContentListItem() { +// '--msg-content-list-item', '', '--msg-content-list-item', 'String', '--msg-content-list-item', '~1', '--msg-content-list-item', '~1.0', '--msg-content-list-item', '1', '--msg-content-list-item', '1.0', '--msg-content-list-item', '~-1', '--msg-content-list-item', '~-1.3', '--msg-content-list-item', '-1', '--msg-content-list-item', '~~1' +// } +} diff --git a/cli-protonj2/src/test/java/com/redhat/mqe/MessageLoggingTest.java b/cli-protonj2/src/test/java/com/redhat/mqe/MessageLoggingTest.java new file mode 100644 index 00000000..c32bf692 --- /dev/null +++ b/cli-protonj2/src/test/java/com/redhat/mqe/MessageLoggingTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe; + +import com.redhat.mqe.lib.MessageFormatter; +import org.apache.qpid.protonj2.client.Message; +import org.apache.qpid.protonj2.client.exceptions.ClientException; + +import java.util.List; +import java.util.stream.Collectors; + +class PreviousImplementation { + void logMessage(String address, Message message, boolean msgContentHashed) throws ClientException { + StringBuilder sb = new StringBuilder(); + + sb.append("{"); + + addKeyValue(sb, "address", address); + addKeyValue(sb, "group-id", message.groupId()); + addKeyValue(sb, "subject", message.subject()); + addKeyValue(sb, "user-id", message.userId()); + addKeyValue(sb, "correlation-id", message.correlationId()); + addKeyValue(sb, "content-encoding", message.contentEncoding()); + addKeyValue(sb, "priority", message.priority()); + addKeyValue(sb, "type", "string"); // ??? + addKeyValue(sb, "ttl", message.timeToLive()); + addKeyValue(sb, "absolute-expiry-time", message.absoluteExpiryTime()); + if (msgContentHashed) { + // this is inlined addKeyValue, TODO do it nicer + sb.append("'"); + sb.append("content"); + sb.append("': "); + sb.append("'"); // extra quotes to format + sb.append(MessageFormatter.hash(formatPython(message.body()))); + sb.append("'"); + sb.append(", "); + } else { + addKeyValue(sb, "content", message.body()); + } + addKeyValue(sb, "redelivered", message.deliveryCount() > 1); + addKeyValue(sb, "reply-to-group-id", message.replyToGroupId()); + addKeyValue(sb, "durable", message.durable()); + addKeyValue(sb, "group-sequence", message.groupSequence()); + addKeyValue(sb, "creation-time", message.creationTime()); + addKeyValue(sb, "content-type", message.contentType()); + addKeyValue(sb, "id", message.messageId()); + addKeyValue(sb, "reply-to", message.replyTo()); + + // getPropertyNames? from JMS missing? + StringBuilder sbb = new StringBuilder(); + sbb.append('{'); +// AtomicBoolean first = new AtomicBoolean(true); + message.forEachProperty((s, o) -> { +// if (!first.get()) { +// sbb.append(", "); +// first.set(false); +// } + addKeyValue(sbb, (String) s, o); // this wanted to cast to string when I removed message generic type; what??? TODO + }); + if (message.hasProperties()) { + sbb.delete(sbb.length() - 2, sbb.length()); // remove last ", " + } + sbb.append('}'); + addKeyValue(sb, "properties", sbb); // ??? + + sb.delete(sb.length() - 2, sb.length()); // remove last ", " + + sb.append("}"); + + System.out.println(sb); + } + + void addKeyValue(StringBuilder sb, String key, Object value) { + sb.append("'"); + sb.append(key); + sb.append("': "); + sb.append(formatPython(value)); + sb.append(", "); + } + + String formatPython(Object parameter) { + if (parameter == null) { + return "None"; + } + if (parameter instanceof String) { + return "'" + parameter + "'"; + } + if (parameter instanceof Boolean) { + return ((boolean)parameter) ? "True" : "False"; + } + if (parameter instanceof StringBuilder) { + return parameter.toString(); + } + if (parameter instanceof List) { + return "[" + ((List) parameter).stream().map(this::formatPython).collect(Collectors.joining(", ")) + "]"; + } + return "'" + parameter + "'"; + } +} + +public class MessageLoggingTest { +} diff --git a/cli-protonj2/src/test/kotlin/MainTest.kt b/cli-protonj2/src/test/kotlin/MainTest.kt new file mode 100644 index 00000000..cc9f2a07 --- /dev/null +++ b/cli-protonj2/src/test/kotlin/MainTest.kt @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2021 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.redhat.mqe + +import AbstractMainTest +import assertNoSystemExit +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import picocli.CommandLine +import java.io.File +import java.nio.file.Files + +class ProtonJ2ClientListener(private val clientListener: ClientListener) : ProtonJ2MessageFormatter() { + override fun printMessageAsPython(format: MutableMap?) { + clientListener.onMessage(format) + super.printMessageAsPython(format) + } +} + +@Tag("external") +class ProtonJ2MainTest : AbstractMainTest() { + + override val brokerUrl = "amqp://127.0.0.1:61616" + override val sslBrokerUrl = "amqps://127.0.0.1:61617" + + override val senderAdditionalOptions = """ +--capacity 1 +--conn-async-acks true +--conn-async-send true +--conn-auth-mechanisms anonymous +--conn-auth-sasl false +--conn-cache-ena false +--conn-cache-size 1 +--conn-clientid aClientId +--conn-clientid-prefix aClientIdPrefix +--conn-close-timeout 1000 +--conn-conn-timeout 1000 +--conn-connid-prefix aConnIdPrefix +--conn-heartbeat 1000 +--conn-local-msg-priority true +--conn-max-frame-size 4096 +--conn-prefetch 1 +--conn-prefetch-browser 1 +--conn-prefetch-queue 1 +--conn-prefetch-topic 1 +--conn-prefetch-topic-dur 1 +--conn-prefix-packet-size-ena false +--conn-queue-prefix aQueuePrefix +--conn-reconnect true +--conn-reconnect-backoff false +--conn-reconnect-backoff-multiplier 1 +--conn-reconnect-initial-delay 1 +--conn-reconnect-interval 1000 +--conn-reconnect-limit 1000 +--conn-reconnect-start-limit 1000 +--conn-reconnect-timeout 1000 +--conn-reconnect-warn-attempts 1 +--conn-redeliveries-max 1 +--conn-server-stack-trace-ena false +--conn-sync-send true +--conn-tcp-buf-size-recv 1 +--conn-tcp-buf-size-send 1 +--conn-tcp-conn-timeout 1000 +--conn-tcp-keep-alive true +--conn-tcp-no-delay false +--conn-tcp-sock-linger 1000 +--conn-tcp-sock-timeout 1000 +--conn-tcp-traffic-class 1 +--conn-tight-encoding-ena false +--conn-topic-prefix aTopicPrefix +--conn-valid-prop-names false +--msg-content-type aMsgContentType +--msg-correlation-id aCorrelationId +--msg-durable false +--msg-group-id aMsgGroupId +--msg-group-seq -1 +--msg-no-timestamp true +--msg-priority 1 +--timeout 2 +--tx-size 1 +--msg-reply-to aReplyToQueue +--msg-reply-to-group-id aReplyToGroupId +--msg-subject aMsgSubject +--msg-ttl 10000 +--msg-user-id aMsgUserId +--property-type String +""".split(" ", "\n").toTypedArray() + + // cannot set Client ID, because more than one connection is created, and these would clash +//--conn-clientid aClientId + override val connectorAdditionalOptions = """ +--conn-async-acks true +--conn-async-send true +--conn-auth-mechanisms anonymous +--conn-auth-sasl false +--conn-cache-ena false +--conn-cache-size 1 +--conn-clientid-prefix aClientIdPrefix +--conn-close-timeout 1000 +--conn-conn-timeout 1000 +--conn-connid-prefix aConnIdPrefix +--conn-heartbeat 1000 +--conn-local-msg-priority true +--conn-max-frame-size 4096 +--conn-prefetch 1 +--conn-prefetch-browser 1 +--conn-prefetch-queue 1 +--conn-prefetch-topic 1 +--conn-prefetch-topic-dur 1 +--conn-prefix-packet-size-ena false +--conn-queue-prefix aQueuePrefix +--conn-reconnect true +--conn-reconnect-backoff false +--conn-reconnect-backoff-multiplier 1 +--conn-reconnect-initial-delay 1 +--conn-reconnect-interval 1000 +--conn-reconnect-limit 1000 +--conn-reconnect-start-limit 1000 +--conn-reconnect-timeout 1000 +--conn-reconnect-warn-attempts 1 +--conn-redeliveries-max 1 +--conn-server-stack-trace-ena false +--conn-sync-send true +--conn-tcp-buf-size-recv 1 +--conn-tcp-buf-size-send 1 +--conn-tcp-conn-timeout 1000 +--conn-tcp-keep-alive true +--conn-tcp-no-delay false +--conn-tcp-sock-linger 1000 +--conn-tcp-sock-timeout 1000 +--conn-tcp-traffic-class 1 +--conn-tight-encoding-ena false +--conn-topic-prefix aTopicPrefix +--conn-valid-prop-names false +""".split(" ", "\n").toTypedArray() + + override fun main_(listener: ClientListener, args: Array) { + val protonJ2ClientListener = ProtonJ2ClientListener(listener) + val main = when (args[0]) { + "sender" -> CommandLine(CliProtonJ2Sender(protonJ2ClientListener)) + "receiver" -> CommandLine(CliProtonJ2Receiver(protonJ2ClientListener)) + "connector" -> CommandLine(CliProtonJ2Connector()) + else -> throw NotImplementedError(args[0]) + } + val returnCode = main.execute(*(args.drop(1).toTypedArray())) + if (returnCode != 0) { + System.exit(returnCode) + } + } + + override val prefix: String + get() = "ProtonJ2MainTest" + + @Test + @Disabled("Client does not use jms-style url parameters") + override fun sendLargeMessageChangingLimit() { + } + + @Disabled("Connector for protonj2 is not implemented properly") + override fun connectConnectorWithAllSenderCLISwitches(senderDynamicOptions: String) { + return + } + + override fun sendAndReceiveWithAllReceiverCLISwitches(receiverDynamicOptions: String) { + if (receiverDynamicOptions.contains("--tx-endloop-action recover")) { + return + } + super.sendAndReceiveWithAllReceiverCLISwitches(receiverDynamicOptions) + } + + override fun sendAndReceiveWithAllSenderCLISwitches(senderDynamicOptions: String) { + if (senderDynamicOptions.contains("--tx-endloop-action recover")) { + return + } + super.sendAndReceiveWithAllSenderCLISwitches(senderDynamicOptions) + } + + override fun sendSingleMessageAllTrustingTls() { + assertNoSystemExit { + val senderParameters = + "sender --log-msgs dict --broker $sslBrokerUrl --address $address --conn-ssl-verify-peer false --conn-ssl-verify-peer-name false --count 1".split(" " + ).toTypedArray() + print("Sending: ") + main(senderParameters) + } + } + + /** + * Large message streaming from/to java.io.{Input,Output}Stream is artemis-jms-client only + */ + @Test + @Disabled("Option --msg-content-stream true is not implemented in cli-protonj2") + fun sendLargeMessageStreamFile() { + val file = File.createTempFile(address, null) + val outputDirectory = Files.createTempDirectory(address) + val output = outputDirectory.resolve("message") + val output0 = outputDirectory.resolve("message_0") + try { + file.writeText("aContent") + val senderParameters = + "sender --log-msgs dict --broker $brokerUrl --address $address --count 1 --msg-content-from-file $file --msg-content-binary true --msg-content-stream true".split( + " " + ).toTypedArray() + val receiverParameters = + "receiver --log-msgs dict --broker $brokerUrl --address $address --count 1 --msg-binary-content-to-file $output".split( + " " + ).toTypedArray() + + print("Sending: ") + main(senderParameters) + print("Receiving: ") + main(receiverParameters) + + assertThat(output0.toFile().readBytes()).isEqualTo(file.readBytes()) + } finally { + file.delete() + outputDirectory.toFile().deleteRecursively() + } + } + + /** + * Large message streaming from/to java.io.{Input,Output}Stream is artemis-jms-client only + */ + @Test + @Disabled("Option --msg-content-stream true is not implemented in cli-protonj2") + fun sendAndReceiveLargeMessageStreamFile() { + val file = File.createTempFile(address, "input") + val outputDirectory = Files.createTempDirectory(address) + val output = outputDirectory.resolve("message") + val output0 = outputDirectory.resolve("message_0") + try { + file.writeText("aContent") + val senderParameters = + "sender --log-msgs dict --broker $brokerUrl --address $address --count 1 --msg-content-from-file $file --msg-content-binary true --msg-content-stream true".split( + " " + ).toTypedArray() + val receiverParameters = + "receiver --log-msgs dict --broker $brokerUrl --address $address --count 1 --msg-binary-content-to-file $output --msg-content-stream true".split( + " " + ).toTypedArray() + + print("Sending: ") + main(senderParameters) + print("Receiving: ") + main(receiverParameters) + + assertThat(output0.toFile().readBytes()).isEqualTo(file.readBytes()) + } finally { + file.delete() + outputDirectory.toFile().deleteRecursively() + } + } +} diff --git a/cli-qpid-jms-1x/pom.xml b/cli-qpid-jms-1x/pom.xml new file mode 100644 index 00000000..8849294f --- /dev/null +++ b/cli-qpid-jms-1x/pom.xml @@ -0,0 +1,196 @@ + + + + + 4.0.0 + + + com.redhat.cli-java + parent + 1.2.2-SNAPSHOT + ../parent/pom.xml + + + cli-qpid-jms-1x + jar + + + jms + com.redhat.mqe.jms.Main + 1.16.0 + ${qpid.jms.1x.client.version} + 2.0.70.Final + linux-x86_64-fedora + 1.8.1 + + + + + + org.apache.qpid + qpid-jms-parent + ${qpid.jms.1x.client.version} + pom + import + + + + + + + org.junit-pioneer + junit-pioneer + test + + + org.awaitility + awaitility + test + + + + com.redhat.cli-java + jmslib + + + + org.apache.qpid + qpid-jms-client + + + org.apache.geronimo.specs + geronimo-jms_2.0_spec + + + + io.opentracing + opentracing-util + compile + + + io.opentracing + opentracing-api + compile + + + + io.jaegertracing + jaeger-client + ${jaeger-client.version} + + + + com.redhat.cli-java + tests + test-jar + test + + + com.redhat.cli-java + jmslib + test-jar + test + + + com.google.truth + truth + test + + + + org.apache.activemq + artemis-amqp-protocol + test + + + com.redhat.cli-java + broker + 1.2.2-SNAPSHOT + test-jar + test + + + + + + + + org.wildfly.extras.batavia + transformer-tools-mvn + ${plugin.transformer-tools-mvn.version} + + + transform-sources + generate-sources + + transform-sources + + + ${project.basedir}/../cli-qpid-jms + true + + + + + + org.wildfly.extras.batavia + transformer-impl-eclipse + ${plugin.transformer-tools-mvn.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + + + + tcnative-boringssl-static + + + io.netty + netty-tcnative-boringssl-static + ${tcnative.version} + + + + + tcnative-openssl-dynamic + + + !disableTcnativeOpensslDynamic + + + + + io.netty + netty-tcnative + ${tcnative.version} + ${tcnative.classifier} + + + + + + diff --git a/cli-qpid-jms/src/main/resources/log4j.properties b/cli-qpid-jms-1x/src/main/resources/log4j2.properties similarity index 52% rename from cli-qpid-jms/src/main/resources/log4j.properties rename to cli-qpid-jms-1x/src/main/resources/log4j2.properties index 37b7713d..5469b57e 100644 --- a/cli-qpid-jms/src/main/resources/log4j.properties +++ b/cli-qpid-jms-1x/src/main/resources/log4j2.properties @@ -16,27 +16,23 @@ # specific language governing permissions and limitations # under the License. # +rootLogger=WARN, console -# Set root logger level to DEBUG and its only appender to console -# This sets ALL the logs to given level. -log4j.rootLogger=ERROR, console +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO -# Uncomment this to get debug logs from qpid-jms -#log4j.logger.org.apache.qpid=DEBUG, console - -log4j.logger.com.redhat.mqe.jms=INFO, console -log4j.additivity.com.redhat.mqe.jms=false - -log4j.logger.io.netty=WARN, console -log4j.logger.netty=WARN, console +logger.netty.name=io.netty +logger.netty.level=WARN # Log transport.traceBytes=true messages -log4j.logger.org.apache.qpid.jms.transports.netty.NettyTcpTransport=DEBUG, console +logger.NettyTcpTransport.name=org.apache.qpid.jms.transports.netty.NettyTcpTransport +logger.NettyTcpTransport.level=DEBUG + +# Uncomment this to get debug logs from qpid-jms +#logger.qpid.name=org.slf4j.simpleLogger.log.org.apache.qpid +#logger.qpid.level=DEBUG -# Appender "console" settings -log4j.appender.console=org.apache.log4j.ConsoleAppender -log4j.appender.console.target=System.err -log4j.appender.console.Threshold=all -log4j.appender.console.layout=org.apache.log4j.PatternLayout -#log4j.appender.console.layout.ConversionPattern=%t %d %p [%c{4}] %m%n -log4j.appender.console.layout.ConversionPattern=%d{HH:mm:ss,SSS} %p %m%n +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-qpid-jms-1x/src/test/kotlin/MainTest.kt b/cli-qpid-jms-1x/src/test/kotlin/MainTest.kt new file mode 100644 index 00000000..f97b4466 --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/MainTest.kt @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2017 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.redhat.mqe.ClientListener +import com.redhat.mqe.jms.Main +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertTimeoutPreemptively +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import java.time.Duration + +@Tag("external") +class AacMainTest : AbstractMainTest() { + + override val brokerUrl = "amqp://127.0.0.1:5672" + override val sslBrokerUrl = "amqps://127.0.0.1:5673" + override val prefix: String + get() = "AacMainTest" + + override val senderAdditionalOptions = """ +--conn-tcp-sock-linger 1000 +--capacity 1 +--conn-async-send true +--conn-auth-mechanisms anonymous +--conn-auth-sasl false +--conn-clientid aClientId +--conn-clientid-prefix aClientIdPrefix +--conn-close-timeout 1000 +--conn-conn-timeout 1000 +--conn-connid-prefix aConnIdPrefix +--conn-heartbeat 1000 +--conn-local-msg-priority true +--conn-max-frame-size 4096 +--conn-prefetch 1 +--conn-prefetch-browser 1 +--conn-prefetch-queue 1 +--conn-prefetch-topic 1 +--conn-prefetch-topic-dur 1 +--conn-queue-prefix aQueuePrefix +--conn-reconnect true +--conn-reconnect-backoff false +--conn-reconnect-backoff-multiplier 1 +--conn-reconnect-initial-delay 1 +--conn-reconnect-interval 1000 +--conn-reconnect-limit 1000 +--conn-reconnect-start-limit 1000 +--conn-reconnect-timeout 1000 +--conn-reconnect-warn-attempts 1 +--conn-redeliveries-max 1 +--conn-tcp-buf-size-recv 1 +--conn-tcp-buf-size-send 1 +--conn-tcp-conn-timeout 1000 +--conn-tcp-keep-alive true +--conn-tcp-no-delay false +--conn-tcp-sock-timeout 1000 +--conn-tcp-traffic-class 1 +--conn-topic-prefix aTopicPrefix +--conn-valid-prop-names false +--msg-content-type aMsgContentType +--msg-correlation-id aCorrelationId +--msg-durable false +--msg-group-id aMsgGroupId +--msg-group-seq -1 +--msg-no-timestamp true +--msg-priority 1 +--timeout 2 +--tx-size 1 +--msg-reply-to aReplyToQueue +--msg-reply-to-group-id aReplyToGroupId +--msg-subject aMsgSubject +--msg-ttl 10000 +--msg-user-id aMsgUserId +--property-type String +""".split(" ", "\n").toTypedArray() + + + // cannot set Client ID, because more than one connection is created, and these would clash +//--conn-clientid aClientId + override val connectorAdditionalOptions = """ +--conn-async-send true +--conn-auth-mechanisms anonymous +--conn-auth-sasl false +--conn-clientid-prefix aClientIdPrefix +--conn-close-timeout 1000 +--conn-conn-timeout 1000 +--conn-connid-prefix aConnIdPrefix +--conn-heartbeat 1000 +--conn-local-msg-priority true +--conn-max-frame-size 4096 +--conn-prefetch 1 +--conn-prefetch-browser 1 +--conn-prefetch-queue 1 +--conn-prefetch-topic 1 +--conn-prefetch-topic-dur 1 +--conn-queue-prefix aQueuePrefix +--conn-reconnect true +--conn-reconnect-backoff false +--conn-reconnect-backoff-multiplier 1 +--conn-reconnect-initial-delay 1 +--conn-reconnect-interval 1000 +--conn-reconnect-limit 1000 +--conn-reconnect-start-limit 1000 +--conn-reconnect-timeout 1000 +--conn-reconnect-warn-attempts 1 +--conn-redeliveries-max 1 +--conn-tcp-buf-size-recv 1 +--conn-tcp-buf-size-send 1 +--conn-tcp-conn-timeout 1000 +--conn-tcp-keep-alive true +--conn-tcp-no-delay false +--conn-tcp-sock-timeout 1000 +--conn-tcp-traffic-class 1 +--conn-topic-prefix aTopicPrefix +--conn-valid-prop-names false +""".split(" ", "\n").toTypedArray() + + override fun main_(listener: ClientListener, args: Array) = Main.main(listener, args) + + /** + * transport.logBytes option in qpid-jms + */ + @Test + fun sendAndReceiveSingleMessageLogBytes() { + val senderParameters = + "sender --log-msgs dict --out json --broker $brokerUrl --address $address --count 1 --log-bytes".split(" ").toTypedArray() + val receiverParameters = + "receiver --log-msgs dict --out json --broker $brokerUrl --address $address --count 1 --log-bytes".split(" ").toTypedArray() + Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10)) { + print("Sending: ") + main(senderParameters) + print("Receiving: ") + main(receiverParameters) + } + } + + // https://issues.apache.org/jira/browse/QPIDJMS-342 + @Tag("issue") + @Test + fun attemptConnectingToWrongPortWithTimeout() { + val senderParameters = + ("connector" + + " --broker-uri failover:(amqp://127.0.0.1:1883)?jms.connectTimeout=1&failover.maxReconnectAttempts=1&failover.startupMaxReconnectAttempts=1" + + "" + ).split(" ").toTypedArray() + assertTimeoutPreemptively(Duration.ofSeconds(5)) { + assertSystemExit(1, Executable { + main(senderParameters) + }) + } + } +} + +// @Test fun sendAndReceiveSingleMessageSsl() { +// val senderParameters = +// "sender --log-msgs dict --broker amqps://127.0.0.1:61616 --address lalaLand --count 1 --conn-ssl-truststore-location /home/jdanek/Downloads/AMQ7/amq7cr1i0/redhatqe.truststore --conn-ssl-truststore-password password --conn-ssl-keystore-location /home/jdanek/Downloads/AMQ7/amq7cr1i0/client.keystore --conn-ssl-keystore-password password".split(" ").toTypedArray() +// val receiverParameters = +// "receiver --log-msgs dict --broker amqp://127.0.0.1:5672 --address lalaLand --count 1".split(" ").toTypedArray() +// print("Sending: ") +// Main.main(senderParameters) +//// print("Receiving: ") +//// Main.main(receiverParameters) +// } +// +// @Test fun reconnect() { +// val host = "amqp://127.0.0.1:61616" +// val address = "reconnect" +// Main.main( +// "receiver --timeout 120 --log-msgs dict --broker $host --conn-reconnect True --conn-username admin --conn-password admin --address $address --count 1".split(" ").toTypedArray() +// ) +// } +// +// @Test fun isAddressMulticast() { +//// val host = "amqp://172.17.0.4:5672 --conn-username a --conn-password b" +// val host = "amqp://172.17.0.4:5672" +// val address = "multicast" +// +// val r1 = Thread { +// Main.main( +// "receiver --log-msgs dict --broker $host --address $address --count 2 --timeout 10".split(" ").toTypedArray() +// ) +// } +// +// val r2 = Thread { +// Main.main( +// "receiver --log-msgs dict --broker $host --address $address --count 2 --timeout 10".split(" ").toTypedArray() +// ) +// +// } +// +// val s1 = Thread { +// Main.main( +// "sender --log-msgs dict --broker $host --address $address --count 2 --msg-content AMQP%d".split(" ").toTypedArray() +// ) +// } +// +// r1.start() +// r2.start() +// Thread.sleep(1 * 1000) +// s1.start() +// +// s1.join() +// r1.join() +// r2.join() +// } +// +// @Test fun sendSingleMessageToTopicAndRollback() { +// val senderParameters = +// ("sender --log-msgs dict --broker amqp://127.0.0.1:5672 --address topic://topic --count 1 --tx-action rollback tx-endloop-action rollback".split(" ").toTypedArray()) +// print("Sending: ") +// Main.main(senderParameters) +// } +//} diff --git a/cli-qpid-jms-1x/src/test/kotlin/MessageFormatterTest.kt b/cli-qpid-jms-1x/src/test/kotlin/MessageFormatterTest.kt new file mode 100644 index 00000000..5f68ed79 --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/MessageFormatterTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2017 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.redhat.mqe.lib.AbstractJmsMessageFormatterTest +import org.apache.qpid.jms.message.JmsBytesMessage +import org.apache.qpid.jms.provider.amqp.message.AmqpJmsBytesMessageFacade +import javax.jms.BytesMessage + +class AacJmsMessageFormatterTest : AbstractJmsMessageFormatterTest() { + override fun getBytesMessage(): BytesMessage = JmsBytesMessage(AmqpJmsBytesMessageFacade()) +} diff --git a/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS286Test.kt b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS286Test.kt new file mode 100644 index 00000000..9f93f4fb --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS286Test.kt @@ -0,0 +1,43 @@ +import com.google.common.truth.Truth +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test + +/* + * Copyright (c) 2017 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@Tags(Tag("issue"), Tag("external")) +class QPIDJMS286Test { + @Test + fun `uri options are not visible in thread names`() { + val f = org.apache.qpid.jms.JmsConnectionFactory( + "amqp://127.0.0.1:5672?jms.username=anUserName&jms.password=aPassword&amqp.vhost=aVHostNotPassword") + val c = f.createConnection() + val s = c.createSession() + + val threadSet = Thread.getAllStackTraces().keys + threadSet.forEach { + println(it.name) + Truth.assertThat(it.name).doesNotContain("Password") + } + + s.close() + c.close() + } +} diff --git a/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS357Test.kt b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS357Test.kt new file mode 100644 index 00000000..5ec81b08 --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS357Test.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2018 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Tags +import org.junit.jupiter.api.Test +import java.math.BigInteger +import java.util.* +import javax.jms.Connection +import javax.jms.ConnectionFactory +import javax.jms.Session + +@Tags(Tag("issue"), Tag("external")) +class QPIDJMS357Test { + val prefix: String = "QPIDJMS357Test_" + lateinit var randomSuffix: String + val address: String + get() = prefix + randomSuffix + + val random = Random() + + @BeforeEach + fun setUp() { + // https://stackoverflow.com/questions/41107/how-to-generate-a-random-alpha-numeric-string + randomSuffix = BigInteger(130, random).toString(32) + } + + class Client { + val INDIVIDUAL_ACKNOWLEDGE = 101 + + lateinit var f: ConnectionFactory + lateinit var c: Connection + lateinit var s: Session + + fun start() { + f = org.apache.qpid.jms.JmsConnectionFactory( + "amqp://127.0.0.1:5672") + c = f.createConnection() + c.start() + s = c.createSession(false, INDIVIDUAL_ACKNOWLEDGE) + } + + fun stop() { + s.close() + c.stop() + c.close() + } + } + + @Test + fun `acknowledge can be switched to individual mode`() { + val client1 = Client() + client1.start() + client1.apply { + val queue = s.createQueue(address) + println(queue) + val producer = s.createProducer(queue) + for (i in 1..3) { + println(i) + val m = s.createMessage() + m.setIntProperty("i", i) + producer.send(m) + } + } + client1.stop() + + Thread.sleep(1000) + + val client2 = Client() + client2.start() + client2.apply { + val queue = s.createQueue(address) + println(queue) + // leave first message unacknowledged, do not receive third + val consumer1 = s.createConsumer(queue) + val m11 = consumer1.receive(2000) + val m12 = consumer1.receive(2000) + s.setMessageListener { } + m12.acknowledge() + consumer1.close() + } + client2.stop() + + val client3 = Client() + client3.start() + client3.apply { + val queue = s.createQueue(address) + // now receive both the first and third message + val consumer2 = s.createConsumer(queue) + val m21 = consumer2.receive(2000) + val m23 = consumer2.receive(2000) + m21.acknowledge() + m23.acknowledge() + consumer2.close() + + assertThat(m21.getIntProperty("i")).isEqualTo(1) + assertThat(m23.getIntProperty("i")).isEqualTo(3) + } + client3.stop() + } +} diff --git a/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS391Test.kt b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS391Test.kt new file mode 100644 index 00000000..a221786b --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS391Test.kt @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2018 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration +import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager +import org.apache.logging.log4j.* +import org.apache.logging.log4j.core.* +import org.apache.logging.log4j.core.appender.AbstractAppender +import org.apache.logging.log4j.core.config.* +import org.apache.qpid.jms.transports.TransportSupport +import org.junit.jupiter.api.* +import org.junit.jupiter.api.io.TempDir +import util.Broker +import java.io.File +import java.math.BigInteger +import java.nio.file.Path +import java.util.* +import javax.jms.Connection +import javax.jms.ConnectionFactory +import javax.jms.Session + +@Tag("issue") +class QPIDJMS391Test { + val prefix: String = "QPIDJMS391Test_" + lateinit var randomSuffix: String + val address: String + get() = prefix + randomSuffix + + val random = Random() + + @BeforeEach + fun checkIfOnRhel() { + Assumptions.assumeTrue(File("/etc/redhat-release").exists()) + } + + @BeforeEach + fun setUp() { + // https://stackoverflow.com/questions/41107/how-to-generate-a-random-alpha-numeric-string + randomSuffix = BigInteger(130, random).toString(32) + } + + @Test + fun `logging says that openssl is being used`(@TempDir tempDir: Path) { + val keystore = this.javaClass.getResourceAsStream("server-side-keystore.jks") + val broker = Broker(tempDir) + configureBroker(broker) + val amqpPort = broker.startBroker() + val amqpsPort = broker.addAMQPSAcceptor(keystore) + + val listAppender = ArrayListAppender.installLogger(TransportSupport::class.java.name, Level.DEBUG) + + // the config option is only used when we create actual ssl connection + val f: ConnectionFactory = org.apache.qpid.jms.JmsConnectionFactory( + "amqps://127.0.0.1:$amqpsPort?transport.useOpenSSL=true&transport.trustAll=true&transport.verifyHost=false" + ) + val c: Connection = f.createConnection(USER_NAME, PASSWORD) + c.start() + val s: Session = c.createSession(Session.AUTO_ACKNOWLEDGE) + s.close() + c.close() + + assertThat(listAppender.messages) + .comparingElementsUsing(Correspondence.from(::regexpCorrespondence, "RegexpCorrespondence())")) + .contains("OpenSSL Enabled: Version .* of OpenSSL will be used") + + broker.close() + } + + private val USER_NAME = "someUser" + private val PASSWORD = "somePassword" + + private fun configureBroker(broker: Broker) { + val securityConfiguration = SecurityConfiguration() + securityConfiguration.addUser(USER_NAME, PASSWORD) + val activeMQJAASSecurityManager = ActiveMQJAASSecurityManager( + "org.apache.activemq.artemis.spi.core.security.jaas.InVMLoginModule", securityConfiguration + ) + broker.embeddedBroker.setSecurityManager(activeMQJAASSecurityManager) + + broker.configuration.isPersistenceEnabled = false + broker.configuration.isSecurityEnabled = true + } + + companion object { + private const val overrideDefaultTLS = "com.ibm.jsse2.overrideDefaultTLS" + + @JvmStatic + fun regexpCorrespondence(actual: String?, expected: String?): Boolean { + return actual!!.matches(Regex(expected!!)) + } + + + @JvmStatic + @BeforeAll + fun setProperty() { + // ENTMQBR-640, enable TLSv1.1 and TLSv1.2 on IBM Java 8 + // https://www.ibm.com/support/knowledgecenter/en/SSYKE2_8.0.0/com.ibm.java.security.component.80.doc/security-component/jsse2Docs/matchsslcontext_tls.html + System.setProperty(overrideDefaultTLS, "true") + } + + @JvmStatic + @AfterAll + fun unsetProperty() { + System.clearProperty(overrideDefaultTLS) + } + } +} + +// https://stackoverflow.com/questions/59713891/appenderskeleton-log4j2 +class ArrayListAppender : AbstractAppender("ArrayListAppender", null, null, true, Property.EMPTY_ARRAY) { + val messages = ArrayList() + + override fun append(event: LogEvent) { + messages.add(event.message.formattedMessage) + } + + companion object { + + /** + * + * Creates an instance and attaches it to log4j2 as logger for the given name. + * + * Blind alleys + * + * val listAppender = ArrayListAppender() + * listAppender.start() + * (LogManager.getContext(true).getLogger(TransportSupport::class.java) as Logger).let { + * it.level = Level.DEBUG + * it.addAppender(listAppender) + * } + * + * That will reconfigure existing logger, and since we don't have config for this yet, it will reconfigure + * the root logger (named ""). It looks up the closest parent, which in this test is going to be root. + * + */ + fun installLogger(loggerName: String, loggerLevel: Level?): ArrayListAppender { + // note this code is specific for log4j2-core, does not use slf4j abstraction nor log4j2 abstraction + val loggerContext = LogManager.getContext(false) as LoggerContext + val configuration = loggerContext.configuration + + val listAppender = ArrayListAppender() + listAppender.start() + + // https://logging.apache.org/log4j/2.x/manual/customconfig.html#AddingToCurrent + configuration.addAppender(listAppender) + val loggerConfig = LoggerConfig.newBuilder() + .withLoggerName(loggerName) + .withLevel(loggerLevel) + .withAdditivity(true) + .withRefs( + arrayOf(AppenderRef.createAppenderRef(listAppender.name, loggerLevel, null)) + ) + .withProperties(null) + .withConfig(loggerContext.configuration) + .withtFilter(null) + .build() + + loggerConfig.addAppender(listAppender, loggerLevel, null) + + configuration.addLogger(loggerName, loggerConfig) + + loggerContext.updateLoggers() + + return listAppender + } + } +} diff --git a/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS502Test.kt b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS502Test.kt new file mode 100644 index 00000000..897f5cf2 --- /dev/null +++ b/cli-qpid-jms-1x/src/test/kotlin/QPIDJMS502Test.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018 Red Hat, Inc. + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import org.apache.qpid.jms.JmsConnection +import org.awaitility.Awaitility.await +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import util.Broker +import java.math.BigInteger +import java.nio.file.Path +import java.util.* +import javax.jms.Connection +import javax.jms.ConnectionFactory +import javax.jms.Session +import org.apache.logging.log4j.Level + +@Tag("issue") +class QPIDJMS502Test { + val prefix: String = "QPIDJMS502Test_" + lateinit var randomSuffix: String + val address: String + get() = prefix + randomSuffix + + val random = Random() + + @BeforeEach + fun setUp() { + // https://stackoverflow.com/questions/41107/how-to-generate-a-random-alpha-numeric-string + randomSuffix = BigInteger(130, random).toString(32) + } + + @Test + fun `failover reconnect is logged`(@TempDir tempDir: Path) { + val broker = Broker(tempDir) + configureBroker(broker) + broker.startBroker() + val amqpPort1 = broker.addAMQPAcceptor() + val amqpPort2 = broker.addAMQPAcceptor() + + val ala = ArrayListAppender.installLogger(JmsConnection::class.java.name, Level.INFO) + + val f: ConnectionFactory = org.apache.qpid.jms.JmsConnectionFactory( + "failover:(amqp://127.0.0.1:$amqpPort1,amqp://127.0.0.1:$amqpPort2)") + val c: Connection = f.createConnection() + c.start() + val s: Session = c.createSession(Session.AUTO_ACKNOWLEDGE) + + val oldConnection = broker.embeddedBroker.activeMQServer.remotingService.connections.first() + val oldId = oldConnection.id + oldConnection.destroy() + + await().untilAsserted { + val connections = broker.embeddedBroker.activeMQServer.remotingService.connections + assertThat(connections).hasSize(1) + assertThat(connections.first().id).isNotEqualTo(oldId) + } + + assertThat(ala.messages) + .comparingElementsUsing(Correspondence.from(::regexpCorrespondence, "RegexpCorrespondence())")) + .contains("Connection .* restored to server: .*") + + s.close() + c.close() + + broker.close() + } + + private fun configureBroker(broker: Broker) { + broker.configuration.isSecurityEnabled = false + broker.configuration.isPersistenceEnabled = false + } + + companion object { + @JvmStatic + fun regexpCorrespondence(actual: String?, expected: String?): Boolean { + return actual!!.matches(Regex(expected!!)) + } + } +} diff --git a/cli-qpid-jms-1x/src/test/resources/log4j2-test.properties b/cli-qpid-jms-1x/src/test/resources/log4j2-test.properties new file mode 100644 index 00000000..bfdba41e --- /dev/null +++ b/cli-qpid-jms-1x/src/test/resources/log4j2-test.properties @@ -0,0 +1,39 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +status=error +rootLogger=debug, console + +logger.lib.name=com.redhat.mqe.lib +logger.lib.level=INFO + +logger.netty.name=io.netty +logger.netty.level=WARN + +# Log transport.traceBytes=true messages +logger.NettyTcpTransport.name=org.apache.qpid.jms.transports.netty.NettyTcpTransport +logger.NettyTcpTransport.level=DEBUG + +# Uncomment this to get debug logs from qpid-jms +#logger.qpid.name=org.apache.qpid +#logger.qpid.level=DEBUG + +# Console appender +appender.console.type=Console +appender.console.name=console +appender.console.target=SYSTEM_ERR diff --git a/cli-qpid-jms-1x/src/test/resources/server-side-keystore.jks b/cli-qpid-jms-1x/src/test/resources/server-side-keystore.jks new file mode 100644 index 00000000..88a80522 Binary files /dev/null and b/cli-qpid-jms-1x/src/test/resources/server-side-keystore.jks differ diff --git a/cli-qpid-jms/pom.xml b/cli-qpid-jms/pom.xml index 57a1d93b..6b055dd3 100644 --- a/cli-qpid-jms/pom.xml +++ b/cli-qpid-jms/pom.xml @@ -35,11 +35,11 @@ jms com.redhat.mqe.jms.Main - 0.58.0 + 2.10.0 ${qpid.jms.client.version} - 2.0.39.Final + 2.0.70.Final linux-x86_64-fedora - 1.6.0 + 1.8.1 @@ -68,7 +68,7 @@ com.redhat.cli-java - jmslib + jakartalib @@ -99,6 +99,12 @@ test-jar test + + com.redhat.cli-java + jakartalib + test-jar + test + com.google.truth truth @@ -117,6 +123,10 @@ test-jar test + + org.apache.logging.log4j + log4j-core + diff --git a/cli-qpid-jms/src/main/java/com/redhat/mqe/jms/AacClientOptions.java b/cli-qpid-jms/src/main/java/com/redhat/mqe/jms/AacClientOptions.java index e4f808c0..1f08a03e 100644 --- a/cli-qpid-jms/src/main/java/com/redhat/mqe/jms/AacClientOptions.java +++ b/cli-qpid-jms/src/main/java/com/redhat/mqe/jms/AacClientOptions.java @@ -39,9 +39,9 @@ public abstract class AacClientOptions extends ClientOptions { private static final Map translationDtestJmsMap = new HashMap<>(); private final List