Does Apache Camel Mail Component support reuse of TCP connection for sending bulk emails?
If not, is it feasible/recommendable to write custom org.apache.camel.component.mail.JavaMailSender to do so?
I think it's not possible with the standard camel mail component and also implementing a JavaMailSender won't do the trick for you imo.
If you have a look at org.apache.camel.component.mail.MailProducer(2.13.0):
public void process(final Exchange exchange) {
try {
MimeMessage mimeMessage;
final Object body = exchange.getIn().getBody();
if (body instanceof MimeMessage) {
// Body is directly a MimeMessage
mimeMessage = (MimeMessage) body;
} else {
// Create a message with exchange data
mimeMessage = new MimeMessage(sender.getSession());
getEndpoint().getBinding().populateMailMessage(getEndpoint(), mimeMessage, exchange);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Sending MimeMessage: {}", MailUtils.dumpMessage(mimeMessage));
}
sender.send(mimeMessage);
// set the message ID for further processing
exchange.getIn().setHeader(MailConstants.MAIL_MESSAGE_ID, mimeMessage.getMessageID());
} catch (MessagingException e) {
exchange.setException(e);
} catch (IOException e) {
exchange.setException(e);
}
}
There is only 1 message or exchange object send/handled.
So maybe implementing your own "BulkMailComponent" with the functionality you need can be a solution for you.
Related
I am trying to receive JSON messages from a Solace JMS queue but I am not receiving any message. Below is my code
#Service
public class QueueConsumer {
final String QUEUE_NAME = "test.Request.Q.V01";
// Latch used for synchronizing between threads
final CountDownLatch latch = new CountDownLatch(1);
public void run(String... args) throws Exception {
String host = "test.solace.com";
String vpnName = "TEST_VPN";
String username = "testVpn";
String password = "test123";
System.out.printf("QueueConsumer is connecting to Solace messaging at %s...%n", host);
SolConnectionFactory connectionFactory = SolJmsUtility.createConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setVPN(vpnName);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setDynamicDurables(true);
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, SupportedProperty.SOL_CLIENT_ACKNOWLEDGE);
System.out.printf("Connected to the Solace Message VPN '%s' with client username '%s'.%n", vpnName, username);
Queue queue = session.createQueue(QUEUE_NAME);
MessageConsumer messageConsumer = session.createConsumer(queue);
messageConsumer.setMessageListener(new MessageListener() {
#Override
public void onMessage(Message message) {
try {
if (message instanceof SolaceMsg) {
System.out.printf("TextMessage received: '%s'%n", ((SolaceMsg) message).getClass());
} else {
System.out.println("Message received.");
}
System.out.printf("Message Content:%n%s%n", SolJmsUtility.dumpMessage(message));
message.acknowledge();
latch.countDown(); // unblock the main thread
} catch (JMSException ex) {
System.out.println("Error processing incoming message.");
ex.printStackTrace();
}
}
});
System.out.println("Start receiving messages....");
connection.start();
System.out.println("Awaiting message...");
latch.await();
connection.stop();
messageConsumer.close();
session.close();
connection.close();
}
public static void main(String... args) throws Exception {
new QueueConsumer().run(args);
}
}
My message type is JSON ad below, and I have created a POJO for this.
{
"customerDetails": {
"customerID": "0001234",
"customerName": "John"
}
}
I am getting one warning saying Response - 400 Queue already exists as it is an existing queue, and I am not receiving any messages. What am I doing wrong here?
Your code snippet looks correct. You can log on to the PubSub+ Manager of your event broker to verify that the client is binding to the correct queue and that the messages were successfully published to the queue and are waiting to be consumed. You can also enable Solace JMS API logging to understand more about what the application is doing: https://docs.solace.com/Solace-JMS-API/Code-and-Compile-Guideli.htm
I'm trying to setup a headers exchange with a queue where messages are routed based on a recipient header.
The exchange is of type headers.
So far the class is able to connect to the exchange and feed messages to the queue.
It's also able to subscribe to the queue and receive messages. It also closes the connection whenever the subscriber's connection is cancelled.
The current problem is that the message is not routed by the recipient's header value.
Given the following class:
import com.rabbitmq.client.*;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
#Slf4j
public class MyQueue {
private final ConnectionFactory connectionFactory;
private Channel channel;
public MyQueue(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
public String sendMessage(TestTextMessage message) throws UndeliverableMessageException {
try (Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel()) {
Map<String, Object> headers = new HashMap<>();
headers.put(RabbitMqConfig.MATCH_HEADER, message.getRecipient());
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.deliveryMode(MessageProperties.PERSISTENT_TEXT_PLAIN.getDeliveryMode())
.priority(MessageProperties.PERSISTENT_TEXT_PLAIN.getPriority())
.headers(headers).build();
log.info("Sending message to {}", headers);
channel.basicPublish(RabbitMqConfig.EXCHANGE_NAME, "", props,
message.getMessage().getBytes(StandardCharsets.UTF_8));
log.info("RabbitMQ sent message {} to {}", message.getMessage(), message.getRecipient());
return "ok";
} catch (TimeoutException e) {
log.error("Rabbit mq timeout", e);
} catch (IOException e) {
log.error("Rabbit mq io error", e);
}
throw new UndeliverableMessageException();
}
public Flux<String> listenMessages(String recipient) throws IOException, TimeoutException {
Connection connection = connectionFactory.newConnection();
this.channel = connection.createChannel();
// The map for the headers.
Map<String, Object> headers = new HashMap<>();
headers.put("x-match", "all");
headers.put(RabbitMqConfig.MATCH_HEADER, recipient);
final String[] consumerTag = new String[1];
Flux<String> as = Flux.create(sink -> new MessageListener<String>() {
{
try {
log.info("Binding to {}", headers);
channel.queueBind(RabbitMqConfig.QUEUE_NAME, RabbitMqConfig.EXCHANGE_NAME, "",
headers);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
log.info("Subscriber {} received a message {} with headers {}", recipient, delivery.getEnvelope(),
delivery.getProperties().getHeaders());
sink.next(delivery.getEnvelope().getDeliveryTag() + "--" + message);
//channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
consumerTag[0] = channel.basicConsume(RabbitMqConfig.QUEUE_NAME,
true, deliverCallback, tag -> {
sink.complete();
});
} catch (IOException e) {
log.error("RabbitMQ IOException ", e);
}
}
});
return as.doOnCancel(() -> {
try {
if (consumerTag[0] == null) {
log.error("RabbitMQ uncloseable subscription, consumerTag is null!");
channel.close();
return;
}
channel.basicCancel(consumerTag[0]);
channel.close();
log.info("RabbitMQ CANCEL subscription for recipient {}", recipient);
} catch (IOException | TimeoutException e) {
log.error("RabbitMQ channel close error", e);
}
});
}
interface MessageListener<T> {
}
}
The exchange is declared by the following call
channel.exchangeDeclare(RabbitMqConfig.EXCHANGE_NAME, BuiltinExchangeType.HEADERS, true);
Binding recipient log:
Binding to {x-match=all, message-recipient=mary}
Binding to {x-match=all, message-recipient=james}
Binding to {x-match=all, message-recipient=john}
Bound 3 recipients with x-match:
However, messages are not matched, as if they were routed randomly
Sending message to {message-recipient=james}
RabbitMQ sent message Hey there to james
Subscriber mary received a message Envelope(deliveryTag=1, redeliver=false, exchange=my-exchange, routingKey=) with headers {message-recipient=james}
Sending message to {message-recipient=james}
RabbitMQ sent message Hey there to james
Subscriber james received a message Envelope(deliveryTag=1, redeliver=false, exchange=my-exchange, routingKey=) with headers {message-recipient=james}
Sending message to {message-recipient=james}
RabbitMQ sent message Hey there to james
Subscriber john received a message Envelope(deliveryTag=1, redeliver=false, exchange=my-exchange, routingKey=) with headers {message-recipient=james}
Why isn't x-match: all, matching?
After reading the comment posted by #Gryphon, on the subscriber side, I ended up creating a queue for each participant.
channel.queueDeclare(RabbitMqConfig.QUEUE_NAME + "-" + recipient,
true,
false,
false,
null)
On the publisher side, code remains unchanged, the messages are sent to the exchange, and the exchange will handle routing based on the x-match: all configuration, routing the messages to the appropiate queue.
I am trying to understand the Bayeux protocol. I haven't found a web-resource explaining how the bayeux client will technically work, in detail.
From this resource,
The Bayeux protocol requires that the first message a new client sends
be a handshake message (a message sent on /meta/handshake channel).
The client processes the handshake reply, and if it is successful,
starts – under the covers – a heartbeat mechanism with the server, by
exchanging connect messages (a message sent on a /meta/connect
channel).
The details of this heartbeat mechanism depend on the client
transport used, but can be seen as the client sending a connect
message and expecting a reply after some time.
Connect messages continue to flow between client and server until
either side decides to disconnect by sending a disconnect message (a
message sent on the /meta/disconnect channel).
I have written in Java methods to first do a handshake, then subscribe to a particular channel. I made use of the Apache HttpClient library to do the HTTP POST requests.
Now comes the part of connect.
My understanding is that, I need to keep a request open to the bayeux server and whenever I receive a response, make another request.
I have the written the below code. Is my understanding correct and does this bayeux client exhibit the correct connect functionality? (please ignore the missing disconnect, unsubscribe methods)
Also, I have tested the code against a bayeux server and it works correctly.
/* clientId - Unique clientId returned by bayeux server during handshake
responseHandler - see interface below */
private static void connect(String clientId, ResponseHandler responseHandler)
throws ClientProtocolException, UnsupportedEncodingException, IOException {
String message = "[{\"channel\":\"/meta/connect\","
+ "\"clientId\":\"" + clientId + "\"}]";
CloseableHttpClient httpClient = HttpClients.createDefault();
Thread t = new Thread(new Runnable() {
#Override
public void run() {
while (!doDisconnect) {
try {
CloseableHttpResponse response = HttpPostHelper.postToURL(ConfigurationMock.urlRealTime,
message, httpClient, ConfigurationMock.getAuthorizationHeader());
responseHandler.handleResponse(response);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
httpClient.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
t.start();
}
/*Simple interface to define what happens with the response when it arrives*/
private interface ResponseHandler {
void handleResponse(CloseableHttpResponse httpResponse);
}
public static void main(String[] args) throws Exception{
String globalClientId = doHandShake(); //assume this method exists
subscribe(globalClientId,"/measurements/10500"); //assume this method exists
connect(globalClientId, new ResponseHandler() {
#Override
public void handleResponse(CloseableHttpResponse httpResponse) {
try {
System.out.println(HttpPostHelper.toStringResponse(httpResponse));
} catch (ParseException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
Your code is not correct.
Messages on the /meta/connect channel do not have the subscription field.
Subscriptions must be sent on the /meta/subscribe channel.
You want to study the Bayeux Specification for further details, in particular the meta messages section and the event messages section.
A suggestion is to launch the CometD Demo and look at the messages exchanged by the client, and mimic those in your implementation.
Have Apache Camel simple message route from folder to ActiveMQ topic:
//Create context to create endpoint, routes, processor within context scope
CamelContext context = new DefaultCamelContext();
//Create endpoint route
context.addRoutes(new RouteBuilder() {
#Override
public void configure() throws Exception
{
from("file:data/outbox").to("activemq:topic:Vadim_Topic");
//from("activemq:topic:TEST").to.to("file:data/outbox");
}
});
context.start();
Thread.sleep(5000);
context.stop();
}
And JMS implementation if Topic Consumer:
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory();
try {
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//connection.setClientID("12345");
connection.start();
Topic topic = session.createTopic("Vadim_Topic");
MessageConsumer messageConsumer = session.createConsumer(topic);
MessageListener messageListener = new MessageListener() {
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
try {
System.out.println("Received message: " + textMessage.getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
};
messageConsumer.setMessageListener(messageListener);
} catch (Exception e) {
e.printStackTrace();
}
Can't understand why is my consumer can't recieve messages sent by Camel route??
I guess thet problem is then I need to subscribe my JMS Consumer on messages sent by Camel?
How can I do this if this is the case?
Camel not only allows you to send messages to a topic, it can also very easily read messages from a topic and send it to one of your POJOs.
A route that reads from your topic and sends the messages to a POJO would look like this:
from("activemq:topic:Vadim_Topic").bean(ExampleBean.class);
Camel will figure out which method to call on the POJO depending on the type of message it received, and the available method signatures. See this page for details on using POJO's in camel routes: https://camel.apache.org/bean.html
I am trying to make my web service secure by making one of the methods require HTTP Basic authentication. In order to do this, I've implemented a custom Interceptor called LoginInterceptor that checks the requested URL, and if it corresponds to a method called open, it checks the message header for the username and password.
If there are no authentication fields in the header, the response code is set to HTTP_UNAUTHORIZED, and if the username or password is incorrect, the response code is set to HTTP_FORBIDDEN. Here's the code:
public LoginInterceptor() {
super(Phase.RECEIVE);
addAfter(RequestInterceptor.class.getName()); //another custom interceptor, for some simple redirections.
}
public void handleMessage(Message message) throws Fault {
String requestURI = message.get(Message.REQUEST_URI).toString();
String methodKeyword = requestURI.substring("localhost".length()+1).split("/")[0];
if(methodKeyword.equals("open")) {
AuthorizationPolicy policy = message.get(AuthorizationPolicy.class);
if(policy == null) {
sendErrorResponse(message, HttpURLConnection.HTTP_UNAUTHORIZED);
return;
}
//userPasswords is a hashmap of usernames and passwords.
String realPassword = userPasswords.get(policy.getUserName());
if (realPassword == null || !realPassword.equals(policy.getPassword())) {
sendErrorResponse(message, HttpURLConnection.HTTP_FORBIDDEN);
}
}
}
//This is where the response code is set, and this is where I'd like to write the response message.
private void sendErrorResponse(Message message, int responseCode) {
Message outMessage = getOutMessage(message);
outMessage.put(Message.RESPONSE_CODE, responseCode);
// Set the response headers
Map responseHeaders = (Map) message.get(Message.PROTOCOL_HEADERS);
if (responseHeaders != null) {
responseHeaders.put("Content-Type", Arrays.asList("text/html"));
responseHeaders.put("Content-Length", Arrays.asList(String.valueOf("0")));
}
message.getInterceptorChain().abort();
try {
getConduit(message).prepare(outMessage);
close(outMessage);
} catch (IOException e) {
e.printStackTrace();
}
}
private Message getOutMessage(Message inMessage) {
Exchange exchange = inMessage.getExchange();
Message outMessage = exchange.getOutMessage();
if (outMessage == null) {
Endpoint endpoint = exchange.get(Endpoint.class);
outMessage = endpoint.getBinding().createMessage();
exchange.setOutMessage(outMessage);
}
outMessage.putAll(inMessage);
return outMessage;
}
//Not actually sure what this does. Copied from a tutorial online. Any explanation is welcome
private Conduit getConduit(Message inMessage) throws IOException {
Exchange exchange = inMessage.getExchange();
Conduit conduit = exchange.getDestination().getBackChannel(inMessage);
exchange.setConduit(conduit);
return conduit;
}
private void close(Message outMessage) throws IOException {
OutputStream os = outMessage.getContent(OutputStream.class);
os.flush();
os.close();
}
This works fine, however, I want to also return a message in the response, something like "incorrect username or password". I've tried, from within the sendErrorResponse method, doing:
outMessage.setContent(String.class, "incorrect username or password")
and I set the content-length to "incorrect username or password".length(). This doesn't work, I guess because the Apache CXF Messages use InputStreams and OutputStreams.
So I tried:
OutputStream os = outMessage.getContent(OutputStream.class);
try {
os.write("incorrect username or password".getBytes() );
outMessage.setContent(OutputStream.class, os);
} catch (IOException e) {
e.printStackTrace();
}
This doesn't work either. When stepping through with a debugger, I notice that os is null When testing with Postman, I get:
Could not get any response This seems to be like an error connecting
to http://localhost:9090/launcher/open. The response status was 0.
Check out the W3C XMLHttpRequest Level 2 spec for more details about
when this happens.
Pressing ctrl+shif+c (opening up dev tools) in Chrome, and checking the networks tab, I see:
"ERR_CONTENT_LENGTH_MISMATCH"
I've tried using an XMLStreamWriter, but that wans't any better.
Questions:
I can return the correct response code (401 Unauthorized and 403 forbidden), but how do I return a message in the response body?
Do I need to specifically extend a particular OutInterceptor like JASRXOutInterceptor in order to modify the message content?
I tried using a JAASInterceptor before, but I didn't manage to get that working. Could someone show me how to implement it that way, if that's somehow easier?
I could also just throw a fault like this: throw new Fault("incorrect username or password", Logger.getGlobal());, but then the HTTP response code would be 500. I'd prefer to return a proper 401 or 403 response.
Note:
Right now I'm still using HTTP for the transport layer. Once I fix this, I'll change to HTTPS.
Basically, what I wanted to do is return a fault with a HTTP response code of 401 (unauthorized) or 403 (forbidden) instead of 500 (server error). Turns out Apache CXF provides a simple way of doing that, using the Fault.setStatusCode(int) method, as I found from this question on Stack Overflow: how to throw a 403 error in Apache CXF - Java
So this is what my handleMessage method looks like now:
public void handleMessage(Message message) throws Fault {
String requestURI = message.get(Message.REQUEST_URI).toString();
String methodKeyword = requestURI.substring("localhost".length()+1).split("/")[0];
if(methodKeyword.equals("open")) {
AuthorizationPolicy policy = message.get(AuthorizationPolicy.class);
if(policy == null) {
Fault fault = new Fault("incorrect username or password", Logger.getGlobal());
fault.setStatusCode(401);
throw fault;
}
String realPassword = userPasswords.get(policy.getUserName());
if (realPassword == null || !realPassword.equals(policy.getPassword())) {
Fault fault = new Fault("incorrect username or password", Logger.getGlobal());
fault.setStatusCode(403);
throw fault;
}
}
}
I removed the other methods, they were unnecessary.