Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Comment thread
jonbartels marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
/*
* Copyright (c) Mirth Corporation. All rights reserved.
*
* http://www.mirthcorp.com
*
* The software in this package is published under the terms of the MPL license a copy of which has
* been included with this distribution in the LICENSE.txt file.
*/
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Mirth Corporation
// SPDX-FileCopyrightText: Open Integration Engine Contributors

package com.mirth.connect.connectors.ws;

Expand All @@ -21,6 +16,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;
Expand All @@ -34,6 +30,7 @@
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.mirth.connect.donkey.model.channel.ConnectorPluginProperties;
import com.mirth.connect.donkey.model.event.ConnectionStatusEventType;
import com.mirth.connect.donkey.model.event.ErrorEventType;
Expand Down Expand Up @@ -163,7 +160,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();

Expand Down Expand Up @@ -229,6 +226,23 @@ 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.
*/
@VisibleForTesting
static ExecutorService createExecutor(int threads, ClassLoader contextClassLoader) {
Comment thread
jonbartels marked this conversation as resolved.
Resolved
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;
Expand Down
Comment thread
jonbartels marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Open Integration Engine Contributors

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 {
try (URLClassLoader expected = new URLClassLoader(new URL[0], getClass().getClassLoader())) {
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 Contributors

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());
}
}
191 changes: 191 additions & 0 deletions smoketest/src/test/resources/web-service-listener/channel.xml
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 &quot;Postprocessor&quot; 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>
Loading