/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.activemq.artemis.junit;
import javax.jms.BytesMessage;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.StreamMessage;
import javax.jms.TextMessage;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.TransportConfiguration;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.config.FileDeploymentManager;
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl;
import org.apache.activemq.artemis.core.config.impl.FileConfiguration;
import org.apache.activemq.artemis.core.remoting.impl.invm.InVMAcceptorFactory;
import org.apache.activemq.artemis.core.remoting.impl.invm.TransportConstants;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.BindingQueryResult;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory;
import org.apache.activemq.artemis.jms.client.ActiveMQDestination;
import org.apache.activemq.artemis.jms.client.DefaultConnectionProperties;
import org.apache.activemq.artemis.jms.server.config.JMSConfiguration;
import org.apache.activemq.artemis.jms.server.config.impl.FileJMSConfiguration;
import org.apache.activemq.artemis.jms.server.config.impl.JMSConfigurationImpl;
import org.apache.activemq.artemis.jms.server.embedded.EmbeddedJMS;
import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A JUnit Rule that embeds an ActiveMQ Artemis JMS server into a test.
*
* This JUnit Rule is designed to simplify using embedded servers in unit tests. Adding the rule to a test will startup
* an embedded JMS server, which can then be used by client applications.
*
* <pre><code>
* public class SimpleTest {
* @Rule
* public EmbeddedJMSResource server = new EmbeddedJMSResource();
*
* @Test
* public void testSomething() throws Exception {
* // Use the embedded server here
* }
* }
* </code></pre>
*/
public class EmbeddedJMSResource extends ExternalResource {
static final String SERVER_NAME = "embedded-jms-server";
Logger log = LoggerFactory.getLogger(this.getClass());
Integer serverId = null;
Configuration configuration;
JMSConfiguration jmsConfiguration;
EmbeddedJMS jmsServer;
InternalClient internalClient;
/**
* Create a default EmbeddedJMSResource
*/
public EmbeddedJMSResource() {
this(false);
}
/**
* Create a default EmbeddedJMSResource
*/
public EmbeddedJMSResource(boolean useNetty) {
try {
configuration = new ConfigurationImpl().setName(SERVER_NAME).setPersistenceEnabled(false).setSecurityEnabled(false).addAcceptorConfiguration("invm", "vm://0");
if (useNetty) {
configuration.addAcceptorConfiguration("netty", DefaultConnectionProperties.DEFAULT_BROKER_BIND_URL);
}
jmsConfiguration = new JMSConfigurationImpl();
init();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* The acceptor used
*/
public EmbeddedJMSResource addAcceptor(String name, String uri) throws Exception {
configuration.addAcceptorConfiguration(name, uri);
return this;
}
/**
* Create a default EmbeddedJMSResource with the specified server id
*/
public EmbeddedJMSResource(int serverId) {
this.serverId = serverId;
Map<String, Object> props = new HashMap<>();
props.put(TransportConstants.SERVER_ID_PROP_NAME, serverId);
configuration = new ConfigurationImpl().setName(SERVER_NAME + "-" + serverId).setPersistenceEnabled(false).setSecurityEnabled(false).addAcceptorConfiguration(new TransportConfiguration(InVMAcceptorFactory.class.getName(), props));
jmsConfiguration = new JMSConfigurationImpl();
init();
}
/**
* Create an EmbeddedJMSResource with the specified configurations
*
* @param configuration ActiveMQServer configuration
* @param jmsConfiguration JMSServerManager configuration
*/
public EmbeddedJMSResource(Configuration configuration, JMSConfiguration jmsConfiguration) {
this.configuration = configuration;
this.jmsConfiguration = jmsConfiguration;
init();
}
/**
* Create an EmbeddedJMSResource with the specified configuration file
*
* @param filename configuration file name
*/
public EmbeddedJMSResource(String filename) {
this(filename, filename);
}
/**
* Create an EmbeddedJMSResource with the specified configuration file
*
* @param serverConfigurationFileName ActiveMQServer configuration file name
* @param jmsConfigurationFileName JMSServerManager configuration file name
*/
public EmbeddedJMSResource(String serverConfigurationFileName, String jmsConfigurationFileName) {
if (serverConfigurationFileName == null) {
throw new IllegalArgumentException("ActiveMQServer configuration file name cannot be null");
}
if (jmsConfigurationFileName == null) {
throw new IllegalArgumentException("JMSServerManager configuration file name cannot be null");
}
FileDeploymentManager coreDeploymentManager = new FileDeploymentManager(serverConfigurationFileName);
FileConfiguration coreConfiguration = new FileConfiguration();
coreDeploymentManager.addDeployable(coreConfiguration);
try {
coreDeploymentManager.readConfiguration();
} catch (Exception readCoreConfigEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to read ActiveMQServer configuration from file %s", serverConfigurationFileName), readCoreConfigEx);
}
this.configuration = coreConfiguration;
FileJMSConfiguration jmsConfiguration = new FileJMSConfiguration();
FileDeploymentManager jmsDeploymentManager = new FileDeploymentManager(jmsConfigurationFileName);
jmsDeploymentManager.addDeployable(jmsConfiguration);
try {
jmsDeploymentManager.readConfiguration();
} catch (Exception readJmsConfigEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to read JMSServerManager configuration from file %s", jmsConfigurationFileName), readJmsConfigEx);
}
this.jmsConfiguration = jmsConfiguration;
init();
}
public static void setMessageProperties(Message message, Map<String, Object> properties) {
if (properties != null && properties.size() > 0) {
for (Map.Entry<String, Object> property : properties.entrySet()) {
try {
message.setObjectProperty(property.getKey(), property.getValue());
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to set property {%s = %s}", property.getKey(), property.getValue().toString()), jmsEx);
}
}
}
}
private void init() {
if (jmsServer == null) {
jmsServer = new EmbeddedJMS().setConfiguration(configuration).setJmsConfiguration(jmsConfiguration);
}
}
/**
* Start the embedded EmbeddedJMSResource.
* <p>
* The server will normally be started by JUnit using the before() method. This method allows the server to
* be started manually to support advanced testing scenarios.
*/
public void start() {
log.info("Starting {}: {}", this.getClass().getSimpleName(), this.getServerName());
try {
jmsServer.start();
} catch (Exception ex) {
throw new RuntimeException(String.format("Exception encountered starting %s: %s", jmsServer.getClass().getSimpleName(), this.getServerName()), ex);
}
}
/**
* Stop the embedded ActiveMQ broker, blocking until the broker has stopped.
* <p>
* The broker will normally be stopped by JUnit using the after() method. This method allows the broker to
* be stopped manually to support advanced testing scenarios.
*/
public void stop() {
log.info("Stopping {}: {}", this.getClass().getSimpleName(), this.getServerName());
if (internalClient != null) {
internalClient.stop();
internalClient = null;
}
if (jmsServer != null) {
try {
jmsServer.stop();
} catch (Exception ex) {
log.warn(String.format("Exception encountered stopping %s: %s - ignoring", jmsServer.getClass().getSimpleName(), this.getServerName()), ex);
}
}
}
/**
* Start the embedded ActiveMQ Broker
* <p>
* Invoked by JUnit to setup the resource
*/
@Override
protected void before() throws Throwable {
log.info("Starting {}: {}", this.getClass().getSimpleName(), getServerName());
this.start();
super.before();
}
/**
* Stop the embedded ActiveMQ Broker
* <p>
* Invoked by JUnit to tear down the resource
*/
@Override
protected void after() {
log.info("Stopping {}: {}", this.getClass().getSimpleName(), getServerName());
super.after();
this.stop();
}
/**
* Get the EmbeddedJMS server.
* <p>
* This may be required for advanced configuration of the EmbeddedJMS server.
*
* @return
*/
public EmbeddedJMS getJmsServer() {
return jmsServer;
}
/**
* Get the name of the EmbeddedJMS server
*
* @return name of the server
*/
public String getServerName() {
String name = "unknown";
ActiveMQServer activeMQServer = jmsServer.getActiveMQServer();
if (activeMQServer != null) {
name = activeMQServer.getConfiguration().getName();
} else if (configuration != null) {
name = configuration.getName();
}
return name;
}
/**
* Get the VM URL for the embedded EmbeddedActiveMQ server
*
* @return the VM URL for the embedded server
*/
public String getVmURL() {
String vmURL = "vm://0";
for (TransportConfiguration transportConfiguration : configuration.getAcceptorConfigurations()) {
Map<String, Object> params = transportConfiguration.getParams();
if (params != null && params.containsKey(TransportConstants.SERVER_ID_PROP_NAME)) {
vmURL = "vm://" + params.get(TransportConstants.SERVER_ID_PROP_NAME);
}
}
return vmURL;
}
/**
* Get the Queue corresponding to a JMS Destination.
* <p>
* The full name of the JMS destination including the prefix should be provided - i.e. queue://myQueue
* or topic://myTopic. If the destination type prefix is not included in the destination name, a prefix
* of "queue://" is assumed.
*
* @param destinationName the full name of the JMS Destination
* @return the number of messages in the JMS Destination
*/
public Queue getDestinationQueue(String destinationName) {
Queue queue = null;
ActiveMQDestination destination = ActiveMQDestination.createDestination(destinationName, ActiveMQDestination.TYPE.QUEUE);
String address = destination.getAddress();
String name = destination.getName();
if (destination.isQueue()) {
queue = jmsServer.getActiveMQServer().locateQueue(destination.getSimpleAddress());
} else {
BindingQueryResult bindingQueryResult = null;
try {
bindingQueryResult = jmsServer.getActiveMQServer().bindingQuery(destination.getSimpleAddress());
} catch (Exception ex) {
log.error(String.format("getDestinationQueue( %s ) - bindingQuery for %s failed", destinationName, destination.getAddress()), ex);
return null;
}
if (bindingQueryResult.isExists()) {
List<SimpleString> queueNames = bindingQueryResult.getQueueNames();
if (queueNames.size() > 0) {
queue = jmsServer.getActiveMQServer().locateQueue(queueNames.get(0));
}
}
}
return queue;
}
/**
* Get the Queues corresponding to a JMS Topic.
* <p>
* The full name of the JMS Topic including the prefix should be provided - i.e. topic://myTopic. If the destination type prefix
* is not included in the destination name, a prefix of "topic://" is assumed.
*
* @param topicName the full name of the JMS Destination
* @return the number of messages in the JMS Destination
*/
public List<Queue> getTopicQueues(String topicName) {
List<Queue> queues = new LinkedList<>();
ActiveMQDestination destination = ActiveMQDestination.createDestination(topicName, ActiveMQDestination.TYPE.TOPIC);
if (!destination.isQueue()) {
BindingQueryResult bindingQueryResult = null;
try {
bindingQueryResult = jmsServer.getActiveMQServer().bindingQuery(destination.getSimpleAddress());
} catch (Exception ex) {
log.error(String.format("getTopicQueues( %s ) - bindingQuery for %s failed", topicName, destination.getAddress()), ex);
return queues;
}
if (bindingQueryResult.isExists()) {
ActiveMQServer activeMQServer = jmsServer.getActiveMQServer();
for (SimpleString queueName : bindingQueryResult.getQueueNames()) {
queues.add(activeMQServer.locateQueue(queueName));
}
}
}
return queues;
}
/**
* Get the number of messages in a specific JMS Destination.
* <p>
* The full name of the JMS destination including the prefix should be provided - i.e. queue://myQueue
* or topic://myTopic. If the destination type prefix is not included in the destination name, a prefix
* of "queue://" is assumed.
*
* NOTE: For JMS Topics, this returned count will be the total number of messages for all subscribers. For
* example, if there are two subscribers on the topic and a single message is published, the returned count will
* be two (one message for each subscriber).
*
* @param destinationName the full name of the JMS Destination
* @return the number of messages in the JMS Destination
*/
public long getMessageCount(String destinationName) {
long count = 0;
ActiveMQDestination destination = ActiveMQDestination.createDestination(destinationName, ActiveMQDestination.TYPE.QUEUE);
if (destination.isQueue()) {
Queue queue = getDestinationQueue(destinationName);
if (queue == null) {
log.warn("getMessageCount(destinationName) - destination {} not found; returning -1", destinationName);
count = -1;
} else {
count = queue.getMessageCount();
}
} else {
for (Queue topicQueue : getTopicQueues(destinationName)) {
count += topicQueue.getMessageCount();
}
}
return count;
}
public BytesMessage createBytesMessage() {
return getInternalClient().createBytesMessage();
}
public TextMessage createTextMessage() {
return getInternalClient().createTextMessage();
}
public MapMessage createMapMessage() {
return getInternalClient().createMapMessage();
}
public ObjectMessage createObjectMessage() {
return getInternalClient().createObjectMessage();
}
public StreamMessage createStreamMessage() {
return getInternalClient().createStreamMessage();
}
public BytesMessage createMessage(byte[] body) {
return createMessage(body, null);
}
public TextMessage createMessage(String body) {
return createMessage(body, null);
}
public MapMessage createMessage(Map<String, Object> body) {
return createMessage(body, null);
}
public ObjectMessage createMessage(Serializable body) {
return createMessage(body, null);
}
public BytesMessage createMessage(byte[] body, Map<String, Object> properties) {
BytesMessage message = this.createBytesMessage();
if (body != null) {
try {
message.writeBytes(body);
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to set body {%s} on BytesMessage", new String(body)), jmsEx);
}
}
setMessageProperties(message, properties);
return message;
}
public TextMessage createMessage(String body, Map<String, Object> properties) {
TextMessage message = this.createTextMessage();
if (body != null) {
try {
message.setText(body);
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to set body {%s} on TextMessage", body), jmsEx);
}
}
setMessageProperties(message, properties);
return message;
}
public MapMessage createMessage(Map<String, Object> body, Map<String, Object> properties) {
MapMessage message = this.createMapMessage();
if (body != null) {
for (Map.Entry<String, Object> entry : body.entrySet()) {
try {
message.setObject(entry.getKey(), entry.getValue());
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to set body entry {%s = %s} on MapMessage", entry.getKey(), entry.getValue().toString()), jmsEx);
}
}
}
setMessageProperties(message, properties);
return message;
}
public ObjectMessage createMessage(Serializable body, Map<String, Object> properties) {
ObjectMessage message = this.createObjectMessage();
if (body != null) {
try {
message.setObject(body);
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to set body {%s} on ObjectMessage", body.toString()), jmsEx);
}
}
setMessageProperties(message, properties);
return message;
}
public void pushMessage(String destinationName, Message message) {
if (destinationName == null) {
throw new IllegalArgumentException("sendMessage failure - destination name is required");
} else if (message == null) {
throw new IllegalArgumentException("sendMessage failure - a Message is required");
}
ActiveMQDestination destination = ActiveMQDestination.createDestination(destinationName, ActiveMQDestination.TYPE.QUEUE);
getInternalClient().pushMessage(destination, message);
}
public BytesMessage pushMessage(String destinationName, byte[] body) {
BytesMessage message = createMessage(body, null);
pushMessage(destinationName, message);
return message;
}
public TextMessage pushMessage(String destinationName, String body) {
TextMessage message = createMessage(body, null);
pushMessage(destinationName, message);
return message;
}
public MapMessage pushMessage(String destinationName, Map<String, Object> body) {
MapMessage message = createMessage(body, null);
pushMessage(destinationName, message);
return message;
}
public ObjectMessage pushMessage(String destinationName, Serializable body) {
ObjectMessage message = createMessage(body, null);
pushMessage(destinationName, message);
return message;
}
public BytesMessage pushMessageWithProperties(String destinationName, byte[] body, Map<String, Object> properties) {
BytesMessage message = createMessage(body, properties);
pushMessage(destinationName, message);
return message;
}
public TextMessage pushMessageWithProperties(String destinationName, String body, Map<String, Object> properties) {
TextMessage message = createMessage(body, properties);
pushMessage(destinationName, message);
return message;
}
public MapMessage pushMessageWithProperties(String destinationName,
Map<String, Object> body,
Map<String, Object> properties) {
MapMessage message = createMessage(body, properties);
pushMessage(destinationName, message);
return message;
}
public ObjectMessage pushMessageWithProperties(String destinationName,
Serializable body,
Map<String, Object> properties) {
ObjectMessage message = createMessage(body, properties);
pushMessage(destinationName, message);
return message;
}
public Message peekMessage(String destinationName) {
if (null == jmsServer) {
throw new NullPointerException("peekMessage failure - BrokerService is null");
}
if (destinationName == null) {
throw new IllegalArgumentException("peekMessage failure - destination name is required");
}
throw new UnsupportedOperationException("Not yet implemented");
}
public BytesMessage peekBytesMessage(String destinationName) {
return (BytesMessage) peekMessage(destinationName);
}
public TextMessage peekTextMessage(String destinationName) {
return (TextMessage) peekMessage(destinationName);
}
public MapMessage peekMapMessage(String destinationName) {
return (MapMessage) peekMessage(destinationName);
}
public ObjectMessage peekObjectMessage(String destinationName) {
return (ObjectMessage) peekMessage(destinationName);
}
public StreamMessage peekStreamMessage(String destinationName) {
return (StreamMessage) peekMessage(destinationName);
}
private InternalClient getInternalClient() {
if (internalClient == null) {
log.info("Creating InternalClient");
internalClient = new InternalClient();
internalClient.start();
}
return internalClient;
}
public static class EmbeddedJMSResourceException extends RuntimeException {
public EmbeddedJMSResourceException(String message) {
super(message);
}
public EmbeddedJMSResourceException(String message, Exception cause) {
super(message, cause);
}
}
private class InternalClient {
ConnectionFactory connectionFactory;
Connection connection;
Session session;
MessageProducer producer;
InternalClient() {
}
void start() {
connectionFactory = new ActiveMQConnectionFactory(getVmURL());
try {
connection = connectionFactory.createConnection();
session = connection.createSession();
producer = session.createProducer(null);
connection.start();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("InternalClient creation failure", jmsEx);
}
}
void stop() {
try {
producer.close();
} catch (JMSException jmsEx) {
log.warn("JMSException encounter closing InternalClient Session - MessageProducer", jmsEx);
} finally {
producer = null;
}
try {
session.close();
} catch (JMSException jmsEx) {
log.warn("JMSException encounter closing InternalClient Session - ignoring", jmsEx);
} finally {
session = null;
}
if (null != connection) {
try {
connection.close();
} catch (JMSException jmsEx) {
log.warn("JMSException encounter closing InternalClient Connection - ignoring", jmsEx);
} finally {
connection = null;
}
}
}
public BytesMessage createBytesMessage() {
checkSession();
try {
return session.createBytesMessage();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("Failed to create BytesMessage", jmsEx);
}
}
public TextMessage createTextMessage() {
checkSession();
try {
return session.createTextMessage();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("Failed to create TextMessage", jmsEx);
}
}
public MapMessage createMapMessage() {
checkSession();
try {
return session.createMapMessage();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("Failed to create MapMessage", jmsEx);
}
}
public ObjectMessage createObjectMessage() {
checkSession();
try {
return session.createObjectMessage();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("Failed to create ObjectMessage", jmsEx);
}
}
public StreamMessage createStreamMessage() {
checkSession();
try {
return session.createStreamMessage();
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException("Failed to create StreamMessage", jmsEx);
}
}
public void pushMessage(ActiveMQDestination destination, Message message) {
if (producer == null) {
throw new IllegalStateException("JMS MessageProducer is null - has the InternalClient been started?");
}
try {
producer.send(destination, message);
} catch (JMSException jmsEx) {
throw new EmbeddedJMSResourceException(String.format("Failed to push %s to %s", message.getClass().getSimpleName(), destination.toString()), jmsEx);
}
}
void checkSession() {
if (session == null) {
throw new IllegalStateException("JMS Session is null - has the InternalClient been started?");
}
}
}
}