From 34d77c6d27dd5367cc49751996d915169ef31298 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Fri, 4 Nov 2022 14:31:14 +0100 Subject: [PATCH 1/6] 4445 JMS connector - NACK support #4445 - WLS custom connector for thin client #5290 Signed-off-by: Daniel Kec 4445 JMS connector Signed-off-by: Daniel Kec JMS nacking DLQ Signed-off-by: Daniel Kec JMS nacking DLQ Signed-off-by: Daniel Kec WLS Connector cleanup Signed-off-by: Daniel Kec WLS Connector cleanup Signed-off-by: Daniel Kec Test fix Signed-off-by: Daniel Kec Example and WLS 14 support Signed-off-by: Daniel Kec Example and WLS 14 support Signed-off-by: Daniel Kec Revert kafka stuff Signed-off-by: Daniel Kec --- bom/pom.xml | 5 + docs/mp/reactivemessaging/weblogic.adoc | 215 ++++++++++++++ docs/sitegen.yaml | 1 + examples/messaging/pom.xml | 1 + examples/messaging/weblogic-jms-mp/README.md | 24 ++ .../weblogic-jms-mp/buildAndRunWeblogic.sh | 53 ++++ .../weblogic-jms-mp/extractThinClientLib.sh | 22 ++ examples/messaging/weblogic-jms-mp/pom.xml | 88 ++++++ .../examples/messaging/mp/FrankResource.java | 94 ++++++ .../examples/messaging/mp/package-info.java | 20 ++ .../src/main/resources/META-INF/beans.xml | 24 ++ .../src/main/resources/WEB/favicon.ico | Bin 0 -> 1230 bytes .../src/main/resources/WEB/img/arrow-1.png | Bin 0 -> 18222 bytes .../src/main/resources/WEB/img/arrow-2.png | Bin 0 -> 31965 bytes .../src/main/resources/WEB/img/cloud.png | Bin 0 -> 2104 bytes .../src/main/resources/WEB/img/frank.png | Bin 0 -> 10516 bytes .../src/main/resources/WEB/index.html | 101 +++++++ .../src/main/resources/WEB/main.css | 171 +++++++++++ .../src/main/resources/application.yaml | 42 +++ .../src/main/resources/logging.properties | 35 +++ .../weblogic-jms-mp/weblogic/Dockerfile | 54 ++++ .../container-scripts/create-wls-domain.py | 104 +++++++ .../createAndStartEmptyDomain.sh | 87 ++++++ .../container-scripts/get_healthcheck_url.sh | 22 ++ .../container-scripts/setupTestJMSQueue.py | 115 ++++++++ .../weblogic/properties/domain.properties | 35 +++ .../properties/domain_security.properties | 18 ++ .../connectors/aq/AqConnectorImpl.java | 11 +- .../connectors/aq/AqMessageImpl.java | 4 +- .../messaging/connectors/aq/AckTest.java | 4 +- .../jms/shim/JakartaDestination.java | 12 +- .../connectors/jms/shim/JakartaJms.java | 54 ++++ .../connectors/jms/shim/JakartaMessage.java | 10 +- .../jms/shim/JakartaMessageProducer.java | 13 +- .../connectors/jms/shim/JakartaProducer.java | 13 +- .../connectors/jms/shim/JakartaSession.java | 21 +- .../connectors/jms/shim/JakartaWrapper.java | 32 ++ messaging/connectors/jms/pom.xml | 4 + .../connectors/jms/AbstractJmsMessage.java | 17 +- .../connectors/jms/ConnectionContext.java | 9 +- .../connectors/jms/JmsBytesMessage.java | 10 +- .../connectors/jms/JmsConnector.java | 279 +++++++++++++----- .../connectors/jms/JmsNackHandler.java | 218 ++++++++++++++ .../connectors/jms/JmsTextMessage.java | 10 +- .../connectors/jms/MessageMapper.java | 37 +++ .../connectors/jms/MessageMappers.java | 8 +- .../connectors/jms/OutgoingJmsMessage.java | 4 +- .../jms/src/main/java/module-info.java | 4 +- .../kafka/KafkaConsumerMessage.java | 4 +- ...NackHandler.java => KafkaNackHandler.java} | 23 +- .../connectors/kafka/KafkaPublisher.java | 2 +- messaging/connectors/pom.xml | 3 +- messaging/connectors/wls-jms/pom.xml | 64 ++++ .../wls/IsolatedContextFactory.java | 51 ++++ .../connectors/wls/ThinClientClassLoader.java | 106 +++++++ .../connectors/wls/WeblogicConnector.java | 167 +++++++++++ .../wls/WlsConnectorConfigAliases.java | 63 ++++ .../connectors/wls/package-info.java | 20 ++ .../wls-jms/src/main/java/module-info.java | 32 ++ .../src/main/resources/META-INF/beans.xml | 24 ++ .../io/helidon/messaging/NackHandler.java | 51 ++++ .../connectors/jms/AbstractJmsTest.java | 8 +- .../connectors/jms/AbstractSampleBean.java | 73 ++--- .../connectors/jms/AssertingHandler.java | 50 ++++ .../messaging/connectors/jms/JmsMpTest.java | 104 +++++-- .../connectors/kafka/KafkaSeTest.java | 2 +- 66 files changed, 2755 insertions(+), 197 deletions(-) create mode 100644 docs/mp/reactivemessaging/weblogic.adoc create mode 100644 examples/messaging/weblogic-jms-mp/README.md create mode 100644 examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh create mode 100644 examples/messaging/weblogic-jms-mp/extractThinClientLib.sh create mode 100644 examples/messaging/weblogic-jms-mp/pom.xml create mode 100644 examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java create mode 100644 examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-2.png create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/cloud.png create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/index.html create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml create mode 100644 examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/Dockerfile create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties create mode 100644 examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties create mode 100644 messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaWrapper.java create mode 100644 messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsNackHandler.java create mode 100644 messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMapper.java rename messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/{NackHandler.java => KafkaNackHandler.java} (90%) create mode 100644 messaging/connectors/wls-jms/pom.xml create mode 100644 messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/IsolatedContextFactory.java create mode 100644 messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/ThinClientClassLoader.java create mode 100644 messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WeblogicConnector.java create mode 100644 messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WlsConnectorConfigAliases.java create mode 100644 messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/package-info.java create mode 100644 messaging/connectors/wls-jms/src/main/java/module-info.java create mode 100644 messaging/connectors/wls-jms/src/main/resources/META-INF/beans.xml create mode 100644 messaging/messaging/src/main/java/io/helidon/messaging/NackHandler.java create mode 100644 tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AssertingHandler.java diff --git a/bom/pom.xml b/bom/pom.xml index 4c60da75d48..ff2d5a235eb 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -1137,6 +1137,11 @@ helidon-messaging-aq ${helidon.version} + + io.helidon.messaging.wls-jms + helidon-messaging-wls-jms + ${helidon.version} + io.helidon.microprofile.tests diff --git a/docs/mp/reactivemessaging/weblogic.adoc b/docs/mp/reactivemessaging/weblogic.adoc new file mode 100644 index 00000000000..710b2ceb43d --- /dev/null +++ b/docs/mp/reactivemessaging/weblogic.adoc @@ -0,0 +1,215 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2022 Oracle and/or its affiliates. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +/////////////////////////////////////////////////////////////////////////////// + += Weblogic JMS Connector +:toc: +:toc-placement: preamble +:description: Reactive Messaging support for Weblogic JMS in Helidon MP +:keywords: helidon, mp, messaging, jms, weblogic, wls, thin +:feature-name: Weblogic JMS connector +:microprofile-bundle: false +:rootdir: {docdir}/../.. + +include::{rootdir}/includes/mp.adoc[] +include::{rootdir}/includes/dependencies.adoc[] + +[source,xml] +---- + + io.helidon.messaging.wls-jms + helidon-messaging-wls-jms + +---- + +== Reactive Weblogic JMS Connector + +Connecting streams to Weblogic JMS with Reactive Messaging couldn't be easier. +This connector extends Helidon JMS connector with special handling for Weblogic T3 thin client. + +Connecting to Weblogic JMS connection factories requires proprietary T3 thin client library which can be obtained from +Weblogic installation. + +WARNING: Avoid placing `wlthint3client.jar` on Helidon classpath, client library location needs to be +configured and loaded by Helidon messaging connector. + +=== Configuration + +Connector name: `helidon-weblogic-jms` + +.Attributes +|=== +|`jms-factory` | JNDI name of the JMS factory configured in Weblogic +|`url` | t3, t3s or http address of Weblogic server +|`thin-jar` | Filepath to the Weblogic thin T3 client jar(wlthint3client.jar), can be usually found within Weblogic installation +`WL_HOME/server/lib/wlthint3client.jar` +|`principal` | Weblogic initial context principal(user) +|`credential` | Weblogic initial context credential(password) +|`type` | Possible values are: `queue`, `topic`. Default value is: `topic` +|`destination` | Queue or topic name in WebLogic CDI Syntax(CDI stands for Create Destination Identifier) +|`acknowledge-mode` |Possible values are: `AUTO_ACKNOWLEDGE`- session automatically acknowledges a client’s receipt of a message, +`CLIENT_ACKNOWLEDGE` - receipt of a message is acknowledged only when `Message.ack()` is called manually, +`DUPS_OK_ACKNOWLEDGE` - session lazily acknowledges the delivery of messages. Default value: `AUTO_ACKNOWLEDGE` +|`transacted` | Indicates whether the session will use a local transaction. Default value: `false` +|`message-selector` | JMS API message selector expression based on a subset of the SQL92. +Expression can only access headers and properties, not the payload. +|`client-id` | Client identifier for JMS connection. +|`client-id` | Client identifier for JMS connection. +|`durable` | True for creating durable consumer (only for topic). Default value: `false` +|`subscriber-name` | Subscriber name for durable consumer used to identify subscription. +|`non-local` | If true then any messages published to the topic using this session's connection, +or any other connection with the same client identifier, +will not be added to the durable subscription. Default value: `false` +|`named-factory` | Select in case factory is injected as a named bean or configured with name. +|`poll-timeout` | Timeout for polling for next message in every poll cycle in millis. Default value: `50` +|`period-executions` | Period for executing poll cycles in millis. Default value: `100` +|`session-group-id` | When multiple channels share same `session-group-id`, they share same JMS session and same JDBC connection as well. +|`producer.unit-of-order` | All messages from same unit of order will be processed sequentially in the order they were created. +|=== + +Configuration is straight forward. Use JNDI for localizing and configuring of JMS ConnectionFactory +from Weblogic. Notice the destination property which is used to define queue with +https://docs.oracle.com/cd/E24329_01/web.1211/e24387/lookup.htm#JMSPG915[WebLogic CDI Syntax](CDI stands for Create Destination Identifier). + +[source,yaml] +.Example config: +---- +mp: + messaging: + connector: + helidon-weblogic-jms: + # JMS factory configured in Weblogic + jms-factory: jms/TestConnectionFactory + # Path to the WLS Thin T3 client jar + thin-jar: wlthint3client.jar + url: t3://localhost:7001 + incoming: + from-wls: + connector: helidon-weblogic-jms + # WebLogic CDI Syntax(CDI stands for Create Destination Identifier) + destination: ./TestJMSModule!TestQueue + outgoing: + to-wls: + connector: helidon-weblogic-jms + destination: ./TestJMSModule!TestQueue +---- + +When configuring destination with WebLogic CDI, the following syntax needs to be applied: + +.Non-Distributed Destinations +`jms-server-name/jms-module-name!destination-name` + +In our example we are replacing jms-server-name with `.` as we don’t have to look up the server we are connected to. + +.Uniform Distributed Destinations (UDDs) +`jms-server-name/jms-module-name!jms-server-name@udd-name` + +Destination for UDD doesn't have `./` prefix, because distributed destinations can be served by multiple servers within a cluster. + +=== Consuming + +[source,java] +.Consuming one by one unwrapped value: +---- +@Incoming("from-wls") +public void consumeWls(String msg) { + System.out.println("Weblogic says: " + msg); +} +---- + +[source,java] +.Consuming one by one, manual ack: +---- +@Incoming("from-wls") +@Acknowledgment(Acknowledgment.Strategy.MANUAL) +public CompletionStage consumewls(JmsMessage msg) { + System.out.println("Weblogic says: " + msg.getPayload()); + return msg.ack(); +} +---- + +=== Producing + +[source,java] +.Producing to Weblogic JMS: +---- +@Outgoing("to-wls") +public PublisherBuilder produceToWls() { + return ReactiveStreams.of("test1", "test2"); +} +---- + +[source,java] +.Example of more advanced producing to Weblogic JMS: +---- +@Outgoing("to-wls") +public PublisherBuilder produceToJms() { + return ReactiveStreams.of("test1", "test2") + .map(s -> JmsMessage.builder(s) + .correlationId(UUID.randomUUID().toString()) + .property("stringProp", "cool property") + .property("byteProp", 4) + .property("intProp", 5) + .onAck(() -> System.out.println("Acked!")) + .build()); +} +---- +[source,java] +.Example of even more advanced producing to Weblogic JMS with custom mapper: +---- +@Outgoing("to-wls") +public PublisherBuilder produceToJms() { + return ReactiveStreams.of("test1", "test2") + .map(s -> JmsMessage.builder(s) + .customMapper((p, session) -> { + TextMessage textMessage = session.createTextMessage(p); + textMessage.setStringProperty("custom-mapped-property", "XXX" + p); + return textMessage; + }) + .build() + ); +} +---- + +=== Secured t3 over SSL(t3s) +For initiating SSL secured t3 connection, trust keystore with WLS public certificate is needed. +Standard WLS installation has pre-configured Demo trust store: `WL_HOME/server/lib/DemoTrust.jks`, +we can store it locally for connecting WLS over t3s. + +[source,yaml] +.Example config: +---- +mp: + messaging: + connector: + helidon-weblogic-jms: + jms-factory: jms/TestConnectionFactory + thin-jar: wlthint3client.jar + # Notice t3s protocol is used + url: t3s://localhost:7002 +---- + +Helidon application needs to be aware about our WLS SSL public certificate. + +[source,bash] +.Running example with WLS truststore +---- +java --add-opens=java.base/java.io=ALL-UNNAMED \ +-Djavax.net.ssl.trustStore=DemoTrust.jks \ +-Djavax.net.ssl.trustStorePassword=DemoTrustKeyStorePassPhrase \ +-jar ./target/helidon-wls-jms-demo.jar +---- \ No newline at end of file diff --git a/docs/sitegen.yaml b/docs/sitegen.yaml index f60c6c5367b..447b7f53726 100644 --- a/docs/sitegen.yaml +++ b/docs/sitegen.yaml @@ -409,6 +409,7 @@ backend: - "kafka.adoc" - "jms.adoc" - "aq.adoc" + - "weblogic.adoc" - "mock.adoc" - type: "PAGE" title: "REST Client" diff --git a/examples/messaging/pom.xml b/examples/messaging/pom.xml index 297f5f0521b..bc079601fd7 100644 --- a/examples/messaging/pom.xml +++ b/examples/messaging/pom.xml @@ -39,5 +39,6 @@ jms-websocket-mp jms-websocket-se oracle-aq-websocket-mp + weblogic-jms-mp diff --git a/examples/messaging/weblogic-jms-mp/README.md b/examples/messaging/weblogic-jms-mp/README.md new file mode 100644 index 00000000000..21023110bf4 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/README.md @@ -0,0 +1,24 @@ +# Helidon Messaging with Oracle Weblogic Example + +## Prerequisites +* JDK 17+ +* Maven +* Docker +* Account at https://container-registry.oracle.com/ with accepted Oracle Standard Terms and Restrictions for Weblogic. + +## Run Weblogic in docker +1. You will need to do a docker login to Oracle container registry with account which previously + accepted Oracle Standard Terms and Restrictions for Weblogic: + `docker login container-registry.oracle.com` +2. Run `bash buildAndRunWeblogic.sh` to build and run example Weblogic container. + * After example JMS resources are deployed, Weblogic console should be available at http://localhost:7001/console with `admin`/`Welcome1` +3. To obtain wlthint3client.jar necessary for connecting to Weblogic execute + `bash extractThinClientLib.sh`, file will be copied to `./weblogic` folder. + +## Build & Run +To run Helidon with thin client, flag `--add-opens=java.base/java.io=ALL-UNNAMED` is needed to +open java.base module to thin client internals. +1. `mvn clean package` +2. `java --add-opens=java.base/java.io=ALL-UNNAMED -jar ./target/weblogic-jms-mp.jar` +3. Visit http://localhost:8080 and try to send and receive messages over Weblogic JMS queue. + diff --git a/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh b/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh new file mode 100644 index 00000000000..11317c060af --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/buildAndRunWeblogic.sh @@ -0,0 +1,53 @@ +#!/bin/bash -e +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +cd ./weblogic + +# Attempt Oracle container registry login. +# You need to accept the licence agreement for Weblogic Server at https://container-registry.oracle.com/ +# Search for weblogic and accept the Oracle Standard Terms and Restrictions +docker login container-registry.oracle.com + +docker build -t wls-admin . + +docker run --rm -d \ + -p 7001:7001 \ + -p 7002:7002 \ + --name wls-admin \ + --hostname wls-admin \ + wls-admin + +printf "Waiting for WLS to start ." +while true; +do + if docker logs wls-admin | grep -q "Server state changed to RUNNING"; then + break; + fi + printf "." + sleep 5 +done +printf " [READY]\n" + +echo Deploying example JMS queues +docker exec wls-admin \ +/bin/bash \ +/u01/oracle/wlserver/common/bin/wlst.sh \ +/u01/oracle/setupTestJMSQueue.py; + +echo Example JMS queues deployed! +echo Console avaiable at http://localhost:7001/console with admin/Welcome1 +echo 'Stop Weblogic server with "docker stop wls-admin"' \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh b/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh new file mode 100644 index 00000000000..b5d6ff1b9ec --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/extractThinClientLib.sh @@ -0,0 +1,22 @@ +#!/bin/bash -e +# +# Copyright (c) 2018, 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + +# Copy wlthint3client.jar from docker container +docker cp wls-admin:/u01/oracle/wlserver/server/lib/wlthint3client.jar ./weblogic/wlthint3client.jar +# Copy DemoTrust.jks from docker container(needed if you want to try t3s protocol) +docker cp wls-admin:/u01/oracle/wlserver/server/lib/DemoTrust.jks ./weblogic/DemoTrust.jks \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/pom.xml b/examples/messaging/weblogic-jms-mp/pom.xml new file mode 100644 index 00000000000..1a3f5d92700 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/pom.xml @@ -0,0 +1,88 @@ + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 3.0.3-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.examples.messaging.wls + weblogic-jms-mp + 1.0-SNAPSHOT + weblogic-jms-mp + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + + io.helidon.microprofile.messaging + helidon-microprofile-messaging + + + io.helidon.messaging.wls-jms + helidon-messaging-wls-jms + + + + org.glassfish.jersey.media + jersey-media-sse + + + + org.jboss + jandex + runtime + true + + + jakarta.activation + jakarta.activation-api + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java new file mode 100644 index 00000000000..1e0062b14d7 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/FrankResource.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.examples.messaging.mp; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.helidon.messaging.connectors.jms.JmsMessage; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseBroadcaster; +import jakarta.ws.rs.sse.SseEventSink; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.glassfish.jersey.media.sse.OutboundEvent; + +/** + * SSE Jax-Rs resource for message publishing and consuming. + */ +@Path("/frank") +@ApplicationScoped +public class FrankResource { + + @Inject + @Channel("to-wls") + private Emitter emitter; + private SseBroadcaster sseBroadcaster; + + /** + * Consuming JMS messages from Weblogic and sending them to the client over SSE. + * + * @param msg dequeued message + * @return completion stage marking end of the processing + */ + @Incoming("from-wls") + public CompletionStage receive(JmsMessage msg) { + if (sseBroadcaster == null) { + System.out.println("No SSE client subscribed yet: " + msg.getPayload()); + return CompletableFuture.completedStage(null); + } + sseBroadcaster.broadcast(new OutboundEvent.Builder().data(msg.getPayload()).build()); + return CompletableFuture.completedStage(null); + } + + /** + * Send message to Weblogic JMS queue. + * + * @param msg message to be sent + */ + @POST + @Path("/send/{msg}") + public void send(@PathParam("msg") String msg) { + emitter.send(msg); + } + + /** + * Register SSE client to listen for messages coming from Weblogic JMS. + * + * @param eventSink client sink + * @param sse SSE context + */ + @GET + @Path("sse") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void listenToEvents(@Context SseEventSink eventSink, @Context Sse sse) { + if (sseBroadcaster == null) { + sseBroadcaster = sse.newBroadcaster(); + } + sseBroadcaster.register(eventSink); + } +} diff --git a/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java new file mode 100644 index 00000000000..372cec04320 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/java/io/helidon/examples/messaging/mp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Helidon MP Reactive Messaging with Weblogic JMS. + */ +package io.helidon.examples.messaging.mp; diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml b/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..80727f9c7fd --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/META-INF/beans.xml @@ -0,0 +1,24 @@ + + + + diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d91659fdb53c934af789e9b93e2d92c679f975d5 GIT binary patch literal 1230 zcmV;<1Tp)GP)*8l(j8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11WQRoK~z|Uy_Z{T6lE00e`jX8mqMWtT1?w51zW;RQDQ<{=()Hdzz zw!1sW2fMg*x83dr`hWW7ob&&FCuhEyofXfTJ#V90OB%YTYYw|#iqKDJj0GMwK@Y%Lzb*2G_bGtSTx(vr7!0A-X zHY!8QihhFz^yc-LWJMPj_pR+MShshrgzV#hCzVZx^mxsF+CGF=0g7G^6xo&($5mbY zRU=mL$Bvzg{1cN{v^qesy}_f-QEV5Pufbps^#z|0E2J zJyz}e*Pm0dHv$96aq&}Dc-QIF?TMP7ook+Zwz{~M`igKuIKUO}#-k`(0XumEaWYG1 zxfWQ4oQVxi%aZoGC63%h^D_sdv4n5{-+}5+6kbjPjv=^9XFKynxC}7K!Hw-JUA7_Q z+)_8%CRg@Xzp3!vh!A7s!|gQXuF?{h>9p{@R_W@r42p)=SP6W$Hc<9MUBQV5@k8F-Od+!DwicL++HXZpB zC>zZl3`18KfDYSW!={DO8_kV6_NXo&tq5jB2G*gRNnRbVz7S`AxlUu1Z1memKcl6e@BcFUqZIhcm4*geIb?&ONqhXK8GBAj}^Zx{0VTbCj)n4Q0uQ@WOY3GbL<) z4%~OMZLl?FTJv)B(5m^FE27uGo$Wf+`qR~JkfcvhC=~8aSTtitV5uFpcLBN4SYEbu za$=_8ztA~&bCdx!9ntmCFVD8LwzMq*%w*sp*>U&0XotVnRy}M8lMAL;Cjh;-Fyq)9 zPsW1|6E#io=VeCs3=S+l1BWlMtO?K|y>C7OHf>NCK>gxNa_|4+r0 z&65W0&@qMkW5V{hzyUlfzsOTVTLhUNh2P<^|1aOc>Q2L8Ezli}jT;=Gecy(kQ8oa< safbu=4pe_F$QIK3H*|aCS+%?NUyMb7$(BA=Gynhq07*qoM6N<$f|!R&8~^|S literal 0 HcmV?d00001 diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/arrow-1.png new file mode 100644 index 0000000000000000000000000000000000000000..bbba0aef8a6ce9efc80b64f1820f5eb38d83a209 GIT binary patch literal 18222 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 L($S*M>{`;laxc~L literal 0 HcmV?d00001 diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/img/frank.png new file mode 100644 index 0000000000000000000000000000000000000000..51a13d8db8bc02ad1613da3b809cff394462a77a GIT binary patch literal 10516 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 + + + + + + Helidon Reactive Messaging + + + + + +
+
+
+ +
+
Send
+
+
+
+
+
REST call /frank/send/{msg}
+
+
+
SSE messages received
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css new file mode 100644 index 00000000000..496dde4fe6c --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/WEB/main.css @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#root { + background-color: #36ABF2; + font-family: Roboto,sans-serif; + color: #fff; + position: absolute; + overflow-x: hidden; + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + top: 0; + left: 0; + width: 100%; + height: 100%; +} +#root::-webkit-scrollbar { + display: none; /* Safari and Chrome */ +} + +#helidon { + width: 509px; + height: 273px; + position: relative; + left: -509px; + z-index: 4; + background: url('img/frank.png'); +} + +#rest-tip { + position: relative; + top: -80px; + left: 160px; +} + +#rest-tip-arrow { + width: 205px; + height: 304px; + z-index: 4; + top: -20px; + background: url('img/arrow-1.png'); +} +#rest-tip-label { + position: absolute; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; + left: -60px; +} + +#sse-tip { + position: absolute; + overflow: hidden; + display: flex; + width: auto; + height: auto; + top: 5%; + right: 10%; + z-index: 0; +} + +#sse-tip-arrow { + position: relative; + top: -30px; + width: 296px; + height: 262px; + z-index: 4; + background: url('img/arrow-2.png'); +} +#sse-tip-label { + position: relative; + white-space: nowrap; + font-size: 18px; + font-weight: bold; + z-index: 4; +} + +#producer { + float: left; + position: relative; + width: 300px; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 99; +} + +#msgBox { + position: absolute; + width: 300px; + top: 25%; + right: 3%; + height: 100%; + margin: 50px; + padding: 10px; + z-index: 20; +} + +#input { + width: 210px; + height: 22px; + top: 58px; + left: 30px; + background-color: white; + border-radius: 10px; + border-style: solid; + border-color: white; + position: absolute; + z-index: 10; +} + +#inputCloud { + position: relative; + width: 310px; + height: 150px; + background: url('img/cloud.png'); +} + +#msg { + background-color: #D2EBFC; + color: #1A9BF4; + border-radius: 10px; + width: 300px; + height: 50px; + margin: 5px; + display: flex; + padding-left: 10px; + justify-content: center; + align-items: center; + z-index: 99; +} + +#submit { + font-weight: bold; + background-color: aqua; + color: #1A9BF4; + border-radius: 12px; + width: 100px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + margin: 5px; + cursor: pointer; +} + +#snippet { + position: absolute; + top: 15%; + left: 30%; + width: 40%; + z-index: 5; +} + +.hljs { + border-radius: 10px; + font-size: 12px; +} \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml new file mode 100644 index 00000000000..491a791cf5d --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml @@ -0,0 +1,42 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +server.port: 8080 +server.host: 0.0.0.0 + +server.static.classpath.location: /WEB +server.static.classpath.welcome: index.html + +mp: + messaging: + connector: + helidon-weblogic-jms: + # JMS factory configured in Weblogic + jms-factory: jms/TestConnectionFactory + # Path to the WLS Thin T3 client jar(extract it from docker container with extractThinClientLib.sh) + thin-jar: weblogic/wlthint3client.jar + url: "t3://localhost:7001" + producer.unit-of-order: kec1 + incoming: + from-wls: + connector: helidon-weblogic-jms + # WebLogic CDI Syntax(CDI stands for Create Destination Identifier) + destination: ./TestJMSModule!TestQueue + outgoing: + to-wls: + connector: helidon-weblogic-jms + # Same queue is used for simplifying test case + destination: ./TestJMSModule!TestQueue diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties new file mode 100644 index 00000000000..a719fd95607 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Example Logging Configuration File +# For more information see $JAVA_HOME/jre/lib/logging.properties + +# Send messages to the console +handlers=io.helidon.common.HelidonConsoleHandler + +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +# Global logging level. Can be overridden by specific loggers +.level=INFO + +# Component specific log levels +#io.helidon.webserver.level=INFO +#io.helidon.config.level=INFO +#io.helidon.security.level=INFO +#io.helidon.common.level=INFO +#io.netty.level=INFO +#io.helidon.messaging.connectors.wls.level=INFO diff --git a/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile b/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile new file mode 100644 index 00000000000..6e79458ebd5 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/Dockerfile @@ -0,0 +1,54 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# ORACLE DOCKERFILES PROJECT +# -------------------------- +# This docker file is customized, originaly taken from https://github.com/oracle/docker-images +# and extends the Oracle WebLogic image by creating a sample domain. +# +# Base image is available at https://container-registry.oracle.com/ +# +FROM container-registry.oracle.com/middleware/weblogic:14.1.1.0-dev-11 + +ENV ORACLE_HOME=/u01/oracle \ + USER_MEM_ARGS="-Djava.security.egd=file:/dev/./urandom" \ + SCRIPT_FILE=/u01/oracle/createAndStartEmptyDomain.sh \ + HEALTH_SCRIPT_FILE=/u01/oracle/get_healthcheck_url.sh \ + PATH=$PATH:${JAVA_HOME}/bin:/u01/oracle/oracle_common/common/bin:/u01/oracle/wlserver/common/bin + +ENV DOMAIN_NAME="${DOMAIN_NAME:-base_domain}" \ + ADMIN_LISTEN_PORT="${ADMIN_LISTEN_PORT:-7001}" \ + ADMIN_NAME="${ADMIN_NAME:-AdminServer}" \ + DEBUG_FLAG=true \ + PRODUCTION_MODE=dev \ + ADMINISTRATION_PORT_ENABLED="${ADMINISTRATION_PORT_ENABLED:-true}" \ + ADMINISTRATION_PORT="${ADMINISTRATION_PORT:-9002}" + +COPY container-scripts/createAndStartEmptyDomain.sh container-scripts/get_healthcheck_url.sh /u01/oracle/ +COPY container-scripts/create-wls-domain.py container-scripts/setupTestJMSQueue.py /u01/oracle/ +COPY properties/domain.properties /u01/oracle/properties/ + +USER root + +RUN chmod +xr $SCRIPT_FILE $HEALTH_SCRIPT_FILE && \ + chown oracle:root $SCRIPT_FILE /u01/oracle/create-wls-domain.py $HEALTH_SCRIPT_FILE + +USER oracle + +HEALTHCHECK --start-period=10s --timeout=30s --retries=3 CMD curl -k -s --fail `$HEALTH_SCRIPT_FILE` || exit 1 +WORKDIR ${ORACLE_HOME} + +CMD ["/u01/oracle/createAndStartEmptyDomain.sh"] \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py new file mode 100644 index 00000000000..e24167e1fb1 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/create-wls-domain.py @@ -0,0 +1,104 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# WebLogic on Docker Default Domain +# +# Domain, as defined in DOMAIN_NAME, will be created in this script. Name defaults to 'base_domain'. +# +# Since : October, 2014 +# Author: monica.riccelli@oracle.com +# ============================================== +domain_name = os.environ.get("DOMAIN_NAME", "base_domain") +admin_name = os.environ.get("ADMIN_NAME", "AdminServer") +admin_listen_port = int(os.environ.get("ADMIN_LISTEN_PORT", "7001")) +domain_path = '/u01/oracle/user_projects/domains/%s' % domain_name +production_mode = os.environ.get("PRODUCTION_MODE", "prod") +administration_port_enabled = os.environ.get("ADMINISTRATION_PORT_ENABLED", "true") +administration_port = int(os.environ.get("ADMINISTRATION_PORT", "9002")) + +print('domain_name : [%s]' % domain_name); +print('admin_listen_port : [%s]' % admin_listen_port); +print('domain_path : [%s]' % domain_path); +print('production_mode : [%s]' % production_mode); +print('admin name : [%s]' % admin_name); +print('administration_port_enabled : [%s]' % administration_port_enabled); +print('administration_port : [%s]' % administration_port); + +# Open default domain template +# ============================ +readTemplate("/u01/oracle/wlserver/common/templates/wls/wls.jar") + +set('Name', domain_name) +setOption('DomainName', domain_name) + +# Set Administration Port +# ======================= +if administration_port_enabled != "false": + set('AdministrationPort', administration_port) + set('AdministrationPortEnabled', 'false') + +# Disable Admin Console +# -------------------- +# cmo.setConsoleEnabled(false) + +# Configure the Administration Server and SSL port. +# ================================================= +cd('/Servers/AdminServer') +set('Name', admin_name) +set('ListenAddress', '') +set('ListenPort', admin_listen_port) +if administration_port_enabled != "false": + create(admin_name, 'SSL') + cd('SSL/' + admin_name) + set('Enabled', 'True') + +# Define the user password for weblogic +# ===================================== +cd(('/Security/%s/User/weblogic') % domain_name) +cmo.setName(username) +cmo.setPassword(password) + +# Write the domain and close the domain template +# ============================================== +setOption('OverwriteDomain', 'true') +setOption('ServerStartMode',production_mode) + +# Create Node Manager +# =================== +#cd('/NMProperties') +#set('ListenAddress','') +#set('ListenPort',5556) +#set('CrashRecoveryEnabled', 'true') +#set('NativeVersionEnabled', 'true') +#set('StartScriptEnabled', 'false') +#set('SecureListener', 'false') +#set('LogLevel', 'FINEST') + +# Set the Node Manager user name and password +# =========================================== +#cd('/SecurityConfiguration/%s' % domain_name) +#set('NodeManagerUsername', username) +#set('NodeManagerPasswordEncrypted', password) + +# Write Domain +# ============ +writeDomain(domain_path) +closeTemplate() + +# Exit WLST +# ========= +exit() diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh new file mode 100644 index 00000000000..1d1a3e4eaff --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/createAndStartEmptyDomain.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# If AdminServer.log does not exists, container is starting for 1st time +# So it should start NM and also associate with AdminServer +# Otherwise, only start NM (container restarted) +########### SIGTERM handler ############ +function _term() { + echo "Stopping container." + echo "SIGTERM received, shutting down the server!" + ${DOMAIN_HOME}/bin/stopWebLogic.sh +} + +########### SIGKILL handler ############ +function _kill() { + echo "SIGKILL received, shutting down the server!" + kill -9 $childPID +} + +# Set SIGTERM handler +trap _term SIGTERM + +# Set SIGKILL handler +trap _kill SIGKILL + +#Define DOMAIN_HOME +export DOMAIN_HOME=/u01/oracle/user_projects/domains/$DOMAIN_NAME +echo "Domain Home is: " $DOMAIN_HOME + +mkdir -p $ORACLE_HOME/properties +# Create Domain only if 1st execution +if [ ! -e ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log ]; then + echo "Create Domain" + PROPERTIES_FILE=/u01/oracle/properties/domain.properties + if [ ! -e "$PROPERTIES_FILE" ]; then + echo "A properties file with the username and password needs to be supplied." + exit + fi + + # Get Username + USER=`awk '{print $1}' $PROPERTIES_FILE | grep username | cut -d "=" -f2` + if [ -z "$USER" ]; then + echo "The domain username is blank. The Admin username must be set in the properties file." + exit + fi + # Get Password + PASS=`awk '{print $1}' $PROPERTIES_FILE | grep password | cut -d "=" -f2` + if [ -z "$PASS" ]; then + echo "The domain password is blank. The Admin password must be set in the properties file." + exit + fi + + # Create an empty domain + wlst.sh -skipWLSModuleScanning -loadProperties $PROPERTIES_FILE /u01/oracle/create-wls-domain.py + mkdir -p ${DOMAIN_HOME}/servers/${ADMIN_NAME}/security/ + chmod -R g+w ${DOMAIN_HOME} + echo "username=${USER}" >> $DOMAIN_HOME/servers/${ADMIN_NAME}/security/boot.properties + echo "password=${PASS}" >> $DOMAIN_HOME/servers/${ADMIN_NAME}/security/boot.properties + ${DOMAIN_HOME}/bin/setDomainEnv.sh + # Setup JMS examples +# wlst.sh -skipWLSModuleScanning -loadProperties $PROPERTIES_FILE /u01/oracle/setupTestJMSQueue.py +fi + +# Start Admin Server and tail the logs +${DOMAIN_HOME}/startWebLogic.sh +if [ -e ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log ]; then + echo "${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log" +fi +touch ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log +tail -f ${DOMAIN_HOME}/servers/${ADMIN_NAME}/logs/${ADMIN_NAME}.log + +childPID=$! +wait $childPID diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh new file mode 100644 index 00000000000..5eb3ded88e4 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/get_healthcheck_url.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +if [ "$ADMINISTRATION_PORT_ENABLED" = "true" ] ; then + echo "https://{localhost:$ADMINISTRATION_PORT}/weblogic/ready" ; +else + echo "http://{localhost:$ADMIN_LISTEN_PORT}/weblogic/ready" ; +fi diff --git a/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py new file mode 100644 index 00000000000..3269b782ae0 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/container-scripts/setupTestJMSQueue.py @@ -0,0 +1,115 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os.path +import sys + +System.setProperty("weblogic.security.SSL.ignoreHostnameVerification", "true") + +connect("admin","Welcome1","t3://localhost:7001") +adm_name=get('AdminServerName') +sub_deployment_name="TestJMSSubdeployment" +jms_module_name="TestJMSModule" +queue_name="TestQueue" +factory_name="TestConnectionFactory" +jms_server_name="TestJMSServer" + + +def createJMSServer(adm_name, jms_server_name): + cd('/JMSServers') + if (len(ls(returnMap='true')) == 0): + print 'No JMS Server found, creating ' + jms_server_name + cd('/') + cmo.createJMSServer(jms_server_name) + cd('/JMSServers/'+jms_server_name) + cmo.addTarget(getMBean("/Servers/" + adm_name)) + + +def createJMSModule(jms_module_name, adm_name, sub_deployment_name): + print "Creating JMS module " + jms_module_name + cd('/JMSServers') + jms_servers=ls(returnMap='true') + cd('/') + module = create(jms_module_name, "JMSSystemResource") + module.addTarget(getMBean("Servers/"+adm_name)) + cd('/SystemResources/'+jms_module_name) + module.createSubDeployment(sub_deployment_name) + cd('/SystemResources/'+jms_module_name+'/SubDeployments/'+sub_deployment_name) + + list=[] + for i in jms_servers: + list.append(ObjectName(str('com.bea:Name='+i+',Type=JMSServer'))) + set('Targets',jarray.array(list, ObjectName)) + +def getJMSModulePath(jms_module_name): + jms_module_path = "/JMSSystemResources/"+jms_module_name+"/JMSResource/"+jms_module_name + return jms_module_path + +def createJMSQueue(jms_module_name,jms_queue_name): + print "Creating JMS queue " + jms_queue_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createQueue(jms_queue_name) + cd(jms_module_path+'/Queues/'+jms_queue_name) + cmo.setJNDIName("jms/" + jms_queue_name) + cmo.setSubDeploymentName(sub_deployment_name) + +def createDistributedJMSQueue(jms_module_name,jms_queue_name): + print "Creating distributed JMS queue " + jms_queue_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createDistributedQueue(jms_queue_name) + cd(jms_module_path+'/DistributedQueues/'+jms_queue_name) + cmo.setJNDIName("jms/" + jms_queue_name) + +def addMemberQueue(udd_name,queue_name): + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path+'/DistributedQueues/'+udd_name) + cmo.setLoadBalancingPolicy('Round-Robin') + cmo.createDistributedQueueMember(queue_name) + +def createJMSFactory(jms_module_name,jms_fact_name): + print "Creating JMS connection factory " + jms_fact_name + jms_module_path = getJMSModulePath(jms_module_name) + cd(jms_module_path) + cmo.createConnectionFactory(jms_fact_name) + cd(jms_module_path+'/ConnectionFactories/'+jms_fact_name) + cmo.setJNDIName("jms/" + jms_fact_name) + cmo.setSubDeploymentName(sub_deployment_name) + + + +edit() +startEdit() + +print "Server name: "+adm_name + +createJMSServer(adm_name,jms_server_name) +createJMSModule(jms_module_name,adm_name,sub_deployment_name) +createJMSFactory(jms_module_name,factory_name) +createJMSQueue(jms_module_name,queue_name) + +### Unified Distributed Destinations(UDD) example +createDistributedJMSQueue(jms_module_name,"udd_queue") +# Normally member queues would be in different sub-deployments +createJMSQueue(jms_module_name,"ms1@udd_queue") +createJMSQueue(jms_module_name,"ms2@udd_queue") +addMemberQueue("udd_queue", "ms1@udd_queue") +addMemberQueue("udd_queue", "ms2@udd_queue") + +save() +activate(block="true") +disconnect() \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties new file mode 100644 index 00000000000..6e9a5fc4b19 --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain.properties @@ -0,0 +1,35 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Env properties inherited from base image +DOMAIN_NAME=myDomain +ADMIN_LISTEN_PORT=7001 +ADMIN_NAME=myadmin +PRODUCTION_MODE=dev +DEBUG_FLAG=true +ADMINISTRATION_PORT_ENABLED=false +ADMINISTRATION_PORT=9002 +# Env properties for this image +ADMIN_HOST=AdminContainer +MANAGED_SERVER_PORT=8001 +MANAGED_SERVER_NAME_BASE=MS +CONFIGURED_MANAGED_SERVER_COUNT=2 +PRODUCTION_MODE_ENABLED=true +CLUSTER_NAME=cluster1 +CLUSTER_TYPE=DYNAMIC +DOMAIN_HOST_VOLUME=/Users/host/temp +username=admin +password=Welcome1 \ No newline at end of file diff --git a/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties new file mode 100644 index 00000000000..fcfb1d90fff --- /dev/null +++ b/examples/messaging/weblogic-jms-mp/weblogic/properties/domain_security.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2022 Oracle and/or its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +username=admin +password=Welcome1 \ No newline at end of file diff --git a/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqConnectorImpl.java b/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqConnectorImpl.java index 5eb75fa34e4..131149ec0ad 100644 --- a/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqConnectorImpl.java +++ b/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqConnectorImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import io.helidon.config.Config; import io.helidon.config.ConfigValue; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; import io.helidon.messaging.connectors.jms.ConnectionContext; import io.helidon.messaging.connectors.jms.JmsConnector; import io.helidon.messaging.connectors.jms.JmsMessage; @@ -167,15 +168,19 @@ private AQjmsConnectionFactory createAqFactory(Config c) throws javax.jms.JMSExc @Override - protected JmsMessage createMessage(jakarta.jms.Message message, + protected JmsMessage createMessage(NackHandler nackHandler, + jakarta.jms.Message message, Executor executor, SessionMetadata sessionMetadata) { - return new AqMessageImpl<>(super.createMessage(message, executor, sessionMetadata), sessionMetadata); + return new AqMessageImpl<>( + super.createMessage(nackHandler, message, executor, sessionMetadata), + sessionMetadata); } @Override protected BiConsumer, JMSException> sendingErrorHandler(Config config) { return (m, e) -> { + m.nack(e); throw new MessagingException("Error during sending Oracle AQ JMS message.", e); }; } diff --git a/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqMessageImpl.java b/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqMessageImpl.java index 1240e700762..aafe1b3971c 100644 --- a/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqMessageImpl.java +++ b/messaging/connectors/aq/src/main/java/io/helidon/messaging/connectors/aq/AqMessageImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class AqMessageImpl implements AqMessage { if (jakartaSession == null) { this.session = null; } else { - this.session = ((JakartaSession) jakartaSession).unwrap(AQjmsSession.class); + this.session = ((JakartaSession) jakartaSession).unwrap(); } } diff --git a/messaging/connectors/aq/src/test/java/io/helidon/messaging/connectors/aq/AckTest.java b/messaging/connectors/aq/src/test/java/io/helidon/messaging/connectors/aq/AckTest.java index 7533c8fddc4..00d9aa2f667 100644 --- a/messaging/connectors/aq/src/test/java/io/helidon/messaging/connectors/aq/AckTest.java +++ b/messaging/connectors/aq/src/test/java/io/helidon/messaging/connectors/aq/AckTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ void ackPropagationTest() throws InterruptedException, JMSException { }).when(mockedMessage).acknowledge(); AqConnectorImpl aqConnector = new AqConnectorImpl(Map.of(), null, null); - JmsMessage jmsMessage = aqConnector.createMessage(mockedMessage, null, sessionMetadata); + JmsMessage jmsMessage = aqConnector.createMessage(null, mockedMessage, null, sessionMetadata); AqMessage aqMessage = new AqMessageImpl<>(jmsMessage, sessionMetadata); aqMessage.ack(); assertThat("Ack not propagated to JmsMessage", diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaDestination.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaDestination.java index c41dd204b7b..6dd6d1550e6 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaDestination.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaDestination.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,14 +20,20 @@ /** * Exposes Jakarta API, delegates to javax API. */ -class JakartaDestination implements Destination { +class JakartaDestination implements Destination, JakartaWrapper { private final T delegate; JakartaDestination(T delegate) { this.delegate = delegate; } - T unwrap() { + @Override + public T unwrap() { return delegate; } + + @Override + public String toString() { + return delegate.toString(); + } } diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaJms.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaJms.java index 082008514bc..96e4653fb6a 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaJms.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaJms.java @@ -15,6 +15,10 @@ */ package io.helidon.messaging.connectors.jms.shim; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + import jakarta.jms.BytesMessage; import jakarta.jms.CompletionListener; import jakarta.jms.Connection; @@ -57,6 +61,7 @@ private JakartaJms() { * @return shimmed jakarta namespace instance */ public static BytesMessage create(javax.jms.BytesMessage delegate) { + if (delegate == null) return null; return new JakartaByteMessage(delegate); } /** @@ -65,6 +70,7 @@ public static BytesMessage create(javax.jms.BytesMessage delegate) { * @return shimmed jakarta namespace instance */ public static CompletionListener create(javax.jms.CompletionListener delegate) { + if (delegate == null) return null; return new JakartaCompletionListener(delegate); } /** @@ -73,6 +79,7 @@ public static CompletionListener create(javax.jms.CompletionListener delegate) { * @return shimmed jakarta namespace instance */ public static Connection create(javax.jms.Connection delegate) { + if (delegate == null) return null; return new JakartaConnection(delegate); } /** @@ -81,6 +88,7 @@ public static Connection create(javax.jms.Connection delegate) { * @return shimmed jakarta namespace instance */ public static ConnectionConsumer create(javax.jms.ConnectionConsumer delegate) { + if (delegate == null) return null; return new JakartaConnectionConsumer(delegate); } /** @@ -89,6 +97,7 @@ public static ConnectionConsumer create(javax.jms.ConnectionConsumer delegate) { * @return shimmed jakarta namespace instance */ public static ConnectionFactory create(javax.jms.ConnectionFactory delegate) { + if (delegate == null) return null; return new JakartaConnectionFactory(delegate); } /** @@ -97,6 +106,7 @@ public static ConnectionFactory create(javax.jms.ConnectionFactory delegate) { * @return shimmed jakarta namespace instance */ public static ConnectionMetaData create(javax.jms.ConnectionMetaData delegate) { + if (delegate == null) return null; return new JakartaConnectionMetaData(delegate); } /** @@ -105,6 +115,7 @@ public static ConnectionMetaData create(javax.jms.ConnectionMetaData delegate) { * @return shimmed jakarta namespace instance */ public static JMSConsumer create(javax.jms.JMSConsumer delegate) { + if (delegate == null) return null; return new JakartaConsumer(delegate); } /** @@ -113,6 +124,7 @@ public static JMSConsumer create(javax.jms.JMSConsumer delegate) { * @return shimmed jakarta namespace instance */ public static JMSContext create(javax.jms.JMSContext delegate) { + if (delegate == null) return null; return new JakartaContext(delegate); } /** @@ -121,6 +133,7 @@ public static JMSContext create(javax.jms.JMSContext delegate) { * @return shimmed jakarta namespace instance */ public static Destination create(javax.jms.Destination delegate) { + if (delegate == null) return null; return new JakartaDestination<>(delegate); } /** @@ -129,6 +142,7 @@ public static Destination create(javax.jms.Destination delegate) { * @return shimmed jakarta namespace instance */ public static ExceptionListener create(javax.jms.ExceptionListener delegate) { + if (delegate == null) return null; return new JakartaExceptionListener(delegate); } /** @@ -137,8 +151,32 @@ public static ExceptionListener create(javax.jms.ExceptionListener delegate) { * @return shimmed jakarta namespace instance */ public static MapMessage create(javax.jms.MapMessage delegate) { + if (delegate == null) return null; return new JakartaMapMessage(delegate); } + + /** + * Convenience method for shimming various javax JMS classes. + * + * @param obj to be shimmed or just typed + * @param expectedType expected type to shim to + * @return typed or shimmed object + * @param expected type to shim to + */ + public static T resolve(Object obj, Class expectedType) { + if (expectedType.isAssignableFrom(obj.getClass())) { + return (T) obj; + } + Map, Function> conversionMap = Map.of( + ConnectionFactory.class, o -> (T) JakartaJms.create((javax.jms.ConnectionFactory) o), + Destination.class, o -> (T) JakartaJms.create((javax.jms.Destination) o) + ); + return Optional.ofNullable(conversionMap.get(expectedType)) + .map(r -> r.apply(obj)) + .orElseThrow(() -> new IllegalStateException("Unexpected type of connection factory: " + obj.getClass())); + } + + /** * Create a jakarta wrapper for the provided javax instance. * @param delegate javax namespace instance @@ -171,6 +209,7 @@ public static Message create(javax.jms.Message delegate) { * @return shimmed jakarta namespace instance */ public static MessageConsumer create(javax.jms.MessageConsumer delegate) { + if (delegate == null) return null; return new JakartaMessageConsumer(delegate); } /** @@ -179,6 +218,7 @@ public static MessageConsumer create(javax.jms.MessageConsumer delegate) { * @return shimmed jakarta namespace instance */ public static MessageListener create(javax.jms.MessageListener delegate) { + if (delegate == null) return null; return new JakartaMessageListener(delegate); } /** @@ -187,6 +227,7 @@ public static MessageListener create(javax.jms.MessageListener delegate) { * @return shimmed jakarta namespace instance */ public static MessageProducer create(javax.jms.MessageProducer delegate) { + if (delegate == null) return null; return new JakartaMessageProducer(delegate); } /** @@ -195,6 +236,7 @@ public static MessageProducer create(javax.jms.MessageProducer delegate) { * @return shimmed jakarta namespace instance */ public static ObjectMessage create(javax.jms.ObjectMessage delegate) { + if (delegate == null) return null; return new JakartaObjectMessage(delegate); } /** @@ -203,6 +245,7 @@ public static ObjectMessage create(javax.jms.ObjectMessage delegate) { * @return shimmed jakarta namespace instance */ public static JMSProducer create(javax.jms.JMSProducer delegate) { + if (delegate == null) return null; return new JakartaProducer(delegate); } /** @@ -211,6 +254,7 @@ public static JMSProducer create(javax.jms.JMSProducer delegate) { * @return shimmed jakarta namespace instance */ public static Queue create(javax.jms.Queue delegate) { + if (delegate == null) return null; return new JakartaQueue(delegate); } /** @@ -219,6 +263,7 @@ public static Queue create(javax.jms.Queue delegate) { * @return shimmed jakarta namespace instance */ public static QueueBrowser create(javax.jms.QueueBrowser delegate) { + if (delegate == null) return null; return new JakartaQueueBrowser(delegate); } /** @@ -227,6 +272,7 @@ public static QueueBrowser create(javax.jms.QueueBrowser delegate) { * @return shimmed jakarta namespace instance */ public static Session create(javax.jms.Session delegate) { + if (delegate == null) return null; return new JakartaSession(delegate); } /** @@ -235,6 +281,7 @@ public static Session create(javax.jms.Session delegate) { * @return shimmed jakarta namespace instance */ public static ServerSessionPool create(javax.jms.ServerSessionPool delegate) { + if (delegate == null) return null; return new JakartaSessionPool(delegate); } /** @@ -243,6 +290,7 @@ public static ServerSessionPool create(javax.jms.ServerSessionPool delegate) { * @return shimmed jakarta namespace instance */ public static ServerSession create(javax.jms.ServerSession delegate) { + if (delegate == null) return null; return new JakartaServerSession(delegate); } /** @@ -251,6 +299,7 @@ public static ServerSession create(javax.jms.ServerSession delegate) { * @return shimmed jakarta namespace instance */ public static StreamMessage create(javax.jms.StreamMessage delegate) { + if (delegate == null) return null; return new JakartaStreamMessage(delegate); } /** @@ -259,6 +308,7 @@ public static StreamMessage create(javax.jms.StreamMessage delegate) { * @return shimmed jakarta namespace instance */ public static TemporaryQueue create(javax.jms.TemporaryQueue delegate) { + if (delegate == null) return null; return new JakartaTemporaryQueue(delegate); } /** @@ -267,6 +317,7 @@ public static TemporaryQueue create(javax.jms.TemporaryQueue delegate) { * @return shimmed jakarta namespace instance */ public static TemporaryTopic create(javax.jms.TemporaryTopic delegate) { + if (delegate == null) return null; return new JakartaTemporaryTopic(delegate); } /** @@ -275,6 +326,7 @@ public static TemporaryTopic create(javax.jms.TemporaryTopic delegate) { * @return shimmed jakarta namespace instance */ public static TextMessage create(javax.jms.TextMessage delegate) { + if (delegate == null) return null; return new JakartaTextMessage(delegate); } /** @@ -283,6 +335,7 @@ public static TextMessage create(javax.jms.TextMessage delegate) { * @return shimmed jakarta namespace instance */ public static Topic create(javax.jms.Topic delegate) { + if (delegate == null) return null; return new JakartaTopic(delegate); } /** @@ -291,6 +344,7 @@ public static Topic create(javax.jms.Topic delegate) { * @return shimmed jakarta namespace instance */ public static TopicSubscriber create(javax.jms.TopicSubscriber delegate) { + if (delegate == null) return null; return new JakartaTopicSubscriber(delegate); } } diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessage.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessage.java index adc91bf8d7b..4f1d7c14d54 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessage.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ import static io.helidon.messaging.connectors.jms.shim.ShimUtil.call; import static io.helidon.messaging.connectors.jms.shim.ShimUtil.run; -class JakartaMessage implements Message { - private final javax.jms.Message delegate; +class JakartaMessage implements Message, JakartaWrapper { + private final T delegate; - JakartaMessage(javax.jms.Message delegate) { + JakartaMessage(T delegate) { this.delegate = delegate; } @@ -276,7 +276,7 @@ public boolean isBodyAssignableTo(Class c) throws JMSException { return call(() -> delegate.isBodyAssignableTo(c)); } - public javax.jms.Message unwrap() { + public T unwrap() { return delegate; } } diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessageProducer.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessageProducer.java index 1a5fd5f44ac..c1e0fbcadf4 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessageProducer.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaMessageProducer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,10 +24,10 @@ import static io.helidon.messaging.connectors.jms.shim.ShimUtil.call; import static io.helidon.messaging.connectors.jms.shim.ShimUtil.run; -class JakartaMessageProducer implements MessageProducer { - private final javax.jms.MessageProducer delegate; +class JakartaMessageProducer implements MessageProducer, JakartaWrapper { + private final T delegate; - JakartaMessageProducer(javax.jms.MessageProducer delegate) { + JakartaMessageProducer(T delegate) { this.delegate = delegate; } @@ -162,4 +162,9 @@ public void send(Destination destination, timeToLive, JavaxJms.create(completionListener))); } + + @Override + public T unwrap() { + return delegate; + } } diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaProducer.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaProducer.java index f27f4051150..4f0da581595 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaProducer.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaProducer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,15 +27,20 @@ /** * Exposes Jakarta API, delegates to javax API. */ -class JakartaProducer implements JMSProducer { - private final javax.jms.JMSProducer delegate; +class JakartaProducer implements JMSProducer, JakartaWrapper { + private final T delegate; private CompletionListener completionListener; private javax.jms.CompletionListener javaxCompletionListener; - JakartaProducer(javax.jms.JMSProducer delegate) { + JakartaProducer(T delegate) { this.delegate = delegate; } + @Override + public T unwrap() { + return delegate; + } + @Override public JMSProducer send(Destination destination, Message message) { delegate.send(ShimUtil.destination(destination), ShimUtil.message(message)); diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaSession.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaSession.java index 4b0d09df3dc..55697e3aeab 100644 --- a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaSession.java +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaSession.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2022 Oracle and/or its affiliates. + * Copyright (c) 2021, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,11 +40,13 @@ /** * Exposes Jakarta API, delegates to javax API. + * + * @param Type of the javax delegate */ -public class JakartaSession implements Session { - private final javax.jms.Session delegate; +public class JakartaSession implements Session, JakartaWrapper { + private final T delegate; - JakartaSession(javax.jms.Session delegate) { + JakartaSession(T delegate) { this.delegate = delegate; } @@ -251,11 +253,18 @@ public void unsubscribe(String name) throws JMSException { * Unwrap the underlying instance of javax session. * * @param type class to unwrap to - * @param type to unwrap to + * @param type to unwrap to * @return unwrapped session + * @deprecated since 3.0.3, use {@link io.helidon.messaging.connectors.jms.shim.JakartaSession#unwrap()} instead. * @throws java.lang.ClassCastException in case the underlying instance is not compatible with the requested type */ - public T unwrap(Class type) { + @Deprecated(forRemoval = true, since = "3.0.3") + public S unwrap(Class type) { return type.cast(delegate); } + + @Override + public T unwrap() { + return delegate; + } } diff --git a/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaWrapper.java b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaWrapper.java new file mode 100644 index 00000000000..cdad45281b4 --- /dev/null +++ b/messaging/connectors/jms-shim/src/main/java/io/helidon/messaging/connectors/jms/shim/JakartaWrapper.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging.connectors.jms.shim; + +/** + * Jakarta JMS shim objects with accessible delegate. + * + * @param Javax JMS delegate type + */ +public interface JakartaWrapper { + + /** + * Unwrap the underlying javax instance. + * + * @return unwrapped javax delegate + */ + T unwrap(); +} diff --git a/messaging/connectors/jms/pom.xml b/messaging/connectors/jms/pom.xml index 320bc07b6d9..73662c2dd4b 100644 --- a/messaging/connectors/jms/pom.xml +++ b/messaging/connectors/jms/pom.xml @@ -63,6 +63,10 @@ provided true
+ + io.helidon.messaging + helidon-messaging-jms-shim + jakarta.jms jakarta.jms-api diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/AbstractJmsMessage.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/AbstractJmsMessage.java index 1d25e1bddb7..30081660b78 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/AbstractJmsMessage.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/AbstractJmsMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +23,12 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; import jakarta.jms.Connection; import jakarta.jms.ConnectionFactory; @@ -40,11 +42,12 @@ abstract class AbstractJmsMessage implements JmsMessage { private Executor executor; private SessionMetadata sharedSessionEntry; private volatile boolean acked = false; + private final NackHandler nackHandler; - protected AbstractJmsMessage() { - } - - protected AbstractJmsMessage(Executor executor, SessionMetadata sharedSessionEntry) { + protected AbstractJmsMessage(NackHandler nackHandler, + Executor executor, + SessionMetadata sharedSessionEntry) { + this.nackHandler = nackHandler; this.sharedSessionEntry = sharedSessionEntry; this.executor = executor; } @@ -116,4 +119,8 @@ public CompletionStage ack() { }); } + @Override + public Function> getNack() { + return this.nackHandler != null ? this.nackHandler.getNack(this) : reason -> CompletableFuture.completedFuture(null); + } } diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/ConnectionContext.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/ConnectionContext.java index 328322b65dc..cb62c9e1349 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/ConnectionContext.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/ConnectionContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import io.helidon.config.Config; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.connectors.jms.shim.JakartaJms; import jakarta.jms.ConnectionFactory; import jakarta.jms.Destination; @@ -87,11 +88,13 @@ Optional lookupDestination() { } Optional lookupFactory(String jndi) { - return Optional.ofNullable((ConnectionFactory) lookup(jndi)); + return Optional.ofNullable(lookup(jndi)) + .map(o -> JakartaJms.resolve(o, ConnectionFactory.class)); } Optional lookupDestination(String jndi) { - return Optional.ofNullable((Destination) lookup(jndi)); + return Optional.ofNullable((Destination) lookup(jndi)) + .map(o -> JakartaJms.resolve(o, Destination.class)); } diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsBytesMessage.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsBytesMessage.java index df2c4a54a24..2b73c492ab2 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsBytesMessage.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsBytesMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.concurrent.Executor; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; import jakarta.jms.BytesMessage; import jakarta.jms.JMSException; @@ -33,8 +34,11 @@ public class JmsBytesMessage extends AbstractJmsMessage { private final jakarta.jms.BytesMessage msg; - JmsBytesMessage(jakarta.jms.BytesMessage msg, Executor executor, SessionMetadata sharedSessionEntry) { - super(executor, sharedSessionEntry); + JmsBytesMessage(NackHandler nackHandler, + jakarta.jms.BytesMessage msg, + Executor executor, + SessionMetadata sharedSessionEntry) { + super(nackHandler, executor, sharedSessionEntry); this.msg = msg; } diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsConnector.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsConnector.java index d8e40459cd2..04f2069af60 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsConnector.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsConnector.java @@ -43,7 +43,9 @@ import io.helidon.config.ConfigValue; import io.helidon.config.mp.MpConfig; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; import io.helidon.messaging.Stoppable; +import io.helidon.messaging.connectors.jms.shim.JakartaWrapper; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.BeforeDestroyed; @@ -64,6 +66,7 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.eclipse.microprofile.reactive.messaging.spi.ConnectorAttribute; import org.eclipse.microprofile.reactive.messaging.spi.IncomingConnectorFactory; import org.eclipse.microprofile.reactive.messaging.spi.OutgoingConnectorFactory; import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; @@ -76,6 +79,102 @@ */ @ApplicationScoped @Connector(JmsConnector.CONNECTOR_NAME) +@ConnectorAttribute(name = JmsConnector.USERNAME_ATTRIBUTE, + description = "User name used to connect JMS session", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.PASSWORD_ATTRIBUTE, + description = "Password to connect JMS session", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.TYPE_ATTRIBUTE, + description = "Possible values are: queue, topic", + defaultValue = "queue", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.DESTINATION_ATTRIBUTE, + description = "Queue or topic name", + mandatory = true, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.ACK_MODE_ATTRIBUTE, + description = "Possible values are: " + + "AUTO_ACKNOWLEDGE- session automatically acknowledges a client’s receipt of a message, " + + "CLIENT_ACKNOWLEDGE - receipt of a message is acknowledged only when Message.ack() is called manually, " + + "DUPS_OK_ACKNOWLEDGE - session lazily acknowledges the delivery of messages.", + defaultValue = "AUTO_ACKNOWLEDGE", + direction = ConnectorAttribute.Direction.INCOMING, + type = "io.helidon.messaging.connectors.jms.AcknowledgeMode") +@ConnectorAttribute(name = JmsConnector.TRANSACTED_ATTRIBUTE, + description = "Indicates whether the session will use a local transaction.", + mandatory = false, + defaultValue = "false", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "boolean") +@ConnectorAttribute(name = JmsConnector.MESSAGE_SELECTOR_ATTRIBUTE, + description = "JMS API message selector expression based on a subset of the SQL92. " + + "Expression can only access headers and properties, not the payload.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING, + type = "string") +@ConnectorAttribute(name = JmsConnector.CLIENT_ID_ATTRIBUTE, + description = "Client identifier for JMS connection.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.DURABLE_ATTRIBUTE, + description = "True for creating durable consumer (only for topic).", + mandatory = false, + defaultValue = "false", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "boolean") +@ConnectorAttribute(name = JmsConnector.SUBSCRIBER_NAME_ATTRIBUTE, + description = "Subscriber name for durable consumer used to identify subscription.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING, + type = "string") +@ConnectorAttribute(name = JmsConnector.NON_LOCAL_ATTRIBUTE, + description = "If true then any messages published to the topic using this session’s connection, " + + "or any other connection with the same client identifier, " + + "will not be added to the durable subscription.", + mandatory = false, + defaultValue = "false", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "boolean") +@ConnectorAttribute(name = JmsConnector.NAMED_FACTORY_ATTRIBUTE, + description = "Select in case factory is injected as a named bean or configured with name.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.POLL_TIMEOUT_ATTRIBUTE, + description = "Timeout for polling for next message in every poll cycle in millis. Default value: 50", + mandatory = false, + defaultValue = "50", + direction = ConnectorAttribute.Direction.INCOMING, + type = "long") +@ConnectorAttribute(name = JmsConnector.PERIOD_EXECUTIONS_ATTRIBUTE, + description = "Period for executing poll cycles in millis.", + mandatory = false, + defaultValue = "100", + direction = ConnectorAttribute.Direction.INCOMING, + type = "long") +@ConnectorAttribute(name = JmsConnector.SESSION_GROUP_ID_ATTRIBUTE, + description = "When multiple channels share same session-group-id, " + + "they share same JMS session and same JDBC connection as well.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.JNDI_ATTRIBUTE + "." + JmsConnector.JNDI_JMS_FACTORY_ATTRIBUTE, + description = "JNDI name of JMS factory.", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = JmsConnector.JNDI_ATTRIBUTE + "." + JmsConnector.JNDI_PROPS_ATTRIBUTE, + description = "Environment properties used for creating initial context java.naming.factory.initial, " + + "java.naming.provider.url …", + mandatory = false, + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "properties") public class JmsConnector implements IncomingConnectorFactory, OutgoingConnectorFactory, Stoppable { private static final Logger LOGGER = Logger.getLogger(JmsConnector.class.getName()); @@ -254,18 +353,22 @@ void terminate(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event) /** * Create reactive messaging message from JMS message. * + * @param nackHandler Not acknowledged handler * @param message JMS message * @param executor executor used for async execution of ack * @param sessionMetadata JMS session metadata * @return reactive messaging message extended with custom JMS features */ - protected JmsMessage createMessage(jakarta.jms.Message message, Executor executor, SessionMetadata sessionMetadata) { + protected JmsMessage createMessage(NackHandler nackHandler, + jakarta.jms.Message message, + Executor executor, + SessionMetadata sessionMetadata) { if (message instanceof TextMessage) { - return new JmsTextMessage((TextMessage) message, executor, sessionMetadata); + return new JmsTextMessage(nackHandler, (TextMessage) message, executor, sessionMetadata); } else if (message instanceof BytesMessage) { - return new JmsBytesMessage((BytesMessage) message, executor, sessionMetadata); + return new JmsBytesMessage(nackHandler, (BytesMessage) message, executor, sessionMetadata); } else { - return new AbstractJmsMessage(executor, sessionMetadata) { + return new AbstractJmsMessage(nackHandler, executor, sessionMetadata) { @Override public jakarta.jms.Message getJmsMessage() { @@ -331,39 +434,38 @@ public PublisherBuilder> getPublisherBuilder(Config mpConfi SessionMetadata sessionEntry = prepareSession(config, factory); Destination destination = createDestination(sessionEntry.session(), ctx); - String messageSelector = config.get(MESSAGE_SELECTOR_ATTRIBUTE).asString().orElse(null); - String subscriberName = config.get(SUBSCRIBER_NAME_ATTRIBUTE).asString().orElse(null); - MessageConsumer consumer; - if (config.get(DURABLE_ATTRIBUTE).asBoolean().orElse(false)) { - if (!(destination instanceof Topic)) { - throw new MessagingException("Can't create durable consumer. Only topic can be durable!"); - } - consumer = sessionEntry.session().createDurableSubscriber( - (Topic) destination, - subscriberName, - messageSelector, - config.get(NON_LOCAL_ATTRIBUTE).asBoolean().orElse(false)); - } else { - consumer = sessionEntry.session().createConsumer(destination, messageSelector); - } + MessageConsumer consumer = createConsumer(config, destination, sessionEntry); BufferedEmittingPublisher> emitter = BufferedEmittingPublisher.create(); + JmsNackHandler nackHandler = JmsNackHandler.create(emitter, config, this); Long pollTimeout = config.get(POLL_TIMEOUT_ATTRIBUTE) .asLong() .orElse(POLL_TIMEOUT_DEFAULT); + Long periodExecutions = config.get(PERIOD_EXECUTIONS_ATTRIBUTE) + .asLong() + .orElse(PERIOD_EXECUTIONS_DEFAULT); + AtomicReference> lastMessage = new AtomicReference<>(); scheduler.scheduleAtFixedRate( - () -> produce(emitter, sessionEntry, consumer, ackMode, awaitAck, pollTimeout, lastMessage), - 0, - config.get(PERIOD_EXECUTIONS_ATTRIBUTE) - .asLong() - .orElse(PERIOD_EXECUTIONS_DEFAULT), - TimeUnit.MILLISECONDS); + () -> { + if (!emitter.hasRequests()) { + return; + } + // When await-ack is true, no message is received until previous one is acked + if (ackMode != AcknowledgeMode.AUTO_ACKNOWLEDGE + && awaitAck + && lastMessage.get() != null + && !lastMessage.get().isAck()) { + return; + } + produce(emitter, sessionEntry, consumer, nackHandler, pollTimeout) + .ifPresent(lastMessage::set); + }, 0, periodExecutions, TimeUnit.MILLISECONDS); sessionEntry.connection().start(); return ReactiveStreams.fromPublisher(FlowAdapters.toPublisher(Multi.create(emitter))); } catch (JMSException e) { @@ -384,9 +486,8 @@ public SubscriberBuilder, Void> getSubscriberBuilder(Config SessionMetadata sessionEntry = prepareSession(config, factory); Session session = sessionEntry.session(); Destination destination = createDestination(session, ctx); - MessageProducer producer = session.createProducer(destination); - configureProducer(producer, ctx); - AtomicReference mapper = new AtomicReference<>(); + MessageProducer producer = createProducer(destination, ctx, sessionEntry); + AtomicReference mapper = new AtomicReference<>(); return ReactiveStreams.>builder() .flatMapCompletionStage(m -> consume(m, session, mapper, producer, config)) .onError(t -> LOGGER.log(Level.SEVERE, t, () -> "Error intercepted from channel " @@ -401,7 +502,15 @@ private void configureProducer(MessageProducer producer, ConnectionContext ctx) io.helidon.config.Config config = ctx.config().get("producer"); if (!config.exists()) return; - Class clazz = producer.getClass(); + final Object instance; + // Shim producer? + if (producer instanceof JakartaWrapper) { + instance = ((JakartaWrapper) producer).unwrap(); + } else { + instance = producer; + } + + Class clazz = instance.getClass(); Map setterMethods = Arrays.stream(clazz.getDeclaredMethods()) .filter(m -> m.getParameterCount() == 1) .collect(Collectors.toMap(m -> ConfigHelper.stripSet(m.getName()), Function.identity())); @@ -417,7 +526,7 @@ private void configureProducer(MessageProducer producer, ConnectionContext ctx) return; } try { - m.invoke(producer, c.as(m.getParameterTypes()[0]).get()); + m.invoke(instance, c.as(m.getParameterTypes()[0]).get()); } catch (Throwable e) { LOGGER.log(Level.WARNING, "Error when setting JMS producer property " + key @@ -428,43 +537,31 @@ private void configureProducer(MessageProducer producer, ConnectionContext ctx) }); } - private void produce( + private Optional> produce( BufferedEmittingPublisher> emitter, SessionMetadata sessionEntry, MessageConsumer consumer, - AcknowledgeMode ackMode, - Boolean awaitAck, - Long pollTimeout, - AtomicReference> lastMessage) { - - if (!emitter.hasRequests()) { - return; - } - // When await-ack is true, no message is received until previous one is acked - if (ackMode != AcknowledgeMode.AUTO_ACKNOWLEDGE - && awaitAck - && lastMessage.get() != null - && !lastMessage.get().isAck()) { - return; - } + JmsNackHandler nackHandler, + Long pollTimeout) { try { jakarta.jms.Message message = consumer.receive(pollTimeout); if (message == null) { - return; + return Optional.empty(); } LOGGER.fine(() -> "Received message: " + message); - JmsMessage preparedMessage = createMessage(message, executor, sessionEntry); - lastMessage.set(preparedMessage); + JmsMessage preparedMessage = createMessage(nackHandler, message, executor, sessionEntry); emitter.emit(preparedMessage); + return Optional.of(preparedMessage); } catch (Throwable e) { emitter.fail(e); + return Optional.empty(); } } - private CompletionStage consume( + CompletionStage consume( Message m, Session session, - AtomicReference mapper, + AtomicReference mapper, MessageProducer producer, io.helidon.config.Config config) { @@ -474,28 +571,34 @@ private CompletionStage consume( } return CompletableFuture - .supplyAsync(() -> { - try { - jakarta.jms.Message jmsMessage; - - if (m instanceof OutgoingJmsMessage) { - // custom mapping, properties etc. - jmsMessage = ((OutgoingJmsMessage) m).toJmsMessage(session, mapper.get()); - } else { - // default mappers - jmsMessage = mapper.get().apply(session, m); - } - // actual send - producer.send(jmsMessage); - return m.ack(); - } catch (JMSException e) { - sendingErrorHandler(config).accept(m, e); - } - return CompletableFuture.completedFuture(null); - }, executor) + .supplyAsync(() -> consumeAsync(m, session, mapper, producer, config), executor) .thenApply(aVoid -> m); } + protected CompletionStage consumeAsync(Message m, + Session session, + AtomicReference mapper, + MessageProducer producer, + io.helidon.config.Config config) { + try { + jakarta.jms.Message jmsMessage; + + if (m instanceof OutgoingJmsMessage) { + // custom mapping, properties etc. + jmsMessage = ((OutgoingJmsMessage) m).toJmsMessage(session, mapper.get()); + } else { + // default mappers + jmsMessage = mapper.get().apply(session, m); + } + // actual send + producer.send(jmsMessage); + return m.ack(); + } catch (JMSException e) { + sendingErrorHandler(config).accept(m, e); + } + return CompletableFuture.completedFuture(null); + } + /** * Customizable handler for errors during sending. * @@ -504,12 +607,13 @@ private CompletionStage consume( */ protected BiConsumer, JMSException> sendingErrorHandler(io.helidon.config.Config config) { return (m, e) -> { + m.nack(e); throw new MessagingException("Error during sending JMS message.", e); }; } - private SessionMetadata prepareSession(io.helidon.config.Config config, - ConnectionFactory factory) throws JMSException { + protected SessionMetadata prepareSession(io.helidon.config.Config config, + ConnectionFactory factory) throws JMSException { Optional sessionGroupId = config.get(SESSION_GROUP_ID_ATTRIBUTE).asString().asOptional(); if (sessionGroupId.isPresent() && sessionRegister.containsKey(sessionGroupId.get())) { return sessionRegister.get(sessionGroupId.get()); @@ -543,9 +647,10 @@ private SessionMetadata prepareSession(io.helidon.config.Config config, sessionRegister.put(sessionGroupId.orElseGet(() -> UUID.randomUUID().toString()), sharedSessionEntry); return sharedSessionEntry; } + } - Destination createDestination(Session session, ConnectionContext ctx) { + protected Destination createDestination(Session session, ConnectionContext ctx) { io.helidon.config.Config config = ctx.config(); if (ctx.isJndi()) { @@ -582,6 +687,34 @@ Destination createDestination(Session session, ConnectionContext ctx) { } + protected MessageConsumer createConsumer(io.helidon.config.Config config, + Destination destination, + SessionMetadata sessionEntry) throws JMSException { + String messageSelector = config.get(MESSAGE_SELECTOR_ATTRIBUTE).asString().orElse(null); + String subscriberName = config.get(SUBSCRIBER_NAME_ATTRIBUTE).asString().orElse(null); + + if (config.get(DURABLE_ATTRIBUTE).asBoolean().orElse(false)) { + if (!(destination instanceof Topic)) { + throw new MessagingException("Can't create durable consumer. Only topic can be durable!"); + } + return sessionEntry.session().createDurableSubscriber( + (Topic) destination, + subscriberName, + messageSelector, + config.get(NON_LOCAL_ATTRIBUTE).asBoolean().orElse(false)); + } else { + return sessionEntry.session().createConsumer(destination, messageSelector); + } + } + + protected MessageProducer createProducer(Destination destination, + ConnectionContext ctx, + SessionMetadata sessionEntry) throws JMSException { + MessageProducer producer = sessionEntry.session().createProducer(destination); + configureProducer(producer, ctx); + return producer; + } + /** * Builder for {@link io.helidon.messaging.connectors.jms.JmsConnector}. */ diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsNackHandler.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsNackHandler.java new file mode 100644 index 00000000000..2c6214649c7 --- /dev/null +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsNackHandler.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging.connectors.jms; + +import java.util.HashMap; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import io.helidon.common.reactive.BufferedEmittingPublisher; +import io.helidon.config.Config; +import io.helidon.config.ConfigSources; +import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; + +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; +import jakarta.jms.JMSException; +import jakarta.jms.MessageProducer; +import org.eclipse.microprofile.reactive.messaging.Message; + +import static java.lang.System.Logger.Level.ERROR; +import static java.lang.System.Logger.Level.WARNING; + +abstract class JmsNackHandler implements NackHandler> { + + static JmsNackHandler create(BufferedEmittingPublisher> emitter, + Config config, + JmsConnector jmsConnector) { + Config dlq = config.get("nack-dlq"); + Config logOnly = config.get("nack-log-only"); + if (dlq.exists()) { + dlq = dlq.detach(); + return new JmsDLQ(config, dlq, jmsConnector); + } else if (logOnly.exists() && logOnly.asBoolean().orElse(true)) { + logOnly = logOnly.detach(); + return new JmsNackHandler.Log(config, logOnly); + } + // Default nack handling strategy + return new JmsNackHandler.KillChannel(emitter, config); + } + + static class Log extends JmsNackHandler { + + private final System.Logger logger; + private final String channelName; + private final System.Logger.Level level; + + Log(Config config, Config logOnlyConfig) { + this.channelName = config.get(JmsConnector.CHANNEL_NAME_ATTRIBUTE) + .asString() + .orElseThrow(() -> new MessagingException("Missing channel name!")); + + this.level = logOnlyConfig.get("level") + .as(System.Logger.Level.class) + .orElse(WARNING); + + this.logger = System.getLogger(logOnlyConfig.get("logger") + .asString() + .orElse(JmsNackHandler.class.getName())); + } + + + @Override + public Function> getNack(JmsMessage message) { + return t -> nack(t, message); + } + + private CompletionStage nack(Throwable t, JmsMessage message) { + logger.log(level, messageToString("NACKED Message ignored", channelName, message)); + message.ack(); + return CompletableFuture.completedFuture(null); + } + } + + static class KillChannel extends JmsNackHandler { + + private static final System.Logger LOGGER = System.getLogger(JmsNackHandler.KillChannel.class.getName()); + private final BufferedEmittingPublisher> emitter; + private final String channelName; + + KillChannel(BufferedEmittingPublisher> emitter, Config config) { + this.emitter = emitter; + this.channelName = config.get(JmsConnector.CHANNEL_NAME_ATTRIBUTE) + .asString() + .orElseThrow(() -> new MessagingException("Missing channel name!")); + } + + @Override + public Function> getNack(JmsMessage message) { + return throwable -> nack(throwable, message); + } + + private CompletionStage nack(Throwable t, JmsMessage message) { + LOGGER.log(ERROR, messageToString("NACKED message, killing the channel", channelName, message), t); + emitter.fail(t); + return CompletableFuture.failedStage(t); + } + } + + static String messageToString(String prefix, String channel, JmsMessage message) { + StringBuilder msg = new StringBuilder(prefix); + msg.append("\n"); + appendNonNull(msg, "channel", channel); + appendNonNull(msg, "correlationId", message.getCorrelationId()); + appendNonNull(msg, "replyTo", message.getReplyTo()); + for (String prop : message.getPropertyNames()) { + appendNonNull(msg, prop, message.getProperty(prop)); + } + return msg.toString(); + } + + static StringBuilder appendNonNull(StringBuilder sb, String name, Object value) { + if (Objects.isNull(value)) return sb; + return sb.append(name + ": ").append(value).append("\n"); + } + + static class JmsDLQ extends JmsNackHandler { + private static final System.Logger LOGGER = System.getLogger(JmsNackHandler.JmsDLQ.class.getName()); + private final MessageProducer producer; + private final SessionMetadata sessionMetadata; + private final AtomicReference mapper = new AtomicReference<>(); + private final String channelName; + private Config config; + private JmsConnector jmsConnector; + private Config dlq; + + JmsDLQ(Config config, Config dlq, JmsConnector jmsConnector) { + this.config = config; + this.jmsConnector = jmsConnector; + this.channelName = config.get(JmsConnector.CHANNEL_NAME_ATTRIBUTE) + .asString() + .orElseThrow(() -> new MessagingException("Missing channel name!")); + + Config.Builder dlqCfgBuilder = Config.builder(); + HashMap dlqCfgMap = new HashMap<>(); + if (dlq.isLeaf()) { + // nack-dlq=destination_name - Uses actual connection config, just set dlq destination + String destination = dlq.asString().orElseThrow(() -> new MessagingException("nack-dlq with no value!")); + dlqCfgMap.put(JmsConnector.DESTINATION_ATTRIBUTE, destination); + dlqCfgMap.put("type", "queue"); // default is queue + this.dlq = dlqCfgBuilder + .sources( + ConfigSources.create(dlqCfgMap), + ConfigSources.create(config.detach()) + ) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .build(); + } else { + // Custom dlq connection config + this.dlq = dlq.detach(); + } + + try { + ConnectionContext ctx = new ConnectionContext(this.dlq); + ConnectionFactory factory = jmsConnector.getFactory(ctx) + .orElseThrow(() -> new MessagingException("No ConnectionFactory found.")); + sessionMetadata = jmsConnector.prepareSession(dlq, factory); + Destination destination = jmsConnector.createDestination(sessionMetadata.session(), ctx); + producer = jmsConnector.createProducer(destination, ctx, sessionMetadata); + } catch (JMSException e) { + throw new MessagingException("Error when setting up DLQ nack handler for channel " + channelName, e); + } + } + + @Override + public Function> getNack(JmsMessage message) { + return throwable -> nack(throwable, message); + } + + private CompletionStage nack(Throwable t, JmsMessage message) { + try { + + Throwable cause = t; + while (cause.getCause() != null && cause != cause.getCause()) { + cause = cause.getCause(); + } + + // It has to be incoming JMS message as this nack handler cannot be used outside of connector + JmsMessage.OutgoingJmsMessageBuilder builder = JmsMessage.builder(message.getJmsMessage()); + builder.property(DLQ_ERROR_PROP, cause.getClass().getName()) + .property(DLQ_ERROR_MSG_PROP, cause.getMessage()) + .correlationId(message.getCorrelationId()) + .payload(message.getPayload()); + + config.get(JmsConnector.DESTINATION_ATTRIBUTE) + .asString() + .ifPresent(s -> builder.property(DLQ_ORIG_TOPIC_PROP, s)); + + Message dlqMessage = builder.build(); + jmsConnector.consume(dlqMessage, sessionMetadata.session(), mapper, producer, config); + } catch (Throwable e) { + e.addSuppressed(t); + LOGGER.log(ERROR, "Error when sending nacked message to DLQ", e); + return CompletableFuture.completedStage(null); + } + return CompletableFuture.completedStage(null); + } + } +} + diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsTextMessage.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsTextMessage.java index 154aeb37b36..e875c7f7864 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsTextMessage.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/JmsTextMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.util.concurrent.Executor; import io.helidon.messaging.MessagingException; +import io.helidon.messaging.NackHandler; import jakarta.jms.JMSException; @@ -29,8 +30,11 @@ public class JmsTextMessage extends AbstractJmsMessage { private final jakarta.jms.TextMessage msg; - JmsTextMessage(jakarta.jms.TextMessage msg, Executor executor, SessionMetadata sharedSessionEntry) { - super(executor, sharedSessionEntry); + JmsTextMessage(NackHandler nackHandler, + jakarta.jms.TextMessage msg, + Executor executor, + SessionMetadata sharedSessionEntry) { + super(nackHandler, executor, sharedSessionEntry); this.msg = msg; } diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMapper.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMapper.java new file mode 100644 index 00000000000..8bbfe770e25 --- /dev/null +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging.connectors.jms; + +import jakarta.jms.JMSException; +import jakarta.jms.Message; +import jakarta.jms.Session; + +/** + * Mapper used for translating reactive messaging message to JMS message. + */ +@FunctionalInterface +public interface MessageMapper { + /** + * Convert messaging message to JMS message. + * + * @param s JMS session + * @param m Reactive messaging message to be converted + * @return JMS message + * @throws JMSException + */ + Message apply(Session s, org.eclipse.microprofile.reactive.messaging.Message m) throws JMSException; +} diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMappers.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMappers.java index ba955ff0261..ffbe8841243 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMappers.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/MessageMappers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,6 @@ import jakarta.jms.BytesMessage; import jakarta.jms.JMSException; -import jakarta.jms.Message; -import jakarta.jms.Session; class MessageMappers { @@ -57,8 +55,4 @@ static MessageMapper getJmsMessageMapper(org.eclipse.microprofile.reactive.messa }); } - @FunctionalInterface - interface MessageMapper { - Message apply(Session s, org.eclipse.microprofile.reactive.messaging.Message m) throws JMSException; - } } diff --git a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/OutgoingJmsMessage.java b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/OutgoingJmsMessage.java index 1cf2ddd1409..c9fe2987aa1 100644 --- a/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/OutgoingJmsMessage.java +++ b/messaging/connectors/jms/src/main/java/io/helidon/messaging/connectors/jms/OutgoingJmsMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ void postProcess(PostProcessor processor) { this.postProcessors.add(processor); } - jakarta.jms.Message toJmsMessage(Session session, MessageMappers.MessageMapper defaultMapper) throws JMSException { + jakarta.jms.Message toJmsMessage(Session session, MessageMapper defaultMapper) throws JMSException { jakarta.jms.Message jmsMessage; if (mapper != null) { jmsMessage = mapper.apply(getPayload(), session); diff --git a/messaging/connectors/jms/src/main/java/module-info.java b/messaging/connectors/jms/src/main/java/module-info.java index 31fa2af968f..1f683b63b7d 100644 --- a/messaging/connectors/jms/src/main/java/module-info.java +++ b/messaging/connectors/jms/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,9 +31,11 @@ requires io.helidon.common.context; requires io.helidon.common.reactive; requires io.helidon.common.configurable; + requires io.helidon.messaging.jms.shim; requires io.helidon.messaging; requires microprofile.config.api; requires java.naming; + requires javax.jms.api; exports io.helidon.messaging.connectors.jms; } diff --git a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConsumerMessage.java b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConsumerMessage.java index 8086afbdf04..dcaae635327 100644 --- a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConsumerMessage.java +++ b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaConsumerMessage.java @@ -37,7 +37,7 @@ class KafkaConsumerMessage implements KafkaMessage { private final CompletableFuture ack; - private final NackHandler nack; + private final KafkaNackHandler nack; private final long millisWaitingTimeout; private final AtomicBoolean acked = new AtomicBoolean(); private final ConsumerRecord consumerRecord; @@ -52,7 +52,7 @@ class KafkaConsumerMessage implements KafkaMessage { */ KafkaConsumerMessage(ConsumerRecord consumerRecord, CompletableFuture ack, - NackHandler nack, + KafkaNackHandler nack, long millisWaitingTimeout) { Objects.requireNonNull(consumerRecord); this.consumerRecord = consumerRecord; diff --git a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/NackHandler.java b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaNackHandler.java similarity index 90% rename from messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/NackHandler.java rename to messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaNackHandler.java index 0dc3b76038e..08f216a8dc3 100644 --- a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/NackHandler.java +++ b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaNackHandler.java @@ -27,34 +27,35 @@ import io.helidon.common.reactive.EmittingPublisher; import io.helidon.common.reactive.Multi; import io.helidon.config.Config; +import io.helidon.messaging.NackHandler; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.header.internals.RecordHeader; import org.reactivestreams.FlowAdapters; -interface NackHandler { +interface KafkaNackHandler extends NackHandler> { Function> getNack(KafkaMessage message); - static NackHandler create(EmittingPublisher> emitter, Config config) { + static KafkaNackHandler create(EmittingPublisher> emitter, Config config) { Config dlq = config.get("nack-dlq"); Config logOnly = config.get("nack-log-only"); if (dlq.exists()) { dlq = dlq.detach(); - return new NackHandler.KafkaDLQ<>(emitter, config, dlq); + return new KafkaNackHandler.KafkaDLQ<>(emitter, config, dlq); } else if (logOnly.exists() && logOnly.asBoolean().orElse(true)) { logOnly = logOnly.detach(); - return new NackHandler.Log<>(config, logOnly); + return new KafkaNackHandler.Log<>(config, logOnly); } - return new NackHandler.KillChannel<>(emitter); + return new KafkaNackHandler.KillChannel<>(emitter); } - class Log implements NackHandler { + class Log implements KafkaNackHandler { Log(Config config, Config logOnlyConfig) { } - private static final Logger LOGGER = Logger.getLogger(NackHandler.Log.class.getName()); + private static final Logger LOGGER = Logger.getLogger(KafkaNackHandler.Log.class.getName()); @Override public Function> getNack(KafkaMessage message) { @@ -67,9 +68,9 @@ private CompletionStage nack(Throwable t, KafkaMessage message) { } } - class KillChannel implements NackHandler { + class KillChannel implements KafkaNackHandler { - private static final Logger LOGGER = Logger.getLogger(NackHandler.KillChannel.class.getName()); + private static final Logger LOGGER = Logger.getLogger(KafkaNackHandler.KillChannel.class.getName()); private final EmittingPublisher> emitter; KillChannel(EmittingPublisher> emitter) { @@ -88,7 +89,7 @@ private CompletionStage nack(Throwable t, KafkaMessage message) { } } - class KafkaDLQ implements NackHandler { + class KafkaDLQ implements KafkaNackHandler { private static final String DESERIALIZER_MASK = "([^.]*)Deserializer([^.]*$)"; private static final String DESERIALIZER_REPLACEMENT = "$1Serializer$2"; @@ -149,7 +150,7 @@ class KafkaDLQ implements NackHandler { @Override public Function> getNack(KafkaMessage message) { - return throwable -> this.nack(throwable, message); + return t -> this.nack(t, message); } private CompletionStage nack(Throwable t, KafkaMessage origMsg) { diff --git a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaPublisher.java b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaPublisher.java index 1dbc973d16e..3a0c233b8d8 100644 --- a/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaPublisher.java +++ b/messaging/connectors/kafka/src/main/java/io/helidon/messaging/connectors/kafka/KafkaPublisher.java @@ -124,7 +124,7 @@ private void start() { kafkaConsumer.subscribe(topics, partitionsAssignedLatch); } - NackHandler nack = NackHandler.create(emitter, config); + KafkaNackHandler nack = KafkaNackHandler.create(emitter, config); // This thread reads from Kafka topics and push in kafkaBufferedEvents scheduler.scheduleAtFixedRate(() -> { diff --git a/messaging/connectors/pom.xml b/messaging/connectors/pom.xml index d2c4cc413f4..0507245d0d4 100644 --- a/messaging/connectors/pom.xml +++ b/messaging/connectors/pom.xml @@ -32,9 +32,10 @@ kafka + jms-shim jms aq - jms-shim + wls-jms mock diff --git a/messaging/connectors/wls-jms/pom.xml b/messaging/connectors/wls-jms/pom.xml new file mode 100644 index 00000000000..0f9dd9fa44d --- /dev/null +++ b/messaging/connectors/wls-jms/pom.xml @@ -0,0 +1,64 @@ + + + + + + 4.0.0 + + + io.helidon.messaging + helidon-messaging-connectors-project + 3.0.3-SNAPSHOT + + + io.helidon.messaging.wls-jms + helidon-messaging-wls-jms + jar + Helidon Messaging Weblogic JMS Connector + + + + io.helidon.messaging.jms + helidon-messaging-jms + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + true + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.mockito + mockito-core + test + + + diff --git a/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/IsolatedContextFactory.java b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/IsolatedContextFactory.java new file mode 100644 index 00000000000..fee2e576d05 --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/IsolatedContextFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.messaging.connectors.wls; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Hashtable; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; + +/** + * Initial JNDI context for Weblogic thin client initial context loaded by different classloader. + */ +public class IsolatedContextFactory implements InitialContextFactory { + + private static final String WLS_INIT_CTX_FACTORY = "weblogic.jndi.WLInitialContextFactory"; + + @Override + public Context getInitialContext(Hashtable env) throws NamingException { + return ThinClientClassLoader.executeInIsolation(() -> { + try { + Class wlInitialContextFactory = ThinClientClassLoader.getInstance().loadClass(WLS_INIT_CTX_FACTORY); + Constructor contextFactoryConstructor = wlInitialContextFactory.getConstructor(); + InitialContextFactory contextFactoryInstance = (InitialContextFactory) contextFactoryConstructor.newInstance(); + return contextFactoryInstance.getInitialContext(env); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Cannot find " + WLS_INIT_CTX_FACTORY, e); + } catch (NoSuchMethodException + | InvocationTargetException + | InstantiationException + | IllegalAccessException e) { + throw new RuntimeException("Cannot instantiate " + WLS_INIT_CTX_FACTORY, e); + } + }); + } +} diff --git a/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/ThinClientClassLoader.java b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/ThinClientClassLoader.java new file mode 100644 index 00000000000..cc0c6cb6a01 --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/ThinClientClassLoader.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.messaging.connectors.wls; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; + +import io.helidon.common.LazyValue; + +import static java.lang.System.Logger.Level.TRACE; + +class ThinClientClassLoader extends URLClassLoader { + + private static final System.Logger LOGGER = System.getLogger(ThinClientClassLoader.class.getName()); + private static final LazyValue ISOLATION_CL = LazyValue.create(ThinClientClassLoader::new); + private static volatile String thinJarLocation = "wlthint3client.jar"; + private final ClassLoader contextClassLoader; + + ThinClientClassLoader() { + super("thinClientClassLoader", new URL[0], null); + contextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + + File currDirFile = Path.of("", thinJarLocation).toFile(); + LOGGER.log(TRACE, "Looking for Weblogic thin client jar file " + currDirFile.getPath() + " on filesystem"); + if (currDirFile.exists()) { + this.addURL(currDirFile.toURI().toURL()); + return; + } + + throw new RuntimeException("Can't locate thin jar file " + thinJarLocation); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (inWlsJar(name)) { + try { + return super.loadClass(name, resolve); + } catch (ClassNotFoundException e) { + LOGGER.log(TRACE, "Cannot load class " + name + " from WLS thin client classloader.", e); + contextClassLoader.loadClass(name); + } + } + return contextClassLoader.loadClass(name); + } + + @Override + public URL getResource(String name) { + + if (inWlsJar(name)) { + return super.getResource(name); + } + return contextClassLoader.getResource(name); + } + + static ThinClientClassLoader getInstance() { + return ISOLATION_CL.get(); + } + + static void setThinJarLocation(String thinJarLocation) { + ThinClientClassLoader.thinJarLocation = thinJarLocation; + } + + static T executeInIsolation(IsolationSupplier supplier) { + ClassLoader originalCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(ISOLATION_CL.get()); + return supplier.get(); + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + Thread.currentThread().setContextClassLoader(originalCl); + } + } + + boolean inWlsJar(String name) { + // Load only javax JMS API from outside, so cast works + return !name.startsWith("javax.jms") + && !name.equals(IsolatedContextFactory.class.getName()); + } + + @FunctionalInterface + interface IsolationSupplier { + T get() throws Throwable; + } +} diff --git a/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WeblogicConnector.java b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WeblogicConnector.java new file mode 100644 index 00000000000..952670a6ac5 --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WeblogicConnector.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.helidon.messaging.connectors.wls; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +import io.helidon.config.Config; +import io.helidon.messaging.connectors.jms.ConnectionContext; +import io.helidon.messaging.connectors.jms.JmsConnector; +import io.helidon.messaging.connectors.jms.MessageMapper; +import io.helidon.messaging.connectors.jms.SessionMetadata; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.jms.ConnectionFactory; +import jakarta.jms.Destination; +import jakarta.jms.JMSException; +import jakarta.jms.MessageConsumer; +import jakarta.jms.MessageProducer; +import jakarta.jms.Session; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.spi.Connector; +import org.eclipse.microprofile.reactive.messaging.spi.ConnectorAttribute; +import org.eclipse.microprofile.reactive.streams.operators.PublisherBuilder; +import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; + +import static io.helidon.messaging.connectors.wls.ThinClientClassLoader.executeInIsolation; + +/** + * MicroProfile Reactive Messaging Weblogic JMS connector. + */ +@ApplicationScoped +@Connector(WeblogicConnector.CONNECTOR_NAME) +@ConnectorAttribute(name = WeblogicConnector.WLS_URL, + description = "Weblogic server URL", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + mandatory = true, + type = "string") +@ConnectorAttribute(name = WeblogicConnector.THIN_CLIENT_PATH, + description = "Filepath to the Weblogic thin T3 client jar(wlthint3client.jar), " + + "can be usually found within Weblogic installation 'server/lib/wlthint3client.jar'", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + mandatory = true, + type = "string") +@ConnectorAttribute(name = WeblogicConnector.JMS_FACTORY_ATTRIBUTE, + description = "Weblogic JMS factory name", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = WeblogicConnector.WLS_INIT_CONTEXT_PRINCIPAL, + description = "Weblogic initial context principal(user)", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = WeblogicConnector.WLS_INIT_CONTEXT_CREDENTIAL, + description = "Weblogic initial context credential(password)", + direction = ConnectorAttribute.Direction.INCOMING_AND_OUTGOING, + type = "string") +@ConnectorAttribute(name = "producer.unit-of-order", + description = "All messages from same unit of order will be processed sequentially in the order they were " + + "created.", + direction = ConnectorAttribute.Direction.OUTGOING, + type = "string") +@ConnectorAttribute(name = "producer.compression-threshold", + description = "Max bytes number of serialized message body so any message that exceeds this limit " + + "will trigger message compression.", + direction = ConnectorAttribute.Direction.OUTGOING, + type = "int") +@ConnectorAttribute(name = "producer.redelivery-limit", + description = "Number of times message is redelivered after recover or rollback.", + direction = ConnectorAttribute.Direction.OUTGOING, + type = "int") +@ConnectorAttribute(name = "producer.send-timeout", + description = "Maximum time the producer will wait for space when sending a message.", + direction = ConnectorAttribute.Direction.OUTGOING, + type = "long") +@ConnectorAttribute(name = "producer.time-to-deliver", + description = "Delay before sent message is made visible on its target destination.", + direction = ConnectorAttribute.Direction.OUTGOING, + type = "long") +public class WeblogicConnector extends JmsConnector { + private static final System.Logger LOGGER = System.getLogger(WeblogicConnector.class.getName()); + static final String JMS_FACTORY_ATTRIBUTE = "jms-factory"; + static final String THIN_CLIENT_PATH = "thin-jar"; + static final String WLS_URL = "url"; + static final String WLS_INIT_CONTEXT_PRINCIPAL = "principal"; + static final String WLS_INIT_CONTEXT_CREDENTIAL = "credentials"; + /** + * Microprofile messaging Weblogic JMS connector name. + */ + public static final String CONNECTOR_NAME = "helidon-weblogic-jms"; + + @Inject + protected WeblogicConnector(Config config, + Instance connectionFactories) { + super(config, connectionFactories); + config.get("mp.messaging.connector.helidon-weblogic-jms.thin-jar") + .asString() + .ifPresent(ThinClientClassLoader::setThinJarLocation); + } + + protected WeblogicConnector(Map connectionFactoryMap, + ScheduledExecutorService scheduler, + String thinJarLocation, + ExecutorService executor) { + super(connectionFactoryMap, scheduler, executor); + ThinClientClassLoader.setThinJarLocation(thinJarLocation); + } + + @Override + public PublisherBuilder> getPublisherBuilder(org.eclipse.microprofile.config.Config mpConfig) { + return super.getPublisherBuilder(WlsConnectorConfigAliases.map(mpConfig)); + } + + @Override + public SubscriberBuilder, Void> getSubscriberBuilder(org.eclipse.microprofile.config.Config mpConfig) { + return super.getSubscriberBuilder(WlsConnectorConfigAliases.map(mpConfig)); + } + + @Override + protected MessageConsumer createConsumer(Config config, + Destination destination, + SessionMetadata sessionEntry) throws JMSException { + return executeInIsolation(() -> super.createConsumer(config, destination, sessionEntry)); + } + + @Override + protected Optional getFactory(ConnectionContext ctx) { + return executeInIsolation(() -> super.getFactory(ctx)); + } + + @Override + protected Destination createDestination(Session session, ConnectionContext ctx) { + return executeInIsolation(() -> super.createDestination(session, ctx)); + } + + @Override + protected SessionMetadata prepareSession(Config config, ConnectionFactory factory) throws JMSException { + return executeInIsolation(() -> super.prepareSession(config, factory)); + } + + @Override + protected CompletionStage consumeAsync(Message m, + Session session, + AtomicReference mapper, + MessageProducer producer, + Config config) { + return executeInIsolation(() -> super.consumeAsync(m, session, mapper, producer, config)); + } +} diff --git a/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WlsConnectorConfigAliases.java b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WlsConnectorConfigAliases.java new file mode 100644 index 00000000000..0e4a2487c53 --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/WlsConnectorConfigAliases.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging.connectors.wls; + +import java.util.HashMap; +import java.util.Map; + +import io.helidon.config.ConfigSources; +import io.helidon.config.mp.MpConfig; +import io.helidon.config.mp.MpConfigSources; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; + +class WlsConnectorConfigAliases { + + private WlsConnectorConfigAliases() { + } + + private static final Map ALIASES = Map.of( + WeblogicConnector.JMS_FACTORY_ATTRIBUTE, "jndi.jms-factory", + "url", "jndi.env-properties.java.naming.provider.url", + "principal", "jndi.env-properties.java.naming.security.principal", + "credentials", "jndi.env-properties.java.naming.security.credentials" + ); + + static Config map(Config connConfig) { + Map mapped = new HashMap<>(); + + mapped.put("jndi.env-properties.java.naming.factory.initial", IsolatedContextFactory.class.getName()); + + ALIASES.forEach((key, value) -> connConfig.getOptionalValue(key, String.class) + .ifPresent(s -> mapped.put(value, s))); + + io.helidon.config.Config cfg = io.helidon.config.Config.builder() + .addSource(ConfigSources.create(MpConfig.toHelidonConfig(connConfig))) + .disableEnvironmentVariablesSource() + .disableSystemPropertiesSource() + .disableParserServices() + .disableCaching() + .disableValueResolving() + .build(); + + return ConfigProviderResolver.instance() + .getBuilder() + .withSources(MpConfigSources.create(mapped), MpConfigSources.create(cfg)) + .build(); + } +} diff --git a/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/package-info.java b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/package-info.java new file mode 100644 index 00000000000..4cde68441b1 --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/io/helidon/messaging/connectors/wls/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Microprofile messaging Weblogic JMS connector. + */ +package io.helidon.messaging.connectors.wls; diff --git a/messaging/connectors/wls-jms/src/main/java/module-info.java b/messaging/connectors/wls-jms/src/main/java/module-info.java new file mode 100644 index 00000000000..4e78fefff7f --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Microprofile messaging Weblogic JMS connector. + */ +module io.helidon.messaging.connectors.wls { + requires java.logging; + + requires static jakarta.cdi; + requires static jakarta.inject; + requires io.helidon.messaging.connectors.jms; + requires jakarta.jms.api; + requires java.naming; + requires microprofile.config.api; + requires io.helidon.config.mp; + + exports io.helidon.messaging.connectors.wls; +} diff --git a/messaging/connectors/wls-jms/src/main/resources/META-INF/beans.xml b/messaging/connectors/wls-jms/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..6d563a7f80c --- /dev/null +++ b/messaging/connectors/wls-jms/src/main/resources/META-INF/beans.xml @@ -0,0 +1,24 @@ + + + + diff --git a/messaging/messaging/src/main/java/io/helidon/messaging/NackHandler.java b/messaging/messaging/src/main/java/io/helidon/messaging/NackHandler.java new file mode 100644 index 00000000000..9e606daa19b --- /dev/null +++ b/messaging/messaging/src/main/java/io/helidon/messaging/NackHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging; + +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.eclipse.microprofile.reactive.messaging.Message; + +/** + * Nack handler for interface for messages connectors. + * + * @param Connector specific message type + */ +public interface NackHandler> { + + /** + * Error type causing the message to be sent to DLQ. + */ + String DLQ_ERROR_PROP = "dlq-error"; + /** + * Message from error causing DLQ redirection. + */ + String DLQ_ERROR_MSG_PROP = "dlq-error-msg"; + /** + * Original destination of this message. + */ + String DLQ_ORIG_TOPIC_PROP = "dlq-orig-destination"; + + /** + * Return nack function to be used by message when nacked. + * + * @param message owner message of the nack function + * @return nack function to be used by message when nacked + */ + Function> getNack(M message); +} diff --git a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractJmsTest.java b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractJmsTest.java index dcb290b80c6..bca413f1718 100644 --- a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractJmsTest.java +++ b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractJmsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,13 +26,17 @@ import org.junit.jupiter.api.BeforeAll; public class AbstractJmsTest { + + static final String BROKER_URL = "vm://localhost?broker.persistent=false"; +// static final String BROKER_URL = "tcp://localhost:61616"; static Session session; static ConnectionFactory connectionFactory; @BeforeAll static void beforeAll() throws Exception { - connectionFactory = JakartaJms.create(new ActiveMQConnectionFactory("vm://localhost?broker.persistent=false")); + connectionFactory = JakartaJms.create(new ActiveMQConnectionFactory(BROKER_URL)); Connection connection = connectionFactory.createConnection(); + connection.start(); session = connection.createSession(false, AcknowledgeMode.AUTO_ACKNOWLEDGE.getAckMode()); } diff --git a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractSampleBean.java b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractSampleBean.java index 15fded35d28..439c4dbf231 100644 --- a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractSampleBean.java +++ b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AbstractSampleBean.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -117,7 +117,7 @@ public static class Channel1 extends AbstractSampleBean { @Incoming("test-channel-1") @Acknowledgment(Acknowledgment.Strategy.MANUAL) - public CompletionStage channel1(Message msg) { + public CompletionStage channel1(Message msg) { LOGGER.fine(() -> String.format("Received %s", msg.getPayload())); consumed().add(msg.getPayload()); msg.ack(); @@ -137,12 +137,12 @@ public Message channel2ToChannel3(Message msg) { return Message.of("Processed" + msg.getPayload()); } - @Incoming("test-channel-7") + @Incoming("test-channel-31") @Acknowledgment(Acknowledgment.Strategy.MANUAL) - public CompletionStage channel7(Message msg) { + public CompletionStage channel31(Message msg) { LOGGER.fine(() -> String.format("Received %s", msg.getPayload())); consumed().add(msg.getPayload()); - msg.ack().whenComplete((a, b) -> countDown("channel7()")); + msg.ack().whenComplete((a, b) -> countDown("channel31()")); return CompletableFuture.completedFuture(null); } } @@ -151,7 +151,7 @@ public CompletionStage channel7(Message msg) { public static class ChannelError extends AbstractSampleBean { @Incoming("test-channel-error") @Acknowledgment(Acknowledgment.Strategy.MANUAL) - public CompletionStage error(Message msg) { + public CompletionStage error(Message msg) { try { LOGGER.fine(() -> String.format("Received possible error %s", msg.getPayload())); consumed().add(Integer.toString(Integer.parseInt(msg.getPayload()))); @@ -168,7 +168,7 @@ public static class ChannelSelector extends AbstractSampleBean { @Incoming("test-channel-selector") @Acknowledgment(Acknowledgment.Strategy.MANUAL) - public CompletionStage selector(Message msg) { + public CompletionStage selector(Message msg) { LOGGER.fine(() -> String.format("Received %s", msg.getPayload())); consumed().add(msg.getPayload()); msg.ack(); @@ -218,38 +218,41 @@ public void onComplete() { public static class Channel5 extends AbstractSampleBean { @Incoming("test-channel-5") - public SubscriberBuilder, Void> channel5() { - LOGGER.fine(() -> "In channel5"); - return ReactiveStreams.>builder() - .to(new Subscriber>() { - @Override - public void onSubscribe(Subscription subscription) { - LOGGER.fine(() -> "channel5 onSubscribe()"); - subscription.request(3); - } + public void channel5(String msg) { + this.consumed().add(String.valueOf(Integer.parseInt(msg))); + countDown("channel5(String msg)"); + } + } - @Override - public void onNext(Message msg) { - consumed().add(Integer.toString(Integer.parseInt(msg.getPayload()))); - LOGGER.fine(() -> "Added " + msg.getPayload()); - msg.ack(); - countDown("onNext()"); - } + @ApplicationScoped + public static class Channel6 extends AbstractSampleBean { - @Override - public void onError(Throwable t) { - LOGGER.fine(() -> "Error " + t.getMessage() + ". Adding error in consumed() list"); - consumed().add("error"); - countDown("onError()"); - } + @Incoming("test-channel-6") + public void channel6(String msg) { + this.consumed().add(String.valueOf(Integer.parseInt(msg))); + countDown("channel6(String msg)"); + } + } - @Override - public void onComplete() { - consumed().add("complete"); - countDown("onComplete()"); - } - }); + @ApplicationScoped + public static class Channel7 extends AbstractSampleBean { + + @Incoming("test-channel-7") + @Outgoing("test-channel-71") + public Integer channel7(String msg) { + return Integer.parseInt(msg); } + + @Incoming("test-channel-71") + public SubscriberBuilder sink(){ + return ReactiveStreams.builder() + .map(String::valueOf) + .onErrorResume(t -> "error") + .peek(s -> this.consumed().add(s)) + .forEach(s -> countDown("channel7(String msg)")); + } + + } @ApplicationScoped diff --git a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AssertingHandler.java b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AssertingHandler.java new file mode 100644 index 00000000000..8692f29c5ce --- /dev/null +++ b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/AssertingHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.messaging.connectors.jms; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import org.hamcrest.Matchers; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class AssertingHandler extends Handler { + + private List recordList = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + recordList.add(record.getMessage()); + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } + + void assertLogMessageLogged(String message) { + assertThat(recordList, Matchers.hasItem(message)); + } +} diff --git a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/JmsMpTest.java b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/JmsMpTest.java index 9b340aa53f9..5e4fb017ffc 100644 --- a/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/JmsMpTest.java +++ b/tests/integration/jms/src/test/java/io/helidon/messaging/connectors/jms/JmsMpTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2022 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,11 @@ import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; +import java.util.logging.LogManager; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.stream.Stream; import io.helidon.microprofile.config.ConfigCdiExtension; import io.helidon.microprofile.messaging.MessagingCdiExtension; @@ -38,9 +41,13 @@ import jakarta.enterprise.inject.spi.CDI; import jakarta.jms.JMSException; import jakarta.jms.TextMessage; -import org.junit.jupiter.api.Disabled; + +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + @HelidonTest @DisableDiscovery @AddBeans({ @@ -48,6 +55,8 @@ @AddBean(AbstractSampleBean.Channel1.class), @AddBean(AbstractSampleBean.Channel4.class), @AddBean(AbstractSampleBean.Channel5.class), + @AddBean(AbstractSampleBean.Channel6.class), + @AddBean(AbstractSampleBean.Channel7.class), @AddBean(AbstractSampleBean.ChannelSelector.class), @AddBean(AbstractSampleBean.ChannelError.class), @AddBean(AbstractSampleBean.ChannelProcessor.class), @@ -62,7 +71,7 @@ }) @AddConfigs({ @AddConfig(key = "mp.messaging.connector.helidon-jms.jndi.env-properties.java.naming.provider.url", - value = "vm://localhost?broker.persistent=false"), + value = AbstractJmsTest.BROKER_URL), @AddConfig(key = "mp.messaging.connector.helidon-jms.jndi.env-properties.java.naming.factory.initial", value = "org.apache.activemq.jndi.ActiveMQInitialContextFactory"), @@ -78,9 +87,9 @@ @AddConfig(key = "mp.messaging.outgoing.test-channel-3.type", value = "topic"), @AddConfig(key = "mp.messaging.outgoing.test-channel-3.destination", value = JmsMpTest.TEST_TOPIC_3), - @AddConfig(key = "mp.messaging.incoming.test-channel-7.connector", value = JmsConnector.CONNECTOR_NAME), - @AddConfig(key = "mp.messaging.incoming.test-channel-7.type", value = "topic"), - @AddConfig(key = "mp.messaging.incoming.test-channel-7.destination", value = JmsMpTest.TEST_TOPIC_3), + @AddConfig(key = "mp.messaging.incoming.test-channel-31.connector", value = JmsConnector.CONNECTOR_NAME), + @AddConfig(key = "mp.messaging.incoming.test-channel-31.type", value = "topic"), + @AddConfig(key = "mp.messaging.incoming.test-channel-31.destination", value = JmsMpTest.TEST_TOPIC_3), @AddConfig(key = "mp.messaging.incoming.test-channel-error.connector", value = JmsConnector.CONNECTOR_NAME), @AddConfig(key = "mp.messaging.incoming.test-channel-error.type", value = "topic"), @@ -91,14 +100,24 @@ @AddConfig(key = "mp.messaging.incoming.test-channel-4.destination", value = JmsMpTest.TEST_TOPIC_4), @AddConfig(key = "mp.messaging.incoming.test-channel-5.connector", value = JmsConnector.CONNECTOR_NAME), - @AddConfig(key = "mp.messaging.incoming.test-channel-5.type", value = "topic"), - @AddConfig(key = "mp.messaging.incoming.test-channel-5.destination", value = JmsMpTest.TEST_TOPIC_5), + @AddConfig(key = "mp.messaging.incoming.test-channel-5.type", value = "queue"), + @AddConfig(key = "mp.messaging.incoming.test-channel-5.destination", value = JmsMpTest.TEST_QUEUE_5), + @AddConfig(key = "mp.messaging.incoming.test-channel-5.nack-dlq", value = JmsMpTest.DLQ_QUEUE), + + @AddConfig(key = "mp.messaging.incoming.test-channel-6.connector", value = JmsConnector.CONNECTOR_NAME), + @AddConfig(key = "mp.messaging.incoming.test-channel-6.type", value = "queue"), + @AddConfig(key = "mp.messaging.incoming.test-channel-6.destination", value = JmsMpTest.TEST_QUEUE_6), + @AddConfig(key = "mp.messaging.incoming.test-channel-6.nack-log-only", value = "true"), + + @AddConfig(key = "mp.messaging.incoming.test-channel-7.connector", value = JmsConnector.CONNECTOR_NAME), + @AddConfig(key = "mp.messaging.incoming.test-channel-7.type", value = "queue"), + @AddConfig(key = "mp.messaging.incoming.test-channel-7.destination", value = JmsMpTest.TEST_QUEUE_7), @AddConfig(key = "mp.messaging.incoming.test-channel-selector.connector", value = JmsConnector.CONNECTOR_NAME), @AddConfig(key = "mp.messaging.incoming.test-channel-selector.message-selector", value = "source IN ('helidon','voyager')"), @AddConfig(key = "mp.messaging.incoming.test-channel-selector.type", value = "topic"), - @AddConfig(key = "mp.messaging.incoming.test-channel-selector.destination", value = JmsMpTest.TEST_TOPIC_6), + @AddConfig(key = "mp.messaging.incoming.test-channel-selector.destination", value = JmsMpTest.TEST_TOPIC_SELECTOR), @AddConfig(key = "mp.messaging.incoming.test-channel-bytes-fromJms.connector", value = JmsConnector.CONNECTOR_NAME), @AddConfig(key = "mp.messaging.incoming.test-channel-bytes-fromJms.type", value = "queue"), @@ -140,21 +159,23 @@ @AddConfig(key = "mp.messaging.outgoing.test-channel-derived-msg-toJms.type", value = "queue"), @AddConfig(key = "mp.messaging.outgoing.test-channel-derived-msg-toJms.destination", value = JmsMpTest.TEST_TOPIC_DERIVED_2), }) -@Disabled("3.0.0-JAKARTA") class JmsMpTest extends AbstractMPTest { static final String TEST_TOPIC_1 = "topic-1"; static final String TEST_TOPIC_2 = "topic-2"; static final String TEST_TOPIC_3 = "topic-3"; static final String TEST_TOPIC_4 = "topic-4"; - static final String TEST_TOPIC_5 = "topic-5"; - static final String TEST_TOPIC_6 = "topic-6"; + static final String TEST_QUEUE_5 = "queue-5"; + static final String TEST_QUEUE_6 = "queue-6"; + static final String TEST_TOPIC_SELECTOR = "topic-selector"; + static final String TEST_QUEUE_7 = "queue-7"; static final String TEST_TOPIC_BYTES = "topic-bytes"; static final String TEST_TOPIC_PROPS = "topic-properties"; static final String TEST_TOPIC_CUST_MAPPER = "topic-cust-mapper"; static final String TEST_TOPIC_DERIVED_1 = "topic-derived-1"; static final String TEST_TOPIC_DERIVED_2 = "topic-derived-2"; static final String TEST_TOPIC_ERROR = "topic-error"; + static final String DLQ_QUEUE = "DLQ_TOPIC"; @Test void messageSelector() { @@ -166,7 +187,7 @@ void messageSelector() { "reliant"); //configured selector: source IN ('helidon','voyager') AbstractSampleBean bean = CDI.current().select(AbstractSampleBean.ChannelSelector.class).get(); - produceAndCheck(bean, testData, TEST_TOPIC_6, List.of("helidon", "voyager"), this::setSourceProperty); + produceAndCheck(bean, testData, TEST_TOPIC_SELECTOR, List.of("helidon", "voyager"), this::setSourceProperty); } private void setSourceProperty(TextMessage m) { @@ -217,13 +238,60 @@ void withBackPressure() { } @Test - void withBackPressureAndError() { + void withBackPressureAndNackKillChannel() { List testData = Arrays.asList("2222", "2222"); - AbstractSampleBean bean = CDI.current().select(AbstractSampleBean.Channel5.class).get(); - produceAndCheck(bean, testData, TEST_TOPIC_5, testData); + AbstractSampleBean bean = CDI.current().select(AbstractSampleBean.Channel7.class).get(); + produceAndCheck(bean, testData, TEST_QUEUE_7, testData); bean.restart(); - testData = Collections.singletonList("not a number"); - produceAndCheck(bean, testData, TEST_TOPIC_5, Collections.singletonList("error")); + produceAndCheck(bean, List.of("not a number"), TEST_QUEUE_7, Collections.singletonList("error")); + } + + @Test + void noAckDQL() throws JMSException { + List expected = List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + List testData = List.of("0", "1", "2", "3", "4", "5", "not a number!", "6", "7", "8", "9", "10"); + + + AbstractSampleBean.Channel5 channel5 = CDI.current().select(AbstractSampleBean.Channel5.class).get(); + produceAndCheck(channel5, testData, TEST_QUEUE_5, expected); + + List dlq = consumeAllCurrent(DLQ_QUEUE) + .map(TextMessage.class::cast) + .toList(); + + assertThat(dlq.stream() + .map(tm -> { + try { + return tm.getText(); + } catch (JMSException e) { + throw new RuntimeException(e); + } + }) + .toList(), Matchers.contains("not a number!")); + TextMessage textMessage = dlq.get(0); + assertThat(textMessage.getStringProperty("dlq-error"), is("java.lang.NumberFormatException")); + assertThat(textMessage.getStringProperty("dlq-error-msg"), is("For input string: \"not a number!\"")); + assertThat(textMessage.getStringProperty("dlq-orig-destination"), is(JmsMpTest.TEST_QUEUE_5)); + } + + @Test + void noAckLogOnly() throws JMSException { + Logger nackHandlerLogger = LogManager.getLogManager().getLogger(JmsNackHandler.class.getName()); + AssertingHandler assertingHandler = new AssertingHandler(); + nackHandlerLogger.addHandler(assertingHandler); + try { + List expected = List.of("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + List testData = expected.stream() + .flatMap(s -> "5".equals(s) ? Stream.of(s, "not a number!") : Stream.of(s)) + .toList(); + + + AbstractSampleBean.Channel6 channel6 = CDI.current().select(AbstractSampleBean.Channel6.class).get(); + produceAndCheck(channel6, testData, TEST_QUEUE_6, expected); + assertingHandler.assertLogMessageLogged("NACKED Message ignored\nchannel: test-channel-6\n"); + } finally { + nackHandlerLogger.removeHandler(assertingHandler); + } } @Test diff --git a/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaSeTest.java b/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaSeTest.java index 757347bc5aa..4d07d6fd69a 100644 --- a/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaSeTest.java +++ b/tests/integration/kafka/src/test/java/io/helidon/messaging/connectors/kafka/KafkaSeTest.java @@ -85,7 +85,7 @@ public class KafkaSeTest extends AbstractKafkaTest { private static final String TEST_SE_TOPIC_9 = "special-se-topic-9"; private static final String TEST_SE_TOPIC_PATTERN_34 = "special-se-topic-[3-4]"; - static Logger nackHandlerLogLogger = Logger.getLogger(NackHandler.Log.class.getName()); + static Logger nackHandlerLogLogger = Logger.getLogger(KafkaNackHandler.Log.class.getName()); private static final List logNackHandlerWarnings = new ArrayList<>(1); From fc4909968f54ba8d064ff5fe2d41ed385e95033f Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 5 Dec 2022 14:34:07 +0100 Subject: [PATCH 2/6] Parent version fix Signed-off-by: Daniel Kec --- examples/messaging/weblogic-jms-mp/pom.xml | 2 +- messaging/connectors/wls-jms/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/messaging/weblogic-jms-mp/pom.xml b/examples/messaging/weblogic-jms-mp/pom.xml index 1a3f5d92700..7b50359c23c 100644 --- a/examples/messaging/weblogic-jms-mp/pom.xml +++ b/examples/messaging/weblogic-jms-mp/pom.xml @@ -22,7 +22,7 @@ io.helidon.applications helidon-mp - 3.0.3-SNAPSHOT + 4.0.0-SNAPSHOT ../../../applications/mp/pom.xml io.helidon.examples.messaging.wls diff --git a/messaging/connectors/wls-jms/pom.xml b/messaging/connectors/wls-jms/pom.xml index 0f9dd9fa44d..a881be8c434 100644 --- a/messaging/connectors/wls-jms/pom.xml +++ b/messaging/connectors/wls-jms/pom.xml @@ -24,7 +24,7 @@ io.helidon.messaging helidon-messaging-connectors-project - 3.0.3-SNAPSHOT + 4.0.0-SNAPSHOT io.helidon.messaging.wls-jms From 84819fd9856c5fec5490d516fda301dbbdd20c88 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 5 Dec 2022 15:24:21 +0100 Subject: [PATCH 3/6] Fix console handler package Signed-off-by: Daniel Kec --- .../weblogic-jms-mp/src/main/resources/logging.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties index a719fd95607..d048f7def6f 100644 --- a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties @@ -18,7 +18,7 @@ # For more information see $JAVA_HOME/jre/lib/logging.properties # Send messages to the console -handlers=io.helidon.common.HelidonConsoleHandler +handlers=io.helidon.logging.jul.HelidonConsoleHandler # HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n From 56902dbbf67c78fda9f464e35f2caaf88b95ad5d Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 5 Dec 2022 15:25:20 +0100 Subject: [PATCH 4/6] Example fix Signed-off-by: Daniel Kec --- examples/messaging/weblogic-jms-mp/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/messaging/weblogic-jms-mp/README.md b/examples/messaging/weblogic-jms-mp/README.md index 21023110bf4..56414c125ef 100644 --- a/examples/messaging/weblogic-jms-mp/README.md +++ b/examples/messaging/weblogic-jms-mp/README.md @@ -19,6 +19,6 @@ To run Helidon with thin client, flag `--add-opens=java.base/java.io=ALL-UNNAMED` is needed to open java.base module to thin client internals. 1. `mvn clean package` -2. `java --add-opens=java.base/java.io=ALL-UNNAMED -jar ./target/weblogic-jms-mp.jar` +2. `java --add-opens=java.base/java.io=ALL-UNNAMED --enable-preview -jar ./target/weblogic-jms-mp.jar` 3. Visit http://localhost:8080 and try to send and receive messages over Weblogic JMS queue. From 6f8a1a984c106a623cce0534ec6fc45a1988cc7f Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Wed, 7 Dec 2022 13:34:52 +0100 Subject: [PATCH 5/6] Review issues Signed-off-by: Daniel Kec --- .../src/main/resources/application.yaml | 12 +++++++----- .../src/main/resources/logging.properties | 7 +------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml index 491a791cf5d..28a147e57a5 100644 --- a/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/application.yaml @@ -14,11 +14,13 @@ # limitations under the License. # -server.port: 8080 -server.host: 0.0.0.0 - -server.static.classpath.location: /WEB -server.static.classpath.welcome: index.html +server: + port: 8080 + host: 0.0.0.0 + static: + classpath: + location: /WEB + welcome: index.html mp: messaging: diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties index d048f7def6f..506dc774bb7 100644 --- a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties @@ -27,9 +27,4 @@ java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$ .level=INFO # Component specific log levels -#io.helidon.webserver.level=INFO -#io.helidon.config.level=INFO -#io.helidon.security.level=INFO -#io.helidon.common.level=INFO -#io.netty.level=INFO -#io.helidon.messaging.connectors.wls.level=INFO +#io.helidon.level=INFO \ No newline at end of file From 609c0b0cf57fe48730c2e6300fd5a371c31627cd Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Wed, 7 Dec 2022 13:45:03 +0100 Subject: [PATCH 6/6] Review issues Signed-off-by: Daniel Kec --- .../weblogic-jms-mp/src/main/resources/logging.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties index 506dc774bb7..1ac57eb5b92 100644 --- a/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties +++ b/examples/messaging/weblogic-jms-mp/src/main/resources/logging.properties @@ -27,4 +27,4 @@ java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$ .level=INFO # Component specific log levels -#io.helidon.level=INFO \ No newline at end of file +#io.helidon.level=INFO