Repository navigation
Fix Web Service Listener SOAP requests failing on JDK 19+ #508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ | |
| import java.util.Map.Entry; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.ThreadFactory; | ||
|
|
||
| import javax.servlet.http.HttpServletResponse; | ||
| import javax.xml.ws.Binding; | ||
|
|
@@ -163,7 +164,7 @@ public void onStart() throws ConnectorTaskException { | |
| } | ||
|
|
||
| // Allow more than the channel processing threads so WDSL requests can be accepted even if all processing threads are busy | ||
| executor = Executors.newFixedThreadPool(processingThreads + 4); | ||
| executor = createExecutor(processingThreads + 4, WebServiceReceiver.class.getClassLoader()); | ||
| server.setExecutor(executor); | ||
| server.start(); | ||
|
|
||
|
|
@@ -229,6 +230,22 @@ public void onStart() throws ConnectorTaskException { | |
| eventController.dispatchEvent(new ConnectionStatusEvent(getChannelId(), getMetaDataId(), getSourceName(), ConnectionStatusEventType.IDLE)); | ||
| } | ||
|
|
||
| /* | ||
| * The HttpServer creates request worker threads from its dispatcher thread, so they inherit the | ||
| * dispatcher's context classloader. Since JDK 19 that is the system classloader, which cannot | ||
| * see server-lib, and lookups through the context classloader (e.g. the SAAJ implementation) | ||
| * fail. Give the worker threads the given classloader explicitly instead. | ||
| */ | ||
| static ExecutorService createExecutor(int threads, ClassLoader contextClassLoader) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (non-blocking): Import and annotate this method with This is from Guava. It's already on the classpath and also used in |
||
| ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory(); | ||
|
|
||
| return Executors.newFixedThreadPool(threads, runnable -> { | ||
| Thread thread = defaultThreadFactory.newThread(runnable); | ||
| thread.setContextClassLoader(contextClassLoader); | ||
| return thread; | ||
| }); | ||
| } | ||
|
|
||
| @Override | ||
| public void onStop() throws ConnectorTaskException { | ||
| ConnectorTaskException firstCause = null; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file needs a SPDX header. See note on |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.mirth.connect.connectors.ws; | ||
|
|
||
| import static org.junit.Assert.assertSame; | ||
|
|
||
| import java.net.HttpURLConnection; | ||
| import java.net.InetSocketAddress; | ||
| import java.net.URI; | ||
| import java.net.URL; | ||
| import java.net.URLClassLoader; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| import com.sun.net.httpserver.HttpServer; | ||
|
|
||
| public class WebServiceReceiverTest { | ||
|
|
||
| @Test | ||
| public void testWorkerThreadsUseGivenContextClassLoader() throws Exception { | ||
| ClassLoader expected = new URLClassLoader(new URL[0], getClass().getClassLoader()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please close the |
||
| CompletableFuture<ClassLoader> actual = new CompletableFuture<>(); | ||
|
|
||
| HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); | ||
| ExecutorService executor = WebServiceReceiver.createExecutor(1, expected); | ||
| server.setExecutor(executor); | ||
| server.createContext("/", exchange -> { | ||
| actual.complete(Thread.currentThread().getContextClassLoader()); | ||
| exchange.sendResponseHeaders(204, -1); | ||
| exchange.close(); | ||
| }); | ||
| server.start(); | ||
|
|
||
| try { | ||
| HttpURLConnection connection = (HttpURLConnection) URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/").toURL().openConnection(); | ||
| connection.getResponseCode(); | ||
| connection.disconnect(); | ||
|
|
||
| assertSame(expected, actual.get(10, TimeUnit.SECONDS)); | ||
| } finally { | ||
| server.stop(0); | ||
| executor.shutdown(); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||
| // SPDX-License-Identifier: MPL-2.0 | ||||||
| // SPDX-FileCopyrightText: Open Integration Engine | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This is what you should use for a generic copyright text. The human authors hold the copyright, not the project. You can also use your own name for a new file if you choose to. |
||||||
|
|
||||||
| package org.openintegrationengine.smoketest; | ||||||
|
|
||||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||||||
|
|
||||||
| import java.net.URI; | ||||||
| import java.net.http.HttpClient; | ||||||
| import java.net.http.HttpRequest; | ||||||
| import java.net.http.HttpResponse; | ||||||
| import java.time.Duration; | ||||||
|
|
||||||
| import org.junit.jupiter.api.AfterAll; | ||||||
| import org.junit.jupiter.api.BeforeAll; | ||||||
| import org.junit.jupiter.api.DisplayName; | ||||||
| import org.junit.jupiter.api.Test; | ||||||
|
|
||||||
| /** | ||||||
| * Sends a SOAP request to a deployed Web Service Listener. Messages submitted through the client | ||||||
| * API never reach the listener's HTTP worker threads, which is where SAAJ is resolved, so the | ||||||
| * fixture tests cannot cover this. | ||||||
| */ | ||||||
| @DisplayName("Web Service Listener") | ||||||
| class WebServiceListenerTest { | ||||||
|
|
||||||
| /** The listener port and service name in web-service-listener/channel.xml. */ | ||||||
| private static final int PORT = 8081; | ||||||
| private static final String SERVICE = "Mirth"; | ||||||
|
|
||||||
| private static final String ENVELOPE = """ | ||||||
| <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ws="http://ws.connectors.connect.mirth.com/"> | ||||||
| <soapenv:Body> | ||||||
| <ws:acceptMessage> | ||||||
| <arg0>smoke test</arg0> | ||||||
| </ws:acceptMessage> | ||||||
| </soapenv:Body> | ||||||
| </soapenv:Envelope> | ||||||
| """; | ||||||
|
|
||||||
| private static String channelId; | ||||||
|
|
||||||
| @BeforeAll | ||||||
| static void deploy() throws Exception { | ||||||
| channelId = Harness.deploy("web-service-listener/channel.xml"); | ||||||
| } | ||||||
|
|
||||||
| @AfterAll | ||||||
| static void undeploy() { | ||||||
| Harness.undeploy(channelId); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * On JDK 19 and later the listener's worker threads lost the server classloader, so every | ||||||
| * request failed with "Unable to create SAAJ meta-factory" while the WSDL still served fine. | ||||||
| */ | ||||||
| @Test | ||||||
| @DisplayName("accepts a SOAP request") | ||||||
| void acceptsSoapRequest() throws Exception { | ||||||
| URI endpoint = new URI("http", null, URI.create(HarnessConfig.BASE_URL).getHost(), PORT, | ||||||
| "/services/" + SERVICE, null, null); | ||||||
|
|
||||||
| HttpRequest request = HttpRequest.newBuilder(endpoint) | ||||||
| .timeout(Duration.ofMillis(HarnessConfig.REQUEST_TIMEOUT_MILLIS)) | ||||||
| .header("Content-Type", "text/xml; charset=utf-8") | ||||||
| .header("SOAPAction", "\"\"") | ||||||
| .POST(HttpRequest.BodyPublishers.ofString(ENVELOPE)) | ||||||
| .build(); | ||||||
|
|
||||||
| HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); | ||||||
|
|
||||||
| assertEquals(200, response.statusCode(), () -> "status of POST " + endpoint + ": " + response.body()); | ||||||
| assertTrue(response.body().contains("acceptMessageResponse"), () -> "response body: " + response.body()); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,191 @@ | ||
| <channel version="4.5.2"> | ||
| <id>f7ed972e-6845-45bf-92bc-0302d6b05204</id> | ||
| <nextMetaDataId>2</nextMetaDataId> | ||
| <name>Web Service Listener</name> | ||
| <description></description> | ||
| <revision>1</revision> | ||
| <sourceConnector version="4.5.2"> | ||
| <metaDataId>0</metaDataId> | ||
| <name>sourceConnector</name> | ||
| <properties class="com.mirth.connect.connectors.ws.WebServiceReceiverProperties" version="4.5.2"> | ||
| <pluginProperties/> | ||
| <listenerConnectorProperties version="4.5.2"> | ||
| <host>0.0.0.0</host> | ||
| <port>8081</port> | ||
| </listenerConnectorProperties> | ||
| <sourceConnectorProperties version="4.5.2"> | ||
| <responseVariable>None</responseVariable> | ||
| <respondAfterProcessing>true</respondAfterProcessing> | ||
| <processBatch>false</processBatch> | ||
| <firstResponse>false</firstResponse> | ||
| <processingThreads>1</processingThreads> | ||
| <resourceIds class="linked-hash-map"> | ||
| <entry> | ||
| <string>Default Resource</string> | ||
| <string>[Default Resource]</string> | ||
| </entry> | ||
| </resourceIds> | ||
| <queueBufferSize>1000</queueBufferSize> | ||
| </sourceConnectorProperties> | ||
| <className>com.mirth.connect.connectors.ws.DefaultAcceptMessage</className> | ||
| <serviceName>Mirth</serviceName> | ||
| <soapBinding>DEFAULT</soapBinding> | ||
| </properties> | ||
| <transformer version="4.5.2"> | ||
| <elements/> | ||
| <inboundDataType>RAW</inboundDataType> | ||
| <outboundDataType>RAW</outboundDataType> | ||
| <inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </inboundProperties> | ||
| <outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </outboundProperties> | ||
| </transformer> | ||
| <filter version="4.5.2"> | ||
| <elements/> | ||
| </filter> | ||
| <transportName>Web Service Listener</transportName> | ||
| <mode>SOURCE</mode> | ||
| <enabled>true</enabled> | ||
| <waitForPrevious>true</waitForPrevious> | ||
| </sourceConnector> | ||
| <destinationConnectors> | ||
| <connector version="4.5.2"> | ||
| <metaDataId>1</metaDataId> | ||
| <name>Destination 1</name> | ||
| <properties class="com.mirth.connect.connectors.vm.VmDispatcherProperties" version="4.5.2"> | ||
| <pluginProperties/> | ||
| <destinationConnectorProperties version="4.5.2"> | ||
| <queueEnabled>false</queueEnabled> | ||
| <sendFirst>false</sendFirst> | ||
| <retryIntervalMillis>10000</retryIntervalMillis> | ||
| <regenerateTemplate>false</regenerateTemplate> | ||
| <retryCount>0</retryCount> | ||
| <rotate>false</rotate> | ||
| <includeFilterTransformer>false</includeFilterTransformer> | ||
| <threadCount>1</threadCount> | ||
| <threadAssignmentVariable></threadAssignmentVariable> | ||
| <validateResponse>false</validateResponse> | ||
| <resourceIds class="linked-hash-map"> | ||
| <entry> | ||
| <string>Default Resource</string> | ||
| <string>[Default Resource]</string> | ||
| </entry> | ||
| </resourceIds> | ||
| <queueBufferSize>1000</queueBufferSize> | ||
| <reattachAttachments>true</reattachAttachments> | ||
| </destinationConnectorProperties> | ||
| <channelId>none</channelId> | ||
| <channelTemplate>${message.encodedData}</channelTemplate> | ||
| <mapVariables/> | ||
| </properties> | ||
| <transformer version="4.5.2"> | ||
| <elements/> | ||
| <inboundDataType>RAW</inboundDataType> | ||
| <outboundDataType>RAW</outboundDataType> | ||
| <inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </inboundProperties> | ||
| <outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </outboundProperties> | ||
| </transformer> | ||
| <responseTransformer version="4.5.2"> | ||
| <elements/> | ||
| <inboundDataType>RAW</inboundDataType> | ||
| <outboundDataType>RAW</outboundDataType> | ||
| <inboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </inboundProperties> | ||
| <outboundProperties class="com.mirth.connect.plugins.datatypes.raw.RawDataTypeProperties" version="4.5.2"> | ||
| <batchProperties class="com.mirth.connect.plugins.datatypes.raw.RawBatchProperties" version="4.5.2"> | ||
| <splitType>JavaScript</splitType> | ||
| <batchScript></batchScript> | ||
| </batchProperties> | ||
| </outboundProperties> | ||
| </responseTransformer> | ||
| <filter version="4.5.2"> | ||
| <elements/> | ||
| </filter> | ||
| <transportName>Channel Writer</transportName> | ||
| <mode>DESTINATION</mode> | ||
| <enabled>true</enabled> | ||
| <waitForPrevious>true</waitForPrevious> | ||
| </connector> | ||
| </destinationConnectors> | ||
| <preprocessingScript>// Modify the message variable below to pre process data | ||
| return message;</preprocessingScript> | ||
| <postprocessingScript>// This script executes once after a message has been processed | ||
| // Responses returned from here will be stored as "Postprocessor" in the response map | ||
| return;</postprocessingScript> | ||
| <deployScript>// This script executes once when the channel is deployed | ||
| // You only have access to the globalMap and globalChannelMap here to persist data | ||
| return;</deployScript> | ||
| <undeployScript>// This script executes once when the channel is undeployed | ||
| // You only have access to the globalMap and globalChannelMap here to persist data | ||
| return;</undeployScript> | ||
| <properties version="4.5.2"> | ||
| <clearGlobalChannelMap>true</clearGlobalChannelMap> | ||
| <messageStorageMode>DEVELOPMENT</messageStorageMode> | ||
| <encryptData>false</encryptData> | ||
| <encryptAttachments>false</encryptAttachments> | ||
| <encryptCustomMetaData>false</encryptCustomMetaData> | ||
| <removeContentOnCompletion>false</removeContentOnCompletion> | ||
| <removeOnlyFilteredOnCompletion>false</removeOnlyFilteredOnCompletion> | ||
| <removeAttachmentsOnCompletion>false</removeAttachmentsOnCompletion> | ||
| <initialState>STARTED</initialState> | ||
| <storeAttachments>true</storeAttachments> | ||
| <metaDataColumns> | ||
| <metaDataColumn> | ||
| <name>SOURCE</name> | ||
| <type>STRING</type> | ||
| <mappingName>mirth_source</mappingName> | ||
| </metaDataColumn> | ||
| <metaDataColumn> | ||
| <name>TYPE</name> | ||
| <type>STRING</type> | ||
| <mappingName>mirth_type</mappingName> | ||
| </metaDataColumn> | ||
| </metaDataColumns> | ||
| <attachmentProperties version="4.5.2"> | ||
| <type>None</type> | ||
| <properties/> | ||
| </attachmentProperties> | ||
| <resourceIds class="linked-hash-map"> | ||
| <entry> | ||
| <string>Default Resource</string> | ||
| <string>[Default Resource]</string> | ||
| </entry> | ||
| </resourceIds> | ||
| </properties> | ||
| <exportData> | ||
| <metadata> | ||
| <enabled>true</enabled> | ||
| <lastModified> | ||
| <time>1775955953073</time> | ||
| <timezone>America/Chicago</timezone> | ||
| </lastModified> | ||
| <pruningSettings> | ||
| <archiveEnabled>true</archiveEnabled> | ||
| <pruneErroredMessages>false</pruneErroredMessages> | ||
| </pruningSettings> | ||
| <userId>1</userId> | ||
| </metadata> | ||
| </exportData> | ||
| </channel> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please convert the header on this file to SPDX? If this was a derivative work (i.e., you told your AI to consider the BridgeLink implementation before suggesting changes) then make sure to include a copyright line after Mirth from the source file.