1 /***
2 * Redistribution and use of this software and associated documentation
3 * ("Software"), with or without modification, are permitted provided
4 * that the following conditions are met:
5 *
6 * 1. Redistributions of source code must retain copyright
7 * statements and notices. Redistributions must also contain a
8 * copy of this document.
9 *
10 * 2. Redistributions in binary form must reproduce the
11 * above copyright notice, this list of conditions and the
12 * following disclaimer in the documentation and/or other
13 * materials provided with the distribution.
14 *
15 * 3. The name "Exolab" must not be used to endorse or promote
16 * products derived from this Software without prior written
17 * permission of Exoffice Technologies. For written permission,
18 * please contact info@exolab.org.
19 *
20 * 4. Products derived from this Software may not be called "Exolab"
21 * nor may "Exolab" appear in their names without prior written
22 * permission of Exoffice Technologies. Exolab is a registered
23 * trademark of Exoffice Technologies.
24 *
25 * 5. Due credit should be given to the Exolab Project
26 * (http://www.exolab.org/).
27 *
28 * THIS SOFTWARE IS PROVIDED BY EXOFFICE TECHNOLOGIES AND CONTRIBUTORS
29 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
30 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
31 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
32 * EXOFFICE TECHNOLOGIES OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
33 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
34 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
35 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
36 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
37 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
38 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
39 * OF THE POSSIBILITY OF SUCH DAMAGE.
40 *
41 * Copyright 2000-2003 (C) Exoffice Technologies Inc. All Rights Reserved.
42 */
43 package org.exolab.jms.messagemgr;
44
45 import java.sql.Connection;
46 import java.sql.SQLException;
47 import java.util.Date;
48 import java.util.HashMap;
49 import java.util.Iterator;
50
51 import javax.jms.JMSException;
52 import javax.jms.Message;
53 import javax.transaction.TransactionManager;
54
55 import org.apache.commons.logging.Log;
56 import org.apache.commons.logging.LogFactory;
57
58 import org.exolab.core.service.BasicService;
59 import org.exolab.core.service.ServiceException;
60 import org.exolab.jms.client.JmsDestination;
61 import org.exolab.jms.client.JmsQueue;
62 import org.exolab.jms.client.JmsTopic;
63 import org.exolab.jms.config.AdministeredDestinations;
64 import org.exolab.jms.config.AdministeredQueue;
65 import org.exolab.jms.config.AdministeredTopic;
66 import org.exolab.jms.config.Configuration;
67 import org.exolab.jms.config.DatabaseConfiguration;
68 import org.exolab.jms.config.MessageManagerConfiguration;
69 import org.exolab.jms.config.Subscriber;
70 import org.exolab.jms.message.MessageHandle;
71 import org.exolab.jms.message.MessageImpl;
72 import org.exolab.jms.persistence.DatabaseService;
73 import org.exolab.jms.persistence.PersistenceException;
74
75
76 /***
77 * This is the active message handling component within the JMS server.
78 * Messages are passed in and added to the appropriate dispatchers for delivery
79 * to the clients.
80 *
81 * @version $Revision: 1.93 $ $Date: 2003/08/17 01:32:24 $
82 * @author <a href="mailto:mourikis@intalio.com">Jim Mourikis</a>
83 * @author <a href="mailto:tma@netspace.net.au">Tim Anderson</a>
84 */
85 public class MessageMgr extends BasicService {
86
87 /***
88 * The service name of the message manager
89 */
90 private final static String MM_SERVICE_NAME = "MessageManager";
91
92 /***
93 * Caches the singleton instance of the message manager.
94 */
95 private static MessageMgr _instance;
96
97 /***
98 * used to synchronise the singleton construction
99 */
100 private static final Object _block = new Object();
101
102 /***
103 * Maintain a list of registered MessageManagerEventListener objects, that
104 * get notified when certain events occur in the MessageManager
105 */
106 private transient HashMap _listeners = new HashMap(1023);
107
108 /***
109 * The sequence number generator is used to differentiate messages arriving
110 * on the same millisecond
111 */
112 private long _sequenceNumberGenerator = 0;
113
114 /***
115 * This is the maximum size the cache can reach before we are forced to
116 * run garbage collection. Garbage collection will also execute in the
117 * background periodically to remove processed messages from the cache.
118 */
119 private int _maximumSize = 2500;
120
121 /***
122 * Tracks the number of messages processed
123 */
124 private long _messagesProcessed;
125
126 /***
127 * The logger
128 */
129 private static final Log _log = LogFactory.getLog(MessageMgr.class);
130
131
132 /***
133 * Create and return an instance of the singleton. If the singleton already
134 * exists then simply return it. If there is a problem creating the
135 * singleton then throw an exception
136 *
137 * @return MessageMgr - the singleton instance
138 * @throws MessageMgrException
139 */
140 public static MessageMgr createInstance() throws MessageMgrException {
141 if (_instance == null) {
142 synchronized (_block) {
143 if (_instance == null) {
144 _instance = new MessageMgr();
145 }
146 }
147 }
148 return _instance;
149 }
150
151 /***
152 * Return an instance to the MessageMgr singleton. This method assumes that
153 * the singleton has already been created with a call to
154 * {@link #createInstance}
155 *
156 * @return MessageMgr
157 */
158 public static MessageMgr instance() {
159 return _instance;
160 }
161
162 /***
163 * The constructor will register itself with the garbage collection
164 * service.
165 *
166 * @throws MessageMgrException - if there are problems during construction
167 */
168 private MessageMgr() throws MessageMgrException {
169 super(MM_SERVICE_NAME);
170 }
171
172 // ovverride BasicService.start
173 public void start() throws ServiceException {
174 try {
175 DestinationManager.createInstance();
176 ConsumerManager.createInstance();
177 } catch (ServiceException exception) {
178 throw exception;
179 } catch (Exception exception) {
180 String msg = "Failed to start MessageMgr";
181 _log.error(msg, exception);
182 throw new ServiceException(msg + ":" + exception);
183 }
184 }
185
186 // implement BasicService.run
187 public void run() {
188 // do nothing
189 }
190
191 // override BasicService.stop
192 public synchronized void stop() throws ServiceException {
193 try {
194 // destroy the consumer manager.
195 ConsumerManager.instance().destroy();
196
197 // destroy the destination manager.
198 DestinationManager.instance().destroy();
199
200 // clear state
201 _listeners.clear();
202 } catch (Exception error) {
203 error.printStackTrace();
204 throw new ServiceException("Failed to stop MessageMgr : " +
205 error.toString());
206 }
207
208 // clear the static reference
209 synchronized (_block) {
210 _instance = null;
211
212 }
213 }
214
215 /***
216 * Create the specified destination. The destination is a container
217 * for messages and consumers. Consumers listen for messages posted on a
218 * particular destination.
219 * <p>
220 * This can be called multiple times without any side effects. If the
221 * destination is null then it throws a JMSException
222 *
223 * @param destination - create this destination
224 * @throws JMSException - if the params is null
225 */
226 public void addDestination(JmsDestination destination)
227 throws JMSException {
228
229 // check the methods preconditions
230 if (destination == null) {
231 throw new JMSException("Call to addDestination with null object");
232 }
233
234 DestinationManager.instance().createDestinationCache(destination);
235 }
236
237 /***
238 * Remove this destination and all attached consumers. If the destination
239 * is null then throw an exception.
240 *
241 * @param destination - the destination to remove
242 * @throws JMSException
243 */
244 public void removeDestination(JmsDestination destination)
245 throws JMSException {
246
247 // check the method's preconditions
248 if (destination == null) {
249 throw new JMSException("Call to removeDestination with null object");
250 }
251
252 DestinationManager.instance().destroyDestinationCache(destination);
253 }
254
255 /***
256 * Return true if the specified destination exists.
257 *
258 * @param destination - destination to check
259 * @return boolean - true if a it exists
260 */
261 public boolean exists(JmsDestination destination) {
262 return DestinationManager.instance().hasDestinationCache(destination);
263 }
264
265 /***
266 * Add a message to the message manager for the specified destination.
267 * If the message or the destination are null then throw a JMSException
268 * <p>
269 * If the destination, specified in the message, does not exist then
270 * create it.
271 * destinations
272 *
273 * @param message the message to add
274 * @throws JMSException if the message cannot be added
275 */
276 public void add(MessageImpl message) throws JMSException {
277 if (message != null) {
278 // if the message is persistent then process it accordingly,
279 // otherwise use the non-persistent quality of service
280 if (message.isPersistent()) {
281 addPersistentMessage(message);
282 } else {
283 addNonPersistentMessage(message);
284 }
285 } else {
286 _log.error("Cannot process a null message.");
287 }
288 }
289
290 /***
291 * Add a message to the message manager for the specified destination.
292 * Note that this method is called exclusively by the
293 * {@link ResourceManager} and should not be used for any other purpose.
294 *
295 * @param connection - this is the database connection that is used
296 * @param message - the message to add
297 * @throws JMSException - thrown if there is a problem processing msg
298 */
299 void add(Connection connection, MessageImpl message) throws JMSException {
300 if (message != null) {
301 // if the message is persistent then process it accordingly,
302 // otherwise use the non-persistent quality of service
303 if (message.isPersistent()) {
304 addPersistentMessage(connection, message);
305 } else {
306 addNonPersistentMessage(message);
307 }
308 } else {
309 _log.error("Cannot process a null message.");
310 }
311 }
312
313 /***
314 * This method is used to process non-persistent messages.
315 *
316 * @param message - the message to process
317 * @throws JMSException - if the message cannot be processed
318 */
319 protected void addNonPersistentMessage(MessageImpl message)
320 throws JMSException {
321 // mark the message as accepted and attach a sequence number
322 message.setAcceptedTime((new Date()).getTime());
323 message.setSequenceNumber(++_sequenceNumberGenerator);
324 message.setReadOnly(true);
325
326 // Use the message to retrieve the corresponding destination object.
327 JmsDestination destination =
328 (JmsDestination) message.getJMSDestination();
329 if (destination != null) {
330 // notify all registered listeners that a new message has arrived
331 // for the specified destination.
332 notifyOnAddMessage(destination, message);
333 _messagesProcessed++;
334 } else {
335 _log.error("Can't locate destination for message");
336 }
337 }
338
339 /***
340 * This method is used to process persistent messages.
341 *
342 * @param message - the message to process
343 * @throws JMSException - if the message cannot be processed
344 */
345 protected void addPersistentMessage(MessageImpl message)
346 throws JMSException {
347
348 // mark the message as accepted and attach a sequence number
349 message.setAcceptedTime((new Date()).getTime());
350 message.setSequenceNumber(++_sequenceNumberGenerator);
351 message.setReadOnly(true);
352
353 JmsDestination destination =
354 (JmsDestination) message.getJMSDestination();
355 if (destination == null) {
356 throw new JMSException(
357 "Can't process message - JMSDestination is null");
358 }
359
360 Connection connection = null;
361 TransactionManager tm = null;
362
363 // do all persistent work in this block
364 try {
365 // get a database connection
366 connection = DatabaseService.getConnection();
367
368 // add the message to the database
369 DatabaseService.getAdapter().addMessage(connection, message);
370
371 if (destination instanceof JmsTopic) {
372 // let the consumer manager handle this
373 ConsumerManager.instance().persistentMessageAdded(
374 connection, message);
375 } else {
376 // if this message is for a queue then simply create a
377 // persistent handle for the destination
378 MessageHandleFactory.createPersistentHandle(
379 connection, destination, null, message);
380 }
381
382 // commit the persistent message and handle
383 connection.commit();
384
385 // Notify all listeners that a persistent message has arrived
386 notifyOnAddPersistentMessage(null, destination, message);
387 _messagesProcessed++;
388 } catch (Exception exception) {
389 if (connection != null) {
390 try {
391 connection.rollback();
392 } catch (SQLException ignore) {
393 // no-op
394 }
395 }
396 _log.error("Failed to make message persistent", exception);
397 throw new JMSException(
398 "Failed to make message persistent: " +
399 exception.toString());
400 } finally {
401 if (connection != null) {
402 try {
403 connection.close();
404 } catch (Exception ignore) {
405 // no-op
406 }
407 }
408 }
409 }
410
411 /***
412 * This method is used to process persistent messages published through
413 * the resource manager.
414 *
415 * @param connection - the database connection to use.
416 * @param message - the message to process
417 * @throws JMSException - if the message cannot be processed
418 */
419 protected void addPersistentMessage(Connection connection,
420 MessageImpl message)
421 throws JMSException {
422
423 // Use the message to retrieve the corresponding destination object.
424 // This method will create the object if one does not already exist.
425 JmsDestination destination = (JmsDestination) message.getJMSDestination();
426 if (destination != null) {
427 try {
428 // notify all listeensers that a persistent message has arrived
429 notifyOnAddPersistentMessage(connection, destination, message);
430 _messagesProcessed++;
431 } catch (PersistenceException exception) {
432 throw new JMSException("Failed in addPersistentMessage : " +
433 exception.toString());
434 } catch (Exception exception) {
435 throw new JMSException("Failed in addPersistentMessage : " +
436 exception.toString());
437 }
438 } else {
439 // shouldn't really get here, since the message should have been
440 // checked and prepared before passed to this routine.
441 _log.error("Can't locate destination for message");
442 }
443 }
444
445 /***
446 * Return the message given the specified message handle.
447 * This will delegate to the appropriate {@link DestinationCache} or
448 * {@link ConsumerManager}
449 *
450 * @param handle - the handle
451 * @return MessageImpl - the associated message or null
452 */
453 MessageImpl getMessage(MessageHandle handle) {
454 // precondition; ensure that the handle is not null
455 if (handle == null) {
456 return null;
457 }
458
459 MessageImpl message = null;
460 if (handle.getDestination() instanceof JmsTopic) {
461 // is for a topic so check the consumer endpoint
462 // cache
463 TopicConsumerEndpoint endpoint = (TopicConsumerEndpoint)
464 ConsumerManager.instance().getConsumerEndpoint(
465 handle.getConsumerName());
466 if (endpoint != null) {
467 message = endpoint.getMessage(handle);
468 }
469 } else {
470 // must be for a queue so check the destination cache
471 DestinationCache cache =
472 DestinationManager.instance().getDestinationCache(
473 handle.getDestination());
474 if (cache != null) {
475 message = cache.getMessage(handle);
476 }
477 }
478
479 return message;
480 }
481
482 /***
483 * This method prepares the message without actually passing it through the
484 * system. It is used by the {@link ResourceManager} to process incoming
485 * messages.
486 * <p>
487 * If there are any issues with the message the method will throw an
488 * exception
489 *
490 * @param message - the message
491 * @throws JMSException - if the message is invalid or cannot be prep'ed
492 */
493 void checkAndPrepareMessage(MessageImpl message)
494 throws JMSException {
495 if (message != null) {
496 // mark the message as accepted and attach a sequence number
497 message.setAcceptedTime((new Date()).getTime());
498 message.setSequenceNumber(++_sequenceNumberGenerator);
499 message.setReadOnly(true);
500
501 if (message.getJMSDestination() == null) {
502 throw new JMSException("Null destination specified in message");
503 }
504 } else {
505 throw new JMSException("checkAndPrepareMessage failed for null message");
506 }
507 }
508
509 /***
510 * Returns true if there are any messages for the specified consumer
511 *
512 * @param consumer - the consumer to check
513 * @return boolean - true if messages are queued
514 * @throws JMSException - if the consumer can't be checked
515 */
516 public boolean hasMessages(ConsumerEndpoint consumer) throws JMSException {
517 if (consumer == null) {
518 throw new JMSException(
519 "Can't call hasMessages with null consumer");
520 }
521 return (consumer.getMessageCount() > 0);
522 }
523
524 /***
525 * Returns a list of active destinations
526 *
527 * @return List a list of JmsDestination objects
528 */
529 public Iterator getDestinations() {
530 return DestinationManager.instance().destinations();
531 }
532
533 /***
534 * Returns an iterator of active consumers registered to a given
535 * destination
536 *
537 * @return Iterator - iterator of {@link ConsumerEndpoint} objects.
538 * @throws JMSException
539 */
540 public Iterator getConsumers(JmsDestination destination)
541 throws JMSException {
542 //check to see that the destination is not null
543 if (destination == null) {
544 throw new JMSException("destination is null in getConsumer");
545 }
546
547 DestinationCache dest =
548 DestinationManager.instance().getDestinationCache(destination);
549
550 return (dest == null) ? null : dest.getConsumers();
551 }
552
553 /***
554 * Resolves a destination given its name
555 *
556 * @param name the name of the destination
557 * @return JmsDestination if an active destination exists for
558 * the given name, else it returns
559 * <tt>null</tt>
560 */
561 public JmsDestination resolve(String name) {
562 return DestinationManager.instance().destinationFromString(name);
563 }
564
565 /***
566 * Resolves a consumer given its destination and an identity. Should look
567 * removing t from here.
568 *
569 * @param destination the destination
570 * @param name the name of the consumer
571 * @return ConsumerIfc if an active consumer exists for
572 * the given name, else it returns
573 * <tt>null</tt>
574 */
575 public ConsumerEndpoint resolveConsumer(JmsDestination destination,
576 String id) {
577 return ConsumerManager.instance().getConsumerEndpoint(id);
578 }
579
580 /***
581 * Stop/start a consumer. When stopped, the consumer will not receive
582 * messages until the consumer is re-started.
583 * This is invoked when the underlying connection is stopped or started
584 *
585 * @param consumer the consumer to stop/start
586 * @param stop when <tt>true</tt> stop the consumer
587 * else start it.
588 */
589 public void setStopped(ConsumerEndpoint consumer, boolean stop)
590 throws JMSException {
591 // need to implement this for the consumer
592 }
593
594 /***
595 * Add a message listener for a specific destination to be informed
596 * when messages, for the destination are added or removed from the
597 * queue. More than one listener can be registered per desitnation
598 * and the same listener can be registered for multiple destinations.
599 * <p>
600 * If a listener is already registered for a particuler destination
601 * then it fails silently.
602 *
603 * @param destination - what messgaes to listen for
604 * @param listener - a JmsMessageListener instance
605 */
606 public void addEventListener(JmsDestination destination,
607 MessageManagerEventListener listener) {
608
609 if ((destination != null) &&
610 (listener != null)) {
611 synchronized (_listeners) {
612 if (!_listeners.containsKey(destination)) {
613 _listeners.put(destination, listener);
614 }
615 }
616 }
617 }
618
619 /***
620 * Remove the listener for the specified destination. If one is not
621 * registered then ignore it.
622 *
623 * @param destination - destination that it listens for
624 * @param listener - listener for that destination.
625 */
626 public void removeEventListener(JmsDestination destination,
627 MessageManagerEventListener listener) {
628 if ((destination != null) &&
629 (listener != null)) {
630 synchronized (_listeners) {
631 if (_listeners.containsKey(destination)) {
632 _listeners.remove(destination);
633 }
634 }
635 }
636 }
637
638 /***
639 * Notify the listeners, registered for the destination that a message has
640 * been added to the message manager.
641 * <p>
642 * All errors are propagated as JMSException exceptions
643 *
644 * @param destination - destination for which message exits
645 * @param message - message that was added
646 * @return boolean - true if the message was processed
647 * @throws JMSException - for any processing error
648 */
649 boolean notifyOnAddMessage(JmsDestination destination,
650 MessageImpl message) throws JMSException {
651 boolean result = false;
652 MessageManagerEventListener listener =
653 (MessageManagerEventListener) _listeners.get(destination);
654
655 if (listener != null) {
656 // if there is a registered destination cache then let the cache
657 // process it.
658 result = listener.messageAdded(destination, message);
659 } else {
660 // let the {@link DestinationManager handle the message
661 result = DestinationManager.instance().messageAdded(destination,
662 message);
663 }
664
665 return result;
666 }
667
668 /***
669 * Notify the listeners, registered for the destination that a message has
670 * been removed from the message manager. There maybe several reason why
671 * this has happened (i.e the message has expired, message has been
672 * purged, message has been consumed etc).
673 *
674 * @param destination - destination for which message exits
675 * @param message - message that was removed
676 * @throws JMSException for any processing error
677 */
678 void notifyOnRemoveMessage(JmsDestination destination,
679 MessageImpl message) throws JMSException {
680
681 MessageManagerEventListener listener =
682 (MessageManagerEventListener) _listeners.get(destination);
683
684 if (listener != null) {
685 // send the notification to the active listener
686 listener.messageRemoved(destination, message);
687 } else {
688 // there is not active listener, send it to the Destination
689 // Manager
690 DestinationManager.instance().messageRemoved(destination, message);
691 }
692 }
693
694 /***
695 * Notify the listeners, registered for the destination that a persistent
696 * message has been added to the message manager.
697 *
698 * @param connection - the database connection to use.
699 * @param destination - destination for which message exits
700 * @param message - message that was added
701 * @return boolean - true if the message was processed
702 * @throws JMSException - is a processing error occured
703 * @throws PersistenceException - if a persistence error occured
704 */
705 boolean notifyOnAddPersistentMessage(Connection connection,
706 JmsDestination destination,
707 MessageImpl message)
708 throws JMSException, PersistenceException {
709
710 boolean result = false;
711 MessageManagerEventListener listener =
712 (MessageManagerEventListener) _listeners.get(destination);
713
714 if (listener != null) {
715 // if there is a registered destination cache then let the cache
716 // process it.
717 result = listener.persistentMessageAdded(connection,
718 destination, message);
719 } else {
720 // let the {@link DestinationManager} handle the message
721 result = DestinationManager.instance().persistentMessageAdded(
722 connection, destination, message);
723 }
724
725 return result;
726 }
727
728 /***
729 * Notify the listeners, registered for the destination that a persistent
730 * message has been removed from the message manager. There maybe several
731 * reason why this has happened (i.e the message has expired, message has
732 * been purged, message has been consumed etc).
733 *
734 * @param connection - the database connection to use
735 * @param destination - destination for which message exits
736 * @param message - message that was removed
737 * @throws JMSException - for any processing problem
738 * @throws PersistenceException - for any persistence related problem
739 */
740 void notifyOnRemovePersistentMessage(Connection connection,
741 JmsDestination destination,
742 MessageImpl message)
743 throws JMSException, PersistenceException {
744
745 MessageManagerEventListener listener =
746 (MessageManagerEventListener) _listeners.get(destination);
747
748 if (listener != null) {
749 // send the notification to the active listener
750 listener.persistentMessageRemoved(connection, destination,
751 message);
752 } else {
753 // there is not active listener, send it to the Destination
754 // Manager
755 DestinationManager.instance().persistentMessageRemoved(connection,
756 destination, message);
757 }
758 }
759
760 /***
761 * Return the maximum size of the cache
762 *
763 * @return int - maximum size of cache
764 */
765 public int getMaximumSize() {
766 return _maximumSize;
767 }
768
769 /***
770 * Notify the destruction of a handle.
771 * <p>
772 * If the handle has been destroyed then we need to do the following
773 * 1. if the handle is for a queue then we can remove the message
774 * from the cache
775 * 2. if the handle is for a topic then we need to see whether we can
776 * garbage collect it
777 *
778 * @param handle a TransientMessageHandle
779 */
780 public void handleDestroyed(MessageHandle handle) {
781
782 // precondition: handle != null
783 if (handle == null) {
784 return;
785 }
786
787 if (handle.getDestination() instanceof JmsTopic) {
788 TopicConsumerEndpoint endpoint = (TopicConsumerEndpoint)
789 ConsumerManager.instance().getConsumerEndpoint(
790 handle.getConsumerName());
791
792 if (endpoint != null) {
793 endpoint.deleteMessage(handle);
794 }
795 } else {
796 DestinationCache cache =
797 DestinationManager.instance().getDestinationCache(
798 handle.getDestination());
799
800 if (cache != null) {
801 cache.deleteMessage(handle);
802 }
803 }
804 }
805 }
This page was automatically generated by Maven