Skip to content

Commit 5c58b58

Browse files
authored
Support IBM MQ for Python JmsIO (#39467)
* Introduce a BeamGenericJmsConnectionFactory interface to support different Jms JmsConnectionFactory cross-lang * Move ConnectionConfiguration outside of JmsIO class * Add test case for IBM MQ
1 parent 42c693f commit 5c58b58

10 files changed

Lines changed: 692 additions & 212 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"modification": 5
3+
"modification": 6
44
}

sdks/java/io/jms/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ dependencies {
3333
implementation library.java.slf4j_api
3434
implementation library.java.joda_time
3535
implementation "org.apache.geronimo.specs:geronimo-jms_2.0_spec:1.0-alpha-2"
36+
// Don't put proprietary licensed ibm mq client into runtimeClasspath
37+
// (affects expansion service shadow jar)
38+
compileOnly("com.ibm.mq:com.ibm.mq.allclient:9.3.0.25") {
39+
// duplicating geronimo-jms_2.0_spec
40+
exclude group: "javax.jms", module: "javax.jms-api"
41+
}
3642
testImplementation library.java.activemq_amqp
3743
testImplementation library.java.activemq_broker
3844
testImplementation library.java.activemq_jaas
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.io.jms;
19+
20+
import java.io.Serializable;
21+
import javax.jms.ConnectionFactory;
22+
23+
/**
24+
* An interface for creating custom JMS {@link ConnectionFactory} instances.
25+
*
26+
* <p>Expansion service users connecting to JMS brokers other than the built-in supported ones
27+
* (ActiveMQ, Qpid, IBM MQ) can implement this interface and specify their implementation class name
28+
* in {@link ConnectionConfiguration}.
29+
*
30+
* <p>The implementation must have a public default constructor.
31+
*/
32+
@FunctionalInterface
33+
public interface BeamGenericJmsConnectionFactory extends Serializable {
34+
35+
/**
36+
* Creates a {@link ConnectionFactory} using the given {@link ConnectionConfiguration}.
37+
*
38+
* @param config the JMS connection configuration
39+
* @return configured JMS {@link ConnectionFactory}
40+
*/
41+
ConnectionFactory createConnectionFactory(ConnectionConfiguration config) throws Exception;
42+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.io.jms;
19+
20+
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
21+
22+
import com.google.auto.value.AutoValue;
23+
import com.ibm.mq.jms.MQConnectionFactory;
24+
import com.ibm.msg.client.wmq.WMQConstants;
25+
import java.io.Serializable;
26+
import java.lang.reflect.InvocationTargetException;
27+
import java.lang.reflect.Method;
28+
import java.net.URI;
29+
import java.util.List;
30+
import javax.jms.ConnectionFactory;
31+
import org.apache.beam.sdk.schemas.AutoValueSchema;
32+
import org.apache.beam.sdk.schemas.annotations.DefaultSchema;
33+
import org.apache.beam.sdk.schemas.annotations.SchemaFieldDescription;
34+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Splitter;
35+
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Strings;
36+
import org.checkerframework.checker.nullness.qual.Nullable;
37+
import org.slf4j.Logger;
38+
import org.slf4j.LoggerFactory;
39+
40+
/** A POJO describing a JMS connection, used by SchemaTransformProvider. */
41+
@DefaultSchema(AutoValueSchema.class)
42+
@AutoValue
43+
public abstract class ConnectionConfiguration implements Serializable {
44+
private static final Logger LOG = LoggerFactory.getLogger(ConnectionConfiguration.class);
45+
46+
public static Builder builder() {
47+
return new AutoValue_ConnectionConfiguration.Builder();
48+
}
49+
50+
public static ConnectionConfiguration create(
51+
String serverUri, @Nullable String connectionFactoryClassName) {
52+
checkArgument(serverUri != null, "serverUri can not be null");
53+
return builder()
54+
.setServerUri(serverUri)
55+
.setConnectionFactoryClassName(connectionFactoryClassName)
56+
.build();
57+
}
58+
59+
public static ConnectionConfiguration create(String serverUri) {
60+
return create(serverUri, null);
61+
}
62+
63+
@SchemaFieldDescription("The JMS broker URI.")
64+
public abstract String getServerUri();
65+
66+
@SchemaFieldDescription("The JMS ConnectionFactory class name.")
67+
public abstract @Nullable String getConnectionFactoryClassName();
68+
69+
@SchemaFieldDescription("The username to connect to the JMS broker.")
70+
public abstract @Nullable String getUsername();
71+
72+
@SchemaFieldDescription("The password to connect to the JMS broker.")
73+
public abstract @Nullable String getPassword();
74+
75+
public ConnectionConfiguration withUsername(String username) {
76+
return toBuilder().setUsername(username).build();
77+
}
78+
79+
public ConnectionConfiguration withPassword(String password) {
80+
return toBuilder().setPassword(password).build();
81+
}
82+
83+
public ConnectionConfiguration withConnectionFactoryClassName(String connectionFactoryClassName) {
84+
return toBuilder().setConnectionFactoryClassName(connectionFactoryClassName).build();
85+
}
86+
87+
abstract Builder toBuilder();
88+
89+
@AutoValue.Builder
90+
public abstract static class Builder {
91+
public abstract Builder setServerUri(String serverUri);
92+
93+
public abstract Builder setConnectionFactoryClassName(
94+
@Nullable String connectionFactoryClassName);
95+
96+
public abstract Builder setUsername(@Nullable String username);
97+
98+
public abstract Builder setPassword(@Nullable String password);
99+
100+
public abstract ConnectionConfiguration build();
101+
}
102+
103+
public ConnectionFactory createConnectionFactory() {
104+
String className = getConnectionFactoryClassName();
105+
// Default to ActiveMQ
106+
if (className == null || className.isEmpty()) {
107+
className = "org.apache.activemq.ActiveMQConnectionFactory";
108+
}
109+
Class<?> clazz;
110+
Class<? extends BeamGenericJmsConnectionFactory> factoryClass;
111+
try {
112+
clazz = Class.forName(className);
113+
} catch (ClassNotFoundException e) {
114+
throw new IllegalArgumentException(
115+
String.format(
116+
"ConnectionFactory %s does not exist. If using expansion service, attach the connection factory jar as part of its invocation classpath.",
117+
className),
118+
e);
119+
}
120+
if (BeamGenericJmsConnectionFactory.class.isAssignableFrom(clazz)) {
121+
factoryClass = (Class<? extends BeamGenericJmsConnectionFactory>) clazz;
122+
} else if (className.contains("org.apache.activemq.ActiveMQConnectionFactory")
123+
|| className.contains("org.apache.qpid.jms")) {
124+
// Connectors supported by StandardJmsConnectionFactory
125+
factoryClass = StandardJmsConnectionFactory.class;
126+
} else if (className.contains("com.ibm.mq")) {
127+
factoryClass = IbmMqJmsConnectionFactory.class;
128+
} else {
129+
// Attempt to use StandardJmsConnectionFactory.class;
130+
factoryClass = StandardJmsConnectionFactory.class;
131+
}
132+
try {
133+
BeamGenericJmsConnectionFactory factory = factoryClass.getDeclaredConstructor().newInstance();
134+
return factory.createConnectionFactory(this);
135+
} catch (Exception e) {
136+
throw new IllegalArgumentException(
137+
"Unable to instantiate JMS ConnectionFactory of class "
138+
+ className
139+
+ ". Must be a supported provider (ActiveMQ, Qpid, IBM MQ) or implement BeamGenericJmsConnectionFactory.",
140+
e);
141+
}
142+
}
143+
144+
/**
145+
* A {@link BeamGenericJmsConnectionFactory} implementation for standard JMS connection factories.
146+
*/
147+
public static class StandardJmsConnectionFactory implements BeamGenericJmsConnectionFactory {
148+
149+
@Override
150+
public ConnectionFactory createConnectionFactory(ConnectionConfiguration config)
151+
throws Exception {
152+
String className = config.getConnectionFactoryClassName();
153+
if (className == null || className.isEmpty()) {
154+
className = "org.apache.activemq.ActiveMQConnectionFactory";
155+
}
156+
Class<?> clazz = Class.forName(className);
157+
String uri = config.getServerUri();
158+
String username = config.getUsername();
159+
String password = config.getPassword();
160+
161+
if (username != null && password != null) {
162+
try {
163+
return (ConnectionFactory)
164+
clazz
165+
.getConstructor(String.class, String.class, String.class)
166+
.newInstance(username, password, uri);
167+
} catch (NoSuchMethodException e) {
168+
// Fall through to 1-arg or 0-arg constructor + setters
169+
}
170+
}
171+
ConnectionFactory cf;
172+
try {
173+
cf = (ConnectionFactory) clazz.getConstructor(String.class).newInstance(uri);
174+
} catch (NoSuchMethodException e) {
175+
cf = (ConnectionFactory) clazz.getConstructor().newInstance();
176+
}
177+
178+
if (username != null && password != null) {
179+
boolean setUsernameSuccess =
180+
// ActiveMQ (capital N)
181+
invokeMethodIfExists(cf, "setUserName", String.class, username)
182+
// Qpid (lowercase n)
183+
|| invokeMethodIfExists(cf, "setUsername", String.class, username);
184+
boolean setPasswordSuccess =
185+
invokeMethodIfExists(cf, "setPassword", String.class, password);
186+
187+
if (!setUsernameSuccess || !setPasswordSuccess) {
188+
LOG.warn("Unable to set username/password on JMS ConnectionFactory of class {}", clazz);
189+
}
190+
}
191+
return cf;
192+
}
193+
194+
private static boolean invokeMethodIfExists(
195+
Object target, String methodName, Class<?> paramType, Object arg) {
196+
try {
197+
Method m = target.getClass().getMethod(methodName, paramType);
198+
m.invoke(target, arg);
199+
return true;
200+
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
201+
return false;
202+
}
203+
}
204+
}
205+
206+
/** A {@link BeamGenericJmsConnectionFactory} implementation for IBM MQ. */
207+
public static class IbmMqJmsConnectionFactory implements BeamGenericJmsConnectionFactory {
208+
209+
@Override
210+
public ConnectionFactory createConnectionFactory(ConnectionConfiguration config)
211+
throws Exception {
212+
MQConnectionFactory cf = new MQConnectionFactory();
213+
cf.setTransportType(WMQConstants.WMQ_CM_CLIENT);
214+
215+
String uri = config.getServerUri();
216+
if (!Strings.isNullOrEmpty(uri)) {
217+
URI parsedUri = new URI(uri);
218+
String host = parsedUri.getHost();
219+
int port = parsedUri.getPort();
220+
if (host != null) {
221+
cf.setHostName(host);
222+
}
223+
if (port > 0) {
224+
cf.setPort(port);
225+
}
226+
if (parsedUri.getQuery() != null) {
227+
for (String param : Splitter.on('&').split(parsedUri.getQuery())) {
228+
List<String> pair = Splitter.on('=').splitToList(param);
229+
if (pair.size() == 2) {
230+
if ("channel".equalsIgnoreCase(pair.get(0))) {
231+
cf.setChannel(pair.get(1));
232+
} else if ("queueManager".equalsIgnoreCase(pair.get(0))) {
233+
cf.setQueueManager(pair.get(1));
234+
}
235+
}
236+
}
237+
}
238+
}
239+
240+
String username = config.getUsername();
241+
if (username != null) {
242+
cf.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, true);
243+
cf.setStringProperty(WMQConstants.USERID, username);
244+
String password = config.getPassword();
245+
if (password != null) {
246+
cf.setStringProperty(WMQConstants.PASSWORD, password);
247+
}
248+
}
249+
return cf;
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)