Skip to content
Draft
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

Copy link
Copy Markdown
Member

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.

// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: Mirth Corporation
// SPDX-FileCopyrightText: Open Integration Engine Contributors <or Jon Bartels>

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): Import and annotate this method with com.google.common.annotations.VisibleForTesting.

This is from Guava. It's already on the classpath and also used in JavaScriptBuilder. It lets static analysis tools know that this method should be treated as private except for test class access.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file needs a SPDX header. See note on WebServiceListenerTest.java‎.

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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please close the URLClassLoader, for example with try-with-resources.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// SPDX-FileCopyrightText: Open Integration Engine
// SPDX-FileCopyrightText: Open Integration Engine Contributors

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());
}
}
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