/* Copyright 2011-2014 Red Hat, Inc This file is part of PressGang CCMS. PressGang CCMS is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. PressGang CCMS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with PressGang CCMS. If not, see <http://www.gnu.org/licenses/>. */ package org.jboss.pressgang.ccms.server.messaging; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.annotation.Resource; import javax.ejb.SessionContext; import javax.ejb.Singleton; import javax.ejb.Startup; import javax.ejb.Timeout; import javax.ejb.Timer; import javax.ejb.TimerConfig; import javax.jms.Connection; import javax.jms.ConnectionFactory; import javax.jms.IllegalStateException; import javax.jms.JMSException; import javax.jms.MessageProducer; import javax.jms.ObjectMessage; import javax.jms.Session; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.Hashtable; import java.util.List; import org.jboss.pressgang.ccms.filter.utils.EntityUtilities; import org.jboss.pressgang.ccms.model.Topic; import org.jboss.pressgang.ccms.model.config.ApplicationConfig; import org.jboss.pressgang.ccms.model.contentspec.ContentSpec; import org.jboss.pressgang.ccms.server.utils.ResourceProducer; import org.jboss.pressgang.ccms.utils.common.CollectionUtilities; /** * This EJB will poll the database periodically looking for changes that can be broadcast via the message * queues. We do this with a periodic database query rather than intercepting changes through the REST * interface in order to support clustered environments where changes may be made on a different server, * and then replicated to this server after some undefined period of time. By using a periodic query we * are assured that the changes we are broadcasting are actually available on this server, and we avoid * having to sync broadcast messages with database replication cycles. * * This code assumes the presence of two JMS topics: * * <jms-destinations> * <jms-topic name="UpdatedTopic"> * <entry name="java:jboss/topics/updatedtopic"/> * </jms-topic> * <jms-topic name="UpdatedSpec"> * <entry name="java:jboss/topics/updatedspec"/> * </jms-topic> * </jms-destinations> */ @Singleton @Startup public class UpdatedEntities { private static final String EJB_REFRESH = "*/10"; private static final String INITIAL_CONTEXT_FACTORY = "org.jboss.as.naming.InitialContextFactory"; private static final String URL_PKG_PREFIXES = "org.jboss.naming:org.jnp.interfaces"; private static final String PROVIDER_URL = "jnp://localhost:1099"; private static final String CONNECTION_FACTORY = "/ConnectionFactory"; /** * JNDI name for the JMS topic that will be notified of updated topics */ private static final String TOPIC_UPDATE_QUEUE = "java:jboss/topics/updatedtopic"; /** * JNDI name for the JMS topic that will be notified of updated content specs */ private static final String SPEC_UPDATE_QUEUE = "java:jboss/topics/updatedspec"; private static final String SERVER_RESTART = "SERVER_RESTART"; /** * How many times to retry opening a connection and resending a message */ private static final int RETRIES = 1; private final Hashtable<String, String> env = new Hashtable<String, String>(); /** * The last highest revision that we checked for updates against */ private Integer lastSpecRevision = null; private Integer lastTopicRevision = null; private Context ctx = null; private ConnectionFactory cf; private Connection connection; private Session session; @PersistenceContext(unitName = ResourceProducer.PERSISTENCE_UNIT_NAME) protected EntityManager entityManager; @Resource private SessionContext context; @Timeout public void onTimeout(final Timer timer) { final Integer thisLatestRevision = getLatestRevision(); checkForUpdatedTopics(timer, thisLatestRevision); checkForUpdatedSpecs(timer, thisLatestRevision); createNewTimer(); } protected Integer getLatestRevision() { return (Integer) entityManager.createQuery("SELECT MAX(id) FROM LoggingRevisionEntity").getSingleResult(); } private void checkForUpdatedTopics(final Timer timer, final Integer thisLatestRevision) { if (lastTopicRevision != null) { try { final List<Integer> topics = EntityUtilities.getEditedEntitiesByRevision(entityManager, Topic.class, "topicId", lastTopicRevision, null); if (topics.size() != 0) { sendMessage(TOPIC_UPDATE_QUEUE, CollectionUtilities.toSeperatedString(topics)); } lastTopicRevision = thisLatestRevision + 1; } catch (final Exception ex) { // the message could not be sent. it will be retried as lastTopicRevision was not updated ex.printStackTrace(); } } else { lastTopicRevision = thisLatestRevision + 1; } } public void checkForUpdatedSpecs(final Timer timer, final Integer thisLatestRevision) { if (lastSpecRevision != null) { try { final List<Integer> specs = EntityUtilities.getEditedEntitiesByRevision(entityManager, ContentSpec.class, "contentSpecId", lastSpecRevision, null); if (specs.size() != 0) { sendMessage(SPEC_UPDATE_QUEUE, CollectionUtilities.toSeperatedString(specs)); } lastSpecRevision = thisLatestRevision + 1; } catch (final Exception ex) { // the message could not be sent. it will be retried as lastSpecRevision was not updated ex.printStackTrace(); } } else { lastSpecRevision = thisLatestRevision + 1; } } /** * Here we create a single action timer that reads the JMS Update Frequency value from the * application config and schedules the next refresh. This allows us to define the refresh * frequency as part of the config, and alter it at runtime. */ private void createNewTimer() { final TimerConfig config = new TimerConfig(); config.setPersistent(false); config.setInfo(ApplicationConfig.getInstance().getJmsUpdateFrequency() * 1000); context.getTimerService().createSingleActionTimer( ApplicationConfig.getInstance().getJmsUpdateFrequency() * 1000, config); } @PostConstruct private void sendStartupMessage() { setup(); try { sendMessage(TOPIC_UPDATE_QUEUE, SERVER_RESTART); sendMessage(SPEC_UPDATE_QUEUE, SERVER_RESTART); } catch (final Exception ex) { // the message could not be sent. ex.printStackTrace(); } } /** * Connect to the JMS subsystem */ private void setup() { try { env.put(Context.PROVIDER_URL, PROVIDER_URL); env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY); env.put(Context.URL_PKG_PREFIXES, URL_PKG_PREFIXES); ctx = new InitialContext(env); cf = (ConnectionFactory)ctx.lookup(CONNECTION_FACTORY); connection = cf.createConnection(); session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); connection.start(); } catch (final Exception ex) { ex.printStackTrace(); ctx = null; cf = null; connection = null; session = null; } createNewTimer(); } /** * Close the connection to the JMS subsystem */ @PreDestroy private void shutdown() { try { if (connection != null) { connection.close(); } } catch (final Exception ex) { } finally { connection = null; } } private void sendMessage(final String topicName, final String jmsMessage) throws NamingException, JMSException { sendMessage(topicName, jmsMessage, 0); } /** * Send a message to a JMS topic * @param topicName The JNDI name of the JMS topic * @param jmsMessage The message to be sent * @throws NamingException * @throws JMSException */ private void sendMessage(final String topicName, final String jmsMessage, final int retries) throws NamingException, JMSException { try { if (retries < RETRIES && ctx != null && session != null) { // Lookup the JMS queue final javax.jms.Topic topic = (javax.jms.Topic) ctx.lookup(topicName); // Create a JMS Message Producer to send a message on the queue final MessageProducer producer = session.createProducer(topic); // Create a Text Message and send it using the producer. The HornetQ REST server requires the use // of Object messages. final ObjectMessage message = session.createObjectMessage(jmsMessage); producer.send(message); } } catch (final IllegalStateException ex) { /* If the session is closed (and this can happen without calling shutdown()) we'll get the following error: javax.jms.IllegalStateException: HQ119019: Session is closed in this case, shutdown, startup and retry. */ shutdown(); setup(); sendMessage(topicName, jmsMessage, retries + 1); } } }