ClassCastException: javax.naming.Reference cannot be cast to javax.jms.ConnectionFactory - jms

I wrote a Java program to connect to Websphere MQ to publish messages. I created a JNDI namespace, connection factory, destinations, and queue manager in Websphere MQ Explore. When I am running my program it is showing ClassCastException for type casting from string to ConnectionFactory.
Here is my code. Can anyone help resolve this problem.
JNDIUtil.java
package com.tradefinance.jms.util;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
//JNDI classes
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
//Standard Java classes
import java.util.Hashtable;
import java.util.Properties;
/**
*
* A wrapper class for JNDI calls
*
*/
public class JNDIUtil
{
private Context context;
public JNDIUtil(String icf, String url) throws JMSException, NamingException
{
Hashtable environment = new Hashtable();
environment.put(Context.INITIAL_CONTEXT_FACTORY, icf );
environment.put(Context.PROVIDER_URL, url);
context= new InitialContext( environment );
}
/**
* #param ObjName Object Name to be retrieved
* #return Retrieved Object
* #throws NamingException
*/
private Object getObjectByName(String ObjName) throws NamingException
{
return context.lookup(ObjName);
}
/**
* #param factoryName Factory Name
* #return ConnectionFactory object
* #throws NamingException
*/
public ConnectionFactory getConnectionFactory(String factoryName) throws NamingException
{
return (ConnectionFactory) getObjectByName(factoryName);
}
/**
* #param destinationName Destination Name
* #return ConnectionFactory object
* #throws NamingException
*/
public Destination getDestination(String destinationName) throws NamingException
{
return (Destination) getObjectByName(destinationName);
}
}
NewPublisher.java
package com.tradefinance.jms.topics;
//JMS classes
import javax.jms.JMSException;
import javax.jms.ConnectionFactory;
import javax.jms.Connection;
import javax.jms.Session;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.TextMessage;
//JNDI classes
import javax.naming.NamingException;
import com.tradefinance.jms.util.JNDIUtil;
/**
* A class to demonstrate how to a publish to a topic.
*/
public class NewsPublisher
{
public static String icf = "com.sun.jndi.fscontext.RefFSContextFactory";
public static String url = "file:/C:/JNDI-Directory/";
public static void main(String[] vars) throws JMSException, NamingException
{
ConnectionFactory factory = null;
Connection connection = null;
Session session = null;
Destination destination= null; // a destination can be a topic or a queue
MessageProducer producer= null;
try
{
JNDIUtil jndiUtil= new JNDIUtil(icf,url);
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
connection = factory.createConnection();
connection.start();
// Indicate a non-transactional session
boolean transacted = false;
session = connection.createSession( transacted, Session.AUTO_ACKNOWLEDGE);
destination = jndiUtil.getDestination("NewsTopic");
producer = session.createProducer(destination);
TextMessage message = session.createTextMessage("No News is Good News!");
producer.send(message);
System.out.println("NewsPublisher: Message Publication Completed");
}
finally
{
// Always release resources
if ( producer!= null )
producer.close();
if ( session!= null )
session.close();
if ( connection!= null )
connection.close();
}
}
}
Getting the error on these lines:
return (ConnectionFactory) getObjectByName(factoryName);
in JNDIUtil.java
factory= jndiUtil.getConnectionFactory("TestQM1ConnectionFactory");
in NewPublisher.java

You are missing some JARs of MQ Client to get this working.
I had the same error, and after some further investigation, I ended up with this list of Jars in order to get this working:
com.ibm.mq.jmqi.jar
com.ibm.mqjms.jar
dhbcore.jar
fscontext.jar
providerutil.jar
com.ibm.mq.headers.jar

What you received back from the jndi context was a reference. This is a recipe to build the connection factory and I suspect that the class responsible for this cannot be found because the MQ jars required are not in the classpath. The error message is not intuitive.
Failing that, I find a good way to debug jndi lookup issues is to acquire the context and execute a list() on it, printing out the details of each object returned, just so you're clear on what exactly resides in the directory.

Add the Below jars to the classpath :
sibc.jms
sibc.jndi

In my case, we use TIBCO JMS adding the following dependency is resolved casting issue
ClassCastException: javax.naming.Reference cannot be cast to
javax.jms.ConnectionFactory
implementation 'com.tibco.tibjms:tibjms:5.1.4'

Related

Problem with GCP Secret Manager and Spring Boot app

Spring Boot (2.5.9) has problem to access password from the GCP Secret Manager using spring-cloud-gcp-starter-secretmanager ver 2.0.8 throwing error
AnnotationConfigApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'defaultFeignClientConfiguration': Injection of autowired dependencies failed; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.google.protobuf.ByteString$LiteralByteString] to type [java.lang.String]
for a passsword declared in the application.properties as
webservices.security.basic.user.password=${sm://my-password}
when I will replace it with regular string or even env variable it will work fine.
Failing part of the code looks like:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import feign.Retryer;
import feign.auth.BasicAuthRequestInterceptor;
import feign.codec.ErrorDecoder;
/**
* Default feign client configuration. Includes retry policies, basic auth user name and password, and HTTP status decoder.
* #author Greg Meyer
* #since 6.0
*/
public class DefaultFeignClientConfiguration
{
#Value("${webservices.retry.backoff.multiplier:3}")
protected double backoffMultiplier;
#Value("${webservices.retry.backoff.initialBackoffInterval:100}")
protected long initialBackoffInterval;
#Value("${webservices.retry.backoff.maxInterval:20000}")
protected long maxInterval;
#Value("${webservices.security.basic.user.name:}")
protected String user;
#Value("${webservices.security.basic.user.password:}")
protected String pass;
/**
* Creates an instance of the a the default HTTP status translator.
* #return An instance of the a the default HTTP status translator
*/
#Bean
public ErrorDecoder feignClientErrorDecoder()
{
return new DefaultErrorDecoder();
}
/**
* Creates an instance of BasicAuth interceptor configured with a username and password. This bean is only created if the
* "webservices.security.basic.user.name" property is set.
* #return An instance of BasicAuth interceptor configured with a username and password
*/
#Bean
#ConditionalOnProperty(name="webservices.security.basic.user.name", matchIfMissing=false)
public BasicAuthRequestInterceptor basicAuthRequestInterceptor()
{
return new BasicAuthRequestInterceptor(user, pass);
}
/**
* Creates an instance of a back off policy used in conjuntion with the retry policy.
* #return An instance of a back off policy
*/
#Bean
public LoadBalancedRetryFactory backOffPolciyFactory()
{
return new LoadBalancedRetryFactory()
{
#Override
public BackOffPolicy createBackOffPolicy(String service)
{
final ExponentialBackOffPolicy backoffPolicy = new ExponentialBackOffPolicy();
backoffPolicy.setMultiplier(backoffMultiplier);
backoffPolicy.setInitialInterval(initialBackoffInterval);
backoffPolicy.setMaxInterval(maxInterval);
return backoffPolicy;
}
};
}
/**
* Creates a default http retry policy.
* #return A default http retry policy.
*/
#Bean
public Retryer retryer()
{
/*
* Default retryer config
*/
return new Retryer.Default(200, 1000, 5);
}
}
Any thoughts?
The problem is that most likely, Feign autoconfiguration happens early on, before GcpSecretManagerEnvironmentPostProcessor had a chance to run and introduce ByteString converters.
Basically, the solution that works is to implement a Custom Converter which implements the Generic Conversion Service and register the Converter in the main method before calling SpringApplication.run.
public static void main(String[] args)
{
((DefaultConversionService)DefaultConversionService.getSharedInstance()).addConverter(new CustomConverter());
SpringApplication.run(STAApplication.class, args);
}
and custom converter:
#Component
public class CustomConverter implements GenericConverter {
#Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(ByteString.class, String.class));
}
#Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (sourceType.getType() == String.class) {
return source;
}
try {
source = ((ByteString) source).toString("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return source;
}
}

Spring AMQP handling 2 ConnectionFactory

My app has 2 rabbit instances it needs to connect to.
Rabbit1 is an entry point to my app, where my listener waits for message and
#RabbitListener(queues = "#{myAmqpProperties.getRequest().getQueue()}")
it is configured through regular Springboot2 properties
spring:
rabbitmq:
host: localhost
username: myUser
password: myPass
port: 5672
virtual-host: myVhost
This works fine.
Now I need to send rabbit message on another rabbitMQ instance, Rabbit2.
So I created a configuration class building the rabbitTemplete and its associated connectionFactory.
package com.mycompany.socle.amqp.ocr;
import com.mycompany.doccontrol.messaging.app.AppMessageConverter;
import com.mycompany.socle.amqp.common.service.CpyAmqpRequestWithReplyToProp;
import com.mycompany.socle.amqp.common.service.CpyAmqpServerProperties;
import com.mycompany.socle.common.bean.SslProperties;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Valid;
#Configuration
#EnableRabbit
public class OcrAmqpConfiguration {
#Autowired
private OcrAmqpProperties ocrAmqpProperties;
/**
* Template used to send analysis request.
*
* #return the template to use
* #throws Exception if there are issues while initialising the connection factory.
*/
#Bean
public RabbitTemplate appRequestTemplate() throws Exception {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(ocrConnectionFactory());
rabbitTemplate.setMessageConverter(new AppMessageConverter());
#Valid final CpyAmqpRequestWithReplyToProp appProperties = ocrAmqpProperties.getApp().getRequest();
// Settings to send the request
rabbitTemplate.setExchange(appProperties.getExchange());
if (appProperties.getRoutingKey() != null) {
rabbitTemplate.setRoutingKey(appProperties.getRoutingKey());
}
return rabbitTemplate;
}
/**
* Define the Connection factory used to contact the broker.
*
* #return the connection factory
* #throws Exception if there are issues while defining the factory context.
*/
#Bean
public CachingConnectionFactory ocrConnectionFactory() throws Exception {
final CachingConnectionFactory result = new CachingConnectionFactory(builConnectionFactory(ocrAmqpProperties.getRabbitmq()).getObject());
result.afterPropertiesSet();
return result;
}
private static RabbitConnectionFactoryBean builConnectionFactory(MyAmqpServerProperties pAmqpProperties) {
RabbitConnectionFactoryBean factory = new RabbitConnectionFactoryBean();
// Generic connection properties
factory.setHost(pAmqpProperties.getHost());
factory.setPort(pAmqpProperties.getPort());
factory.setUsername(pAmqpProperties.getUsername());
factory.setPassword(pAmqpProperties.getPassword());
factory.setVirtualHost(pAmqpProperties.getVirtualHost());
factory.afterPropertiesSet();
return factory;
}
}
The class MyAmqpServerProperties is just a property class matching extra property i added in my property file to define Rabbit2 info.
But then the default SpringBoot ConnectionFactory is NOT generated, and my listener which listened originaly to Rabbit1, now also listen to Rabbit2.
I see RabbitAutoConfiguration java doc says:
Registers the following beans: * * {#link
org.springframework.amqp.rabbit.core.RabbitTemplate RabbitTemplate} if
there * is no other bean of the same type in the context. *
{#link
org.springframework.amqp.rabbit.connection.CachingConnectionFactory *
CachingConnectionFactory} instance if there is no other bean of the
same type in the * context.
And in the code there is the annotation
#ConditionalOnMissingBean(ConnectionFactory.class)
=> If I remove the #Bean of the ocrConnectionFactory method, it works fine, however I need to have the ocrConnectionFactory registered in spring context for monitoring ...
So is there a way to either register it AFTER the default one has been generated, or some other properties to force the default one to be generated as normal ?
No. Boot will only configure one if you haven't any. If you configure one you must configure both.

How to Give manual Acknowledge using JmsTemplate and delete message from Rabbitmq queue

I am using RabbitMq(with JMS) with jmsTemplate I am able to Consume Message from RabbitMq Queue But it is taking acknowledgment AUTO.
I have Search API for it but not able to find it out.
How can I set manual acknowledgment.
In Below code when Message is consumed from queue I want to call web service with that message and depends on that response from from I want to delete that message from queue.
I have created one project in which I am using Listener and other project with call to read message from queue
first Project:
package com.es.jms.listener;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.MessageListenerContainer;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import com.rabbitmq.jms.admin.RMQConnectionFactory;
#Configuration
public class RabbitMqMessageListener {
#Bean
public ConnectionFactory jmsConnectionFactory() {
RMQConnectionFactory connectionFactory = new RMQConnectionFactory();
connectionFactory.setUsername("Username");
connectionFactory.setPassword("Password");
connectionFactory.setVirtualHost("vhostname");
connectionFactory.setHost("hostname");
return connectionFactory;
}
#Bean
public MessageListener msgListener() {
return new MessageListener() {
public void onMessage(Message message) {
System.out.println(message.toString());
if (message instanceof TextMessage) {
try {
String msg = ((TextMessage) message).getText();
System.out.println("Received message: " + msg);
// call web service here and depends on web service
// response
// if 200 then delete msg from queue else keep msg in
// queue
} catch (JMSException ex) {
throw new RuntimeException(ex);
}
}
}
};
}
#Bean
public MessageListenerContainer messageListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(jmsConnectionFactory());
container.setDestinationName("test");
container.setMessageListener(msgListener());
return container;
}
}
2nd Project:
package com.rabbitmq.jms.consumer.controller;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.jms.ConnectionFactory;
import org.json.JSONException;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.jms.JmsException;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.rabbitmq.jms.admin.RMQConnectionFactory;
import redis.clients.jedis.Jedis;
#Controller
public class ReceiverController {
#Autowired
JmsTemplate jmsTemplate;
#Bean
public ConnectionFactory jmsConnectionFactory() {
RMQConnectionFactory connectionFactory = new RMQConnectionFactory();
connectionFactory.setUsername("Username");
connectionFactory.setPassword("Password");
connectionFactory.setVirtualHost("vhostname");
connectionFactory.setHost("hostname");
return connectionFactory;
}
#CrossOrigin
#SuppressWarnings({ "unchecked", "rawtypes" })
#RequestMapping(method = RequestMethod.GET, value = "/getdata")
#ResponseBody
public ResponseEntity<String> fecthDataFromRedis()
throws JSONException, InterruptedException, JmsException, ExecutionException, TimeoutException {
System.out.println("in controller");
jmsTemplate.setReceiveTimeout(500L);
// jmsTemplate.
String message = (String) jmsTemplate.receiveAndConvert("test");
// call web service here and depends on web service
// response
// if 200 then delete msg from queue else keep msg in
// queue
System.out.println(message);
}
return new ResponseEntity(message , HttpStatus.OK);
}
}
How Can I do That?
Thanks In Advance.
You are not using a JmsTemplate, you are using a SimpleMessageListenerContainer to receive the message.
If you were using the template, you would have to use the execute method with a SessionCallback since the acknowledgement must occur within the scope of the session within which the message was received.
However, with the SimpleMessageListenerContainer, you simply set the sessionAcknowledgeMode to Session.CLIENT_ACKNOWLEDGE. See the container javadocs...
/**
* Message listener container that uses the plain JMS client API's
* {#code MessageConsumer.setMessageListener()} method to
* create concurrent MessageConsumers for the specified listeners.
*
* <p>This is the simplest form of a message listener container.
* It creates a fixed number of JMS Sessions to invoke the listener,
* not allowing for dynamic adaptation to runtime demands. Its main
* advantage is its low level of complexity and the minimum requirements
* on the JMS provider: Not even the ServerSessionPool facility is required.
*
* <p>See the {#link AbstractMessageListenerContainer} javadoc for details
* on acknowledge modes and transaction options. Note that this container
* exposes standard JMS behavior for the default "AUTO_ACKNOWLEDGE" mode:
* that is, automatic message acknowledgment after listener execution,
* with no redelivery in case of a user exception thrown but potential
* redelivery in case of the JVM dying during listener execution.
*
* <p>For a different style of MessageListener handling, through looped
* {#code MessageConsumer.receive()} calls that also allow for
* transactional reception of messages (registering them with XA transactions),
* see {#link DefaultMessageListenerContainer}.
...
EDIT
When using the JmsTemplate, you must do your work within the scope of the session - here's how...
First, you have to enable client acknowledge in your template...
this.jmsTemplate.setSessionAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);
Then, use the execute method with a SessionCallback ...
Boolean result = this.jmsTemplate.execute(session -> {
MessageConsumer consumer = session.createConsumer(
this.jmsTemplate.getDestinationResolver().resolveDestinationName(session, "bar", false));
String result = null;
try {
Message received = consumer.receive(5000);
if (received != null) {
result = (String) this.jmsTemplate.getMessageConverter().fromMessage(received);
// Do some stuff here.
received.acknowledge();
return true;
}
}
catch (Exception e) {
return false;
}
finally {
consumer.close();
}
}, true);

Retry mechanism for producer's client of ActiveMQ with JMS and spring

Is there a mechanism or example implementation of a retry mechanism/solution for a producer using ActiveMQ with JMS (more precisely, with JmsTemplate) and spring framework ?
My use case, which I want to handle is, when the broker is not available, for example, I want to make some number of retries, maximum 6 (if possible with exponential delays between each). So, I need also to track the number of retries for a message between each attempt.
I am aware the the redelivery policy for the consumer, but also I want to implement a reliable producer's client side as well
Thanks,
Simeon
i think that the easiest way is to use what exists for this by using an embedded broker with persistence enabled which must be used by the producer to send the messages to and by creating a Camel route to read from local Queue and forward to the remote one or by using a JmsBridgeConnector or NetworkConnector nut i think the JmsBridgeConnector is easier.
here is an Spring code example :
producer have to use jmsConnectionFactory() to create a ConnectionFactory
package com.example.amq;
import java.io.File;
import javax.jms.ConnectionFactory;
import javax.jms.QueueConnectionFactory;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.network.jms.JmsConnector;
import org.apache.activemq.network.jms.OutboundQueueBridge;
import org.apache.activemq.network.jms.ReconnectionPolicy;
import org.apache.activemq.network.jms.SimpleJmsQueueConnector;
import org.apache.activemq.store.PersistenceAdapter;
import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class ActiveMQConfiguration {
public static final String DESTINATION_NAME = "localQ";
#Bean // (initMethod = "start", destroyMethod = "stop")
public BrokerService broker() throws Exception {
final BrokerService broker = new BrokerService();
broker.addConnector("vm://localhost");
SimpleJmsQueueConnector simpleJmsQueueConnector = new SimpleJmsQueueConnector();
OutboundQueueBridge bridge = new OutboundQueueBridge();
bridge.setLocalQueueName(DESTINATION_NAME);
bridge.setOutboundQueueName("remoteQ");
OutboundQueueBridge[] outboundQueueBridges = new OutboundQueueBridge[] { bridge };
simpleJmsQueueConnector.getReconnectionPolicy().setMaxSendRetries(ReconnectionPolicy.INFINITE);
simpleJmsQueueConnector.setOutboundQueueBridges(outboundQueueBridges);
simpleJmsQueueConnector.setLocalQueueConnectionFactory((QueueConnectionFactory) jmsConnectionFactory());
simpleJmsQueueConnector.setOutboundQueueConnectionFactory(outboundQueueConnectionFactory());
JmsConnector[] jmsConnectors = new JmsConnector[] { simpleJmsQueueConnector };
broker.setJmsBridgeConnectors(jmsConnectors);
PersistenceAdapter persistenceAdapter = new KahaDBPersistenceAdapter();
File dir = new File(System.getProperty("user.home") + File.separator + "kaha");
if (!dir.exists()) {
dir.mkdirs();
}
persistenceAdapter.setDirectory(dir);
broker.setPersistenceAdapter(persistenceAdapter);
broker.setPersistent(true);
broker.setUseShutdownHook(false);
broker.setUseJmx(true);
return broker;
}
#Bean
public QueueConnectionFactory outboundQueueConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(
"auto://localhost:5671");
connectionFactory.setUserName("admin");
connectionFactory.setPassword("admin");
return connectionFactory;
}
#Bean
public ConnectionFactory jmsConnectionFactory() {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost");
connectionFactory.setObjectMessageSerializationDefered(true);
connectionFactory.setCopyMessageOnSend(false);
return connectionFactory;
}
}
By using Camel :
import org.apache.activemq.camel.component.ActiveMQComponent;
import org.apache.activemq.camel.component.ActiveMQConfiguration;
import org.apache.camel.CamelContext;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultCamelContext;
public class ActiveMQCamelBridge {
public static void main(String args[]) throws Exception {
CamelContext context = new DefaultCamelContext();
context.addComponent("inboundQueue", ActiveMQComponent.activeMQComponent("tcp://localhost:61616"));
ActiveMQComponent answer = ActiveMQComponent.activeMQComponent("tcp://localhost:5671");
if (answer.getConfiguration() instanceof ActiveMQConfiguration) {
((ActiveMQConfiguration) answer.getConfiguration()).setUserName("admin");
((ActiveMQConfiguration) answer.getConfiguration()).setPassword("admin");
}
context.addComponent("outboundQueue", answer);
context.addRoutes(new RouteBuilder() {
public void configure() {
from("inboundQueue:queue:localQ").to("outboundQueue:queue:remoteQ");
}
});
context.start();
Thread.sleep(60 * 5 * 1000);
context.stop();
}
}
Producer does not provide any kind of retry mechanism like consumer. You need to make sure in your code that message sent by producer acknowledge by broker.

Lightweight IPC to WebSocketListener in Jetty

Android, iOS and desktop browser clients are currently polling a PHP-backend (utilizing PostgreSQL database on CentOS Linux) every few seconds.
I would like to replace the polling by using standalone Jetty Websocket Server to notify clients, that new data is available for pickup at the backend.
So in the custom WebSocketListener I authenticate connected clients and store them in a ConcurrentHashMap<String,Session>:
public class MyListener implements WebSocketListener
{
private Session mSession;
#Override
public void onWebSocketConnect(Session session) {
mSession = session;
}
#Override
public void onWebSocketText(String message) {
if (mSession != null && mSession.isOpen()) {
// 1. validate client id and password
// 2. store client id and session into Map
}
}
My question: How to notify the connected (via websockets) clients?
I.e. in the PHP-scripts I would like to run a lightweight program java -jar MyNotify.jar client-1234 to tell the Jetty standalone server:
Hey, there is new data available for the client-1234 at the database!
Please send it a short message over websockets by calling
MyMap.get("client-1234").getRemote().sendString("hey", null);
You have to put your
ConcurrentHashMap<String,Session> sessionMap.
into public static field on custom javax.servlet.ServletContextEvent. Field should be initialized on event
#Override
public void contextInitialized(ServletContextEvent ctx) {
Then anywhere in you app, you can access this static field in normal way (using dot syntax).
Because contextInitialized is fired before any servlets or websockets methods (get, put, onMessage), map will be there. Also being concurrent map, it should have no duplicate id's inside.
Of course, you need also strategy for cleaning up the session map. To sum up, you have to build up your system together with events from javax.servlet API.
Similar example:
package example;
import java.io.FileNotFoundException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.*;
/**
* Application lifecycle events. Handles:
* <ul>
* <li>start, shutdown of application
* <li>start, stop of session
* </ul>
*
* #author mitjag
*
*/
public class AppInit implements HttpSessionListener, ServletContextListener {
public static final Logger log = Logger.getLogger(AppInit.class.getCanonicalName());
public static final Map<String, HttpSession> SESSION_MAP = new ConcurrentHashMap<String, HttpSession>(); /* access AppInit.SESSION_MAP from anywhere in your app*/
#Override
public void contextInitialized(ServletContextEvent ctx) {}
#Override
public void sessionCreated(HttpSessionEvent arg0) {
// With this trick we maintain the list of sessionid's together with corresponding session
// It is used to grab the session if you have the valid session id
final String sid = arg0.getSession().getId();
log.info("SESSION CREATED with id " + arg0.getSession().getId());
SESSION_MAP.put(sid, arg0.getSession());
}
/**
* Called on session invalidation (manual or session timeout trigger, defined in web.xml (session-timeout)).
* #see javax.servlet.http.HttpSessionListener#sessionDestroyed(javax.servlet.http.HttpSessionEvent)
*/
#Override
public void sessionDestroyed(HttpSessionEvent arg0) {
// remove session from our list (see method: sessionCreated)
final String sid = arg0.getSession().getId();
SESSION_MAP.remove(sid);
}
#Override
public void contextDestroyed(ServletContextEvent arg0) {
}
}

Resources