├── .ruby-version ├── .gitignore ├── lib ├── jms │ ├── version.rb │ ├── object_message.rb │ ├── text_message.rb │ ├── queue_browser.rb │ ├── imports.rb │ ├── bytes_message.rb │ ├── oracle_a_q_connection_factory.rb │ ├── message_producer.rb │ ├── mq_workaround.rb │ ├── map_message.rb │ ├── message_listener_impl.rb │ ├── message.rb │ ├── message_consumer.rb │ ├── session_pool.rb │ ├── session.rb │ └── connection.rb └── jms.rb ├── Gemfile ├── examples ├── producer-consumer │ ├── consumer.rb │ ├── browser.rb │ ├── producer.rb │ └── consumer_async.rb ├── publish-subscribe │ ├── publish.rb │ └── subscribe.rb ├── performance │ ├── consumer.rb │ └── producer.rb ├── client-server │ ├── replier.rb │ └── requestor.rb ├── advanced │ └── session_pool.rb ├── file-to-q │ ├── q_to_files.rb │ └── files_to_q.rb ├── invm │ ├── invm.rb │ └── log4j.properties └── jms.yml ├── Gemfile.lock ├── Rakefile ├── jruby-jms.gemspec ├── test ├── log4j.properties ├── test_helper.rb ├── session_pool_test.rb ├── connection_test.rb ├── session_test.rb ├── message_test.rb └── jms.yml ├── HISTORY.md ├── README.md └── LICENSE.txt /.ruby-version: -------------------------------------------------------------------------------- 1 | jruby 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem 3 | .idea/* 4 | *.log 5 | -------------------------------------------------------------------------------- /lib/jms/version.rb: -------------------------------------------------------------------------------- 1 | module JMS #:nodoc 2 | VERSION = '1.3.0' 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | gem 'minitest' 7 | gem 'minitest-reporters' 8 | -------------------------------------------------------------------------------- /lib/jms/object_message.rb: -------------------------------------------------------------------------------- 1 | # Interface javax.jms.ObjectMessage 2 | module JMS::ObjectMessage 3 | def data 4 | getObject 5 | end 6 | 7 | def data(val) 8 | setObject(val) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/jms/text_message.rb: -------------------------------------------------------------------------------- 1 | #Interface javax.jms.TextMessage 2 | module JMS::TextMessage 3 | def data 4 | getText 5 | end 6 | 7 | def data=(val) 8 | setText(val.to_s) 9 | end 10 | 11 | def to_s 12 | data 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /lib/jms.rb: -------------------------------------------------------------------------------- 1 | require 'java' 2 | require 'jms/version' 3 | require 'jms/connection' 4 | require 'semantic_logger' 5 | 6 | module JMS 7 | # Add Logging capabilities 8 | include SemanticLogger::Loggable 9 | 10 | autoload :SessionPool, 'jms/session_pool' 11 | end 12 | -------------------------------------------------------------------------------- /lib/jms/queue_browser.rb: -------------------------------------------------------------------------------- 1 | # Interface javax.jms.QueueBrowser 2 | module JMS::QueueBrowser 3 | # For each message on the queue call the supplied Proc 4 | def each(params={}, &block) 5 | raise(ArgumentError, 'JMS::QueueBrowser::each requires a code block to be executed for each message received') unless block 6 | 7 | e = self.getEnumeration 8 | while e.hasMoreElements 9 | block.call(e.nextElement) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/producer-consumer/consumer.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Consumer: 3 | # Retrieve all messages from a queue 4 | # 5 | require 'jms' 6 | require 'yaml' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | # Consume all available messages on the queue 15 | JMS::Connection.session(config) do |session| 16 | session.consume(:queue_name => 'ExampleQueue', :timeout => 1000) do |message| 17 | JMS.logger.info message.inspect 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/producer-consumer/browser.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Browsing Consumer: 3 | # Browse all messages on a queue without removing them 4 | # 5 | require 'jms' 6 | require 'yaml' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | # Consume all available messages on the queue 15 | JMS::Connection.session(config) do |session| 16 | session.browse(queue_name: 'ExampleQueue', timeout: 1000) do |message| 17 | JMS.logger.info message.inspect 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/publish-subscribe/publish.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Publisher: 3 | # Write messages to a topic 4 | # 5 | require 'yaml' 6 | require 'jms' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | JMS::Connection.session(config) do |session| 15 | session.producer(topic_name: 'SampleTopic') do |producer| 16 | producer.send(session.message("Hello World: #{Time.now}")) 17 | JMS.logger.info 'Successfully published one message to topic SampleTopic' 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /examples/performance/consumer.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Consumer: 3 | # Retrieve all messages from a queue 4 | # 5 | require 'yaml' 6 | require 'jms' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | JMS::Connection.session(config) do |session| 15 | stats = session.consume(queue_name: 'ExampleQueue', statistics: true) do |message| 16 | # Do nothing in this example with each message 17 | end 18 | 19 | JMS.logger.info "Consumed #{stats[:messages]} messages. Average #{stats[:ms_per_msg]}ms per message" 20 | end 21 | -------------------------------------------------------------------------------- /examples/producer-consumer/producer.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Producer: 3 | # Write messages to the queue 4 | # 5 | require 'yaml' 6 | require 'jms' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | JMS::Connection.session(config) do |session| 15 | session.producer(:queue_name => 'ExampleQueue') do |producer| 16 | producer.delivery_mode_sym = :non_persistent 17 | producer.send(session.message("Hello World: #{Time.now}")) 18 | JMS.logger.info 'Successfully sent one message to queue ExampleQueue' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | jruby-jms (1.2.0-java) 5 | gene_pool 6 | semantic_logger 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | ansi (1.5.0) 12 | builder (3.2.2) 13 | gene_pool (1.4.1) 14 | thread_safe 15 | minitest (5.8.0) 16 | minitest-reporters (1.0.20) 17 | ansi 18 | builder 19 | minitest (>= 5.0) 20 | ruby-progressbar 21 | rake (10.4.2) 22 | ruby-progressbar (1.7.5) 23 | semantic_logger (2.15.0) 24 | sync_attr (~> 2.0) 25 | thread_safe (~> 0.1) 26 | sync_attr (2.0.0) 27 | thread_safe (0.3.5-java) 28 | 29 | PLATFORMS 30 | java 31 | 32 | DEPENDENCIES 33 | jruby-jms! 34 | minitest 35 | minitest-reporters 36 | rake 37 | 38 | BUNDLED WITH 39 | 1.10.6 40 | -------------------------------------------------------------------------------- /lib/jms/imports.rb: -------------------------------------------------------------------------------- 1 | # Import Java classes into JMS Namespace 2 | module JMS 3 | java_import 'javax.jms.DeliveryMode' 4 | java_import 'javax.jms.Message' 5 | java_import 'javax.jms.BytesMessage' 6 | java_import 'javax.jms.TextMessage' 7 | java_import 'javax.jms.MapMessage' 8 | java_import 'javax.jms.ObjectMessage' 9 | java_import 'javax.jms.StreamMessage' 10 | java_import 'javax.jms.Session' 11 | java_import 'javax.jms.Destination' 12 | java_import 'javax.jms.Queue' 13 | java_import 'javax.jms.Topic' 14 | java_import 'javax.jms.TemporaryQueue' 15 | java_import 'javax.jms.TemporaryTopic' 16 | java_import 'javax.jms.MessageConsumer' 17 | java_import 'javax.jms.MessageProducer' 18 | java_import 'javax.jms.QueueBrowser' 19 | java_import 'javax.jms.MessageListener' 20 | java_import 'javax.jms.ExceptionListener' 21 | end -------------------------------------------------------------------------------- /examples/performance/producer.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Producer: 3 | # Write multiple messages to the queue 4 | # 5 | require 'yaml' 6 | require 'jms' 7 | require 'benchmark' 8 | 9 | jms_provider = ARGV[0] || 'activemq' 10 | count = (ARGV[1] || 10).to_i 11 | 12 | # Load Connection parameters from configuration file 13 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 14 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 15 | 16 | JMS::Connection.session(config) do |session| 17 | duration = Benchmark.realtime do 18 | session.producer(queue_name: 'ExampleQueue') do |producer| 19 | count.times do |i| 20 | producer.send(session.message("Hello Producer #{i}")) 21 | end 22 | end 23 | end 24 | 25 | JMS.logger.info "Produced #{count} messages. Average #{duration*1000/count}ms per message" 26 | end 27 | -------------------------------------------------------------------------------- /lib/jms/bytes_message.rb: -------------------------------------------------------------------------------- 1 | # Interface javax.jms.BytesMessage 2 | module JMS::BytesMessage 3 | def data 4 | # Puts the message body in read-only mode and repositions the stream of 5 | # bytes to the beginning 6 | self.reset 7 | 8 | available = self.body_length 9 | 10 | return nil if available == 0 11 | 12 | result = '' 13 | bytes_size = 1024 14 | bytes = Java::byte[bytes_size].new 15 | 16 | while (n = available < bytes_size ? available : bytes_size) > 0 17 | self.read_bytes(bytes, n) 18 | if n == bytes_size 19 | result << String.from_java_bytes(bytes) 20 | else 21 | result << String.from_java_bytes(bytes)[0..n-1] 22 | end 23 | available -= n 24 | end 25 | result 26 | end 27 | 28 | def data=(val) 29 | self.write_bytes(val.respond_to?(:to_java_bytes) ? val.to_java_bytes : val) 30 | end 31 | 32 | def to_s 33 | data 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /examples/client-server/replier.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample request/reply pattern replier: 3 | # Basically does the following: 4 | # 1) Consume messages from ExampleQueue indefinitely 5 | # 2) Get JMSReplyTo from consumed message 6 | # 3) Produce and send response to received message 7 | # 8 | require 'jms' 9 | require 'yaml' 10 | 11 | jms_provider = ARGV[0] || 'activemq' 12 | 13 | # Load Connection parameters from configuration file 14 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 15 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 16 | 17 | JMS::Connection.session(config) do |session| 18 | session.consume(queue_name: 'ExampleQueue', timeout: -1) do |message| 19 | p "Got message: #{message.data}. Replying politely." 20 | session.producer(destination: message.reply_to) do |producer| 21 | producer.send(session.message('Hello to you too!')) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/clean' 2 | require 'rake/testtask' 3 | 4 | raise 'jruby-jms must be built with JRuby' unless defined?(JRUBY_VERSION) 5 | 6 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 7 | require 'jms/version' 8 | 9 | task :gem do 10 | system 'gem build jruby-jms.gemspec' 11 | end 12 | 13 | task :publish => :gem do 14 | system "git tag -a v#{JMS::VERSION} -m 'Tagging #{JMS::VERSION}'" 15 | system 'git push --tags' 16 | system "gem push jruby-jms-#{JMS::VERSION}-java.gem" 17 | system "rm jruby-jms-#{JMS::VERSION}-java.gem" 18 | end 19 | 20 | desc 'Run Test Suite' 21 | task :test do 22 | Rake::TestTask.new(:functional) do |t| 23 | t.test_files = FileList['test/*_test.rb'] 24 | t.verbose = true 25 | end 26 | 27 | Rake::Task['functional'].invoke 28 | end 29 | 30 | task :default => :test 31 | 32 | desc 'Generate RDOC documentation' 33 | task :doc do 34 | system "rdoc --main README.md --inline-source --quiet README.md `find lib -name '*.rb'`" 35 | end 36 | -------------------------------------------------------------------------------- /jruby-jms.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | # Maintain gem's version: 5 | require 'jms/version' 6 | 7 | # Describe your gem and declare its dependencies: 8 | Gem::Specification.new do |spec| 9 | spec.name = 'jruby-jms' 10 | spec.version = JMS::VERSION 11 | spec.platform = 'java' 12 | spec.authors = ['Reid Morrison'] 13 | spec.email = ['reidmo@gmail.com'] 14 | spec.homepage = 'https://github.com/reidmorrison/jruby-jms' 15 | spec.summary = 'JRuby interface into JMS' 16 | spec.description = 'jruby-jms is a complete JRuby API into Java Messaging Specification (JMS) V1.1. For JRuby only.' 17 | spec.files = Dir['lib/**/*', 'bin/*', 'LICENSE.txt', 'Rakefile', 'README.md', 'HISTORY.md'] 18 | spec.test_files = Dir['test/**/*'] 19 | spec.license = 'Apache-2.0' 20 | spec.has_rdoc = true 21 | spec.add_dependency 'gene_pool' 22 | spec.add_dependency 'semantic_logger' 23 | end 24 | -------------------------------------------------------------------------------- /examples/publish-subscribe/subscribe.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Topic Subscriber: 3 | # Retrieve all messages from a topic using a non-durable subscription 4 | # 5 | # Try starting multiple copies of this Consumer. All active instances should 6 | # receive the same messages 7 | # 8 | # Since the topic subscription is non-durable, it will only receive new messages. 9 | # Any messages sent prior to the instance starting will not be received. 10 | # Also, any messages sent after the instance has stopped will not be received 11 | # when the instance is re-started, only new messages sent after it started will 12 | # be received. 13 | require 'jms' 14 | require 'yaml' 15 | 16 | jms_provider = ARGV[0] || 'activemq' 17 | 18 | # Load Connection parameters from configuration file 19 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 20 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 21 | 22 | JMS::Connection.session(config) do |session| 23 | session.consume(topic_name: 'SampleTopic', timeout: 30000) do |message| 24 | JMS.logger.info message.inspect 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/jms/oracle_a_q_connection_factory.rb: -------------------------------------------------------------------------------- 1 | module JMS 2 | # Full Qualified name causes a Java exception 3 | java_import 'oracle.jms.AQjmsFactory' 4 | 5 | # Connection Factory to support Oracle AQ 6 | class OracleAQConnectionFactory 7 | attr_accessor :username, :url 8 | attr_writer :password 9 | 10 | # Creates a connection per standard JMS 1.1 techniques from the Oracle AQ JMS Interface 11 | def create_connection(*args) 12 | # Since username and password are not assigned (see lib/jms/connection.rb:200) 13 | # and connection_factory.create_connection expects 2 arguments when username is not null ... 14 | if args.length == 2 15 | @username = args[0] 16 | @password = args[1] 17 | end 18 | 19 | # Full Qualified name causes a Java exception 20 | #cf = oracle.jms.AQjmsFactory.getConnectionFactory(@url, java.util.Properties.new) 21 | cf = AQjmsFactory.getConnectionFactory(@url, java.util.Properties.new) 22 | 23 | if username 24 | cf.createConnection(@username, @password) 25 | else 26 | cf.createConnection() 27 | end 28 | end 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /examples/producer-consumer/consumer_async.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample Asynchronous Consumer: 3 | # Retrieve all messages from the queue in a separate thread 4 | # 5 | require 'jms' 6 | require 'yaml' 7 | 8 | jms_provider = ARGV[0] || 'activemq' 9 | 10 | # Load Connection parameters from configuration file 11 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 12 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 13 | 14 | continue = true 15 | 16 | trap('INT') { 17 | JMS.logger.info 'CTRL + C' 18 | continue = false 19 | } 20 | 21 | # Consume all available messages on the queue 22 | JMS::Connection.start(config) do |connection| 23 | 24 | # Define Asynchronous code block to be called every time a message is received 25 | connection.on_message(queue_name: 'ExampleQueue') do |message| 26 | JMS.logger.info message.inspect 27 | end 28 | 29 | # Since the on_message handler above is in a separate thread the thread needs 30 | # to do some other work. For this example it will just sleep for 10 seconds 31 | while (continue) 32 | sleep 10 33 | end 34 | 35 | JMS.logger.info 'closing ...' 36 | end 37 | -------------------------------------------------------------------------------- /examples/advanced/session_pool.rb: -------------------------------------------------------------------------------- 1 | # 2 | # How to use the session pool in a multi-threaded application such as Rails 3 | # This example shows how to have multiple producers publishing information 4 | # to topics 5 | # 6 | require 'yaml' 7 | require 'jms' 8 | # Also add 'gene_pool' to list of gems in Gemfile 9 | require 'gene_pool' 10 | 11 | jms_provider = ARGV[0] || 'activemq' 12 | 13 | ### This part would typically go in a Rails Initializer ### 14 | 15 | # Load Connection parameters from configuration file 16 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 17 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 18 | 19 | JMS_CONNECTION = JMS::Connection.new(config) 20 | JMS_CONNECTION.start 21 | JMS_SESSION_POOL = JMS_CONNECTION.create_session_pool(config) 22 | 23 | # Ensure connections are released if application is shutdown 24 | at_exit do 25 | JMS_SESSION_POOL.close 26 | JMS_CONNECTION.close 27 | end 28 | 29 | ### This part would typically go in the Rails Model ### 30 | 31 | JMS_SESSION_POOL.producer(queue_name: 'SampleQueue') do |session, producer| 32 | producer.send(session.message('Hello World')) 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/jms/message_producer.rb: -------------------------------------------------------------------------------- 1 | # Extend JMS Message Producer Interface with Ruby methods 2 | # 3 | # For further help on javax.jms.MessageProducer 4 | # http://download.oracle.com/javaee/6/api/javax/jms/MessageProducer.html 5 | # 6 | # Interface javax.jms.Producer 7 | module JMS::MessageProducer 8 | 9 | # Return the Delivery Mode as a Ruby symbol 10 | # :persistent 11 | # :non_persistent 12 | # nil if unknown 13 | def delivery_mode_sym 14 | case delivery_mode 15 | when JMS::DeliveryMode::PERSISTENT 16 | :persistent 17 | when JMS::DeliveryMode::NON_PERSISTENT 18 | :non_persistent 19 | else 20 | nil 21 | end 22 | end 23 | 24 | # Set the JMS Delivery Mode from a Ruby Symbol 25 | # Valid values for mode 26 | # :persistent 27 | # :non_persistent 28 | # 29 | # Example: 30 | # producer.delivery_mode_sym = :persistent 31 | def delivery_mode_sym=(mode) 32 | self.delivery_mode = 33 | case mode 34 | when :persistent 35 | JMS::DeliveryMode::PERSISTENT 36 | when :non_persistent 37 | JMS::DeliveryMode::NON_PERSISTENT 38 | else 39 | raise "Unknown delivery mode symbol: #{mode}" 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /test/log4j.properties: -------------------------------------------------------------------------------- 1 | # 2 | # This file controls most of the logging in ActiveMQ which is mainly based around 3 | # the commons logging API. 4 | # 5 | log4j.rootLogger=INFO, console 6 | log4j.logger.org.apache.activemq.spring=WARN 7 | log4j.logger.org.apache.activemq.web.handler=WARN 8 | log4j.logger.org.springframework=WARN 9 | log4j.logger.org.apache.xbean=WARN 10 | log4j.logger.org.apache.camel=INFO 11 | 12 | # When debugging or reporting problems to the ActiveMQ team, 13 | # comment out the above lines and uncomment the next. 14 | 15 | #log4j.rootLogger=DEBUG, logfile, console 16 | 17 | # Or for more fine grained debug logging uncomment one of these 18 | #log4j.logger.org.apache.activemq=DEBUG 19 | #log4j.logger.org.apache.camel=DEBUG 20 | 21 | # Console appender 22 | log4j.appender.console=org.apache.log4j.ConsoleAppender 23 | log4j.appender.console.layout=org.apache.log4j.PatternLayout 24 | log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p: %m [%c %t]%n 25 | log4j.appender.console.threshold=INFO 26 | 27 | log4j.additivity.org.apache.activemq.audit=false 28 | log4j.logger.org.apache.activemq.audit=INFO, audit 29 | 30 | log4j.appender.audit=org.apache.log4j.ConsoleAppender 31 | log4j.appender.audit.layout=org.apache.log4j.PatternLayout 32 | log4j.appender.audit.layout.ConversionPattern=%d{ISO8601} %-5p: %m [%c %t]%n 33 | -------------------------------------------------------------------------------- /examples/file-to-q/q_to_files.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Example: q_to_files: 3 | # Copy all messages in a queue to separate files in a directory 4 | # The messages are left on the queue by 5 | # 6 | # jruby q_to_files.rb activemq my_queue 7 | # 8 | require 'jms' 9 | require 'yaml' 10 | require 'fileutils' 11 | 12 | raise("Required Parameters: 'jms_provider' 'queue_name' 'output_directory'") unless ARGV.count >= 2 13 | jms_provider = ARGV[0] 14 | queue_name = ARGV[1] 15 | path = ARGV[2] || queue_name 16 | 17 | # Load Connection parameters from configuration file 18 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 19 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 20 | 21 | # Create supplied path if it does not exist 22 | FileUtils.mkdir_p(path) 23 | 24 | counter = 0 25 | # Consume all available messages on the queue 26 | JMS::Connection.session(config) do |session| 27 | session.browse(:queue_name => queue_name, :timeout => 1000) do |message| 28 | counter += 1 29 | filename = File.join(path, 'message_%03d' % counter) 30 | File.open(filename+'.data', 'wb') { |file| file.write(message.data) } 31 | header = { 32 | :attributes => message.attributes, 33 | :properties => message.properties 34 | } 35 | File.open(filename+'.yml', 'wb') { |file| file.write(header.to_yaml) } 36 | end 37 | end 38 | 39 | puts "Saved #{counter} messages to #{path}" 40 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Allow test to be run in-place without requiring a gem install 2 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 3 | 4 | # Configure Rails Environment 5 | ENV['RAILS_ENV'] = 'test' 6 | 7 | require 'minitest/autorun' 8 | require 'minitest/reporters' 9 | require 'yaml' 10 | require 'jms' 11 | 12 | Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new 13 | 14 | SemanticLogger.add_appender(file_name: 'test.log', formatter: :color) 15 | SemanticLogger.default_level = :trace 16 | 17 | # Set Log4J properties file so that it does not need to be in the CLASSPATH 18 | java.lang.System.properties['log4j.configuration'] = 'test/log4j.properties' 19 | 20 | # Load configuration from jms.yml 21 | # Returns [Hash, String, String] the configuration, queue_name and topic_name 22 | def read_config 23 | # To change the JMS provider, edit jms.yml and change :default 24 | 25 | # Load Connection parameters from configuration file 26 | cfg = YAML.load_file(File.join(File.dirname(__FILE__), 'jms.yml')) 27 | jms_provider = cfg['default'] 28 | config = cfg[jms_provider] 29 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 30 | queue_name = config.delete(:queue_name) || raise("Mandatory :queue_name missing from jms.yml") 31 | topic_name = config.delete(:topic_name) || raise("Mandatory :topic_name missing from jms.yml") 32 | [config, queue_name, topic_name] 33 | end 34 | -------------------------------------------------------------------------------- /examples/invm/invm.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample ActiveMQ InVM Example: 3 | # Write to a queue and then consume the message in a separate thread 4 | # 5 | # Note: This example only works with ActiveMQ 6 | # Update the jar files path in ../jms.yml to point to your ActiveMQ installation 7 | require 'yaml' 8 | require 'jms' 9 | require 'benchmark' 10 | 11 | # Set Log4J properties file so that it does not need to be in the CLASSPATH 12 | java.lang.System.properties['log4j.configuration'] = "file://#{File.join(File.dirname(__FILE__), 'log4j.properties')}" 13 | 14 | jms_provider = 'activemq-invm' 15 | 16 | # Load Connection parameters from configuration file 17 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 18 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 19 | 20 | JMS::Connection.start(config) do |connection| 21 | # Consume messages in a separate thread 22 | connection.on_message(:queue_name => 'ExampleQueue') do |message| 23 | JMS.logger.info "Consumed message from ExampleQueue: '#{message.data}'" 24 | end 25 | 26 | # Send a single message within a new session 27 | connection.session do |session| 28 | session.producer(:queue_name => 'ExampleQueue') do |producer| 29 | producer.send(session.message("Hello World. #{Time.now}")) 30 | end 31 | end 32 | 33 | JMS.logger.info 'Put message on ExampleQueue' 34 | 35 | # Give the consume thread time to process the message before terminating 36 | sleep 1 37 | 38 | JMS.logger.info 'Shutting down' 39 | end 40 | -------------------------------------------------------------------------------- /examples/client-server/requestor.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample request/reply pattern requestor implementation: 3 | # Basically what this does is: 4 | # 1) Create temporary Queue to MQ Session 5 | # 2) Create consumer to session (This is important to be up before producer to make sure response isn't available before consumer is up) 6 | # 3) Create producer to session 7 | # 4) Create message for session 8 | # 5) Set message's JMSReplyTo to point to the temporary queue created in #1 9 | # 6) Send message to send queue 10 | # 7) Consume the first message available from the temporary queue within the time set in :timeout 11 | # 8) Close temporary queue, consumer, producer and session by ending the blocks 12 | # 13 | require 'yaml' 14 | require 'jms' 15 | 16 | jms_provider = ARGV[0] || 'activemq' 17 | 18 | # Load Connection parameters from configuration file 19 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 20 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 21 | 22 | JMS::Connection.session(config) do |session| 23 | session.temporary_queue do |temporary_queue| 24 | session.consumer(destination: temporary_queue) do |consumer| 25 | session.producer(queue_name: 'ExampleQueue') do |producer| 26 | message = session.message('Hello World') 27 | message.jms_reply_to = temporary_queue 28 | producer.send(message) 29 | end 30 | # Using timeout of 5seconds here 31 | response_message = consumer.get(timeout: 5000) 32 | # Get message data as response if response_message is available 33 | response = response_message != nil ? response_message.data : nil 34 | p response 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/jms/mq_workaround.rb: -------------------------------------------------------------------------------- 1 | # Workaround for IBM MQ JMS implementation that implements some undocumented methods 2 | begin 3 | class com.ibm.mq.jms::MQQueueSession 4 | if self.method_defined?(:consume) 5 | def consume(params, &proc) 6 | Java::JavaxJms::Session.instance_method(:consume).bind(self).call(params, &proc) 7 | end 8 | end 9 | end 10 | 11 | class com.ibm.mq.jms::MQSession 12 | if self.method_defined?(:consume) 13 | def consume(params, &proc) 14 | Java::JavaxJms::Session.instance_method(:consume).bind(self).call(params, &proc) 15 | end 16 | end 17 | 18 | if self.method_defined?(:create_destination) 19 | def create_destination(params) 20 | Java::JavaxJms::Session.instance_method(:create_destination).bind(self).call(params) 21 | end 22 | end 23 | end 24 | 25 | class com.ibm.mq.jms::MQQueueBrowser 26 | if self.method_defined?(:each) 27 | def each(params, &proc) 28 | Java::ComIbmMsgClientJms::JmsQueueBrowser.instance_method(:each).bind(self).call(params, &proc) 29 | end 30 | end 31 | end 32 | 33 | class com.ibm.mq.jms::MQQueueReceiver 34 | if self.method_defined?(:each) 35 | def each(params, &proc) 36 | Java::JavaxJms::MessageConsumer.instance_method(:each).bind(self).call(params, &proc) 37 | end 38 | end 39 | 40 | if self.method_defined?(:get) 41 | def get(params={}) 42 | Java::JavaxJms::MessageConsumer.instance_method(:get).bind(self).call(params) 43 | end 44 | end 45 | end 46 | 47 | class com.ibm.mq.jms::MQQueue 48 | if self.method_defined?(:delete) 49 | undef_method :delete 50 | end 51 | end 52 | 53 | rescue NameError 54 | # Ignore errors (when we aren't using MQ) 55 | end 56 | -------------------------------------------------------------------------------- /examples/file-to-q/files_to_q.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Example : files_to_q : Place all files in a directory to a queue 3 | # Each file is written as a separate message 4 | # Place the data in a file ending with '.data' 5 | # and the header information in a file with same name, but with an 6 | # extension of '.yml' 7 | # 8 | # jruby files_to_q.rb activemq my_queue 9 | # 10 | require 'jms' 11 | require 'yaml' 12 | 13 | raise("Required Parameters: 'jms_provider' 'queue_name' 'input_directory'") unless ARGV.count >= 2 14 | jms_provider = ARGV[0] 15 | queue_name = ARGV[1] 16 | path = ARGV[2] || queue_name 17 | 18 | # Load Connection parameters from configuration file 19 | config = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'jms.yml'))[jms_provider] 20 | raise "JMS Provider option:#{jms_provider} not found in jms.yml file" unless config 21 | 22 | counter = 0 23 | # Consume all available messages on the queue 24 | JMS::Connection.session(config) do |session| 25 | session.producer(:queue_name => queue_name) do |producer| 26 | Dir.glob(File.join(path, '*.data')) do |filename| 27 | unless File.directory?(filename) 28 | printf("%5d: #{filename}\n", counter = counter + 1) 29 | data = File.open(filename, 'rb') { |file| file.read } 30 | header_filename = File.join(File.dirname(filename), File.basename(filename)) 31 | header_filename = header_filename[0, header_filename.length - '.data'.length] + '.yml' 32 | header = File.exist?(header_filename) ? YAML.load_file(header_filename) : nil 33 | message = session.message(data, :bytes) 34 | if header 35 | header[:attributes].each_pair do |k, v| 36 | next if k == :jms_destination 37 | message.send("#{k}=".to_sym, v) if message.respond_to?("#{k}=".to_sym) 38 | end if header[:attributes] 39 | message.properties = header[:properties] || {} 40 | end 41 | producer.send(message) 42 | end 43 | end 44 | end 45 | end 46 | puts "Read #{counter} messages from #{path} and wrote to #{queue_name}" 47 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 (2015-08-29) 2 | 3 | * Tested against JRuby 9.0.0.0 4 | * Drop support for JRuby 1.5 and 1.6 5 | * Tested against ActiveMQ 5.11, and HornetQ 2.4 6 | * Raises ArgumentError instead of RuntimeError for missing arguments 7 | * Update tests 8 | * Upgrade to Minitest 5.8 9 | * Switch to Ruby 1.9 hash syntax 10 | * Reformat code 11 | 12 | ## 1.1.0 (2014-04-10) 13 | 14 | * Support Oracle AQ 11gR2 15 | * Tibco EMS Examples 16 | * Add .gemspec file 17 | 18 | ## 1.0.0 (2012-10-21) 19 | 20 | * Issue #10 Support WebSphereMQ V7 21 | * Issue #11 Add GenePool dependency for ConnectionPool 22 | * Include version 23 | 24 | ## 0.11.2 (2011-06-01) 25 | 26 | * Issue #8 Add ability to set Producer delivery mode using a Symbol 27 | * Include ActiveMQ InVM working example along with Log4J properties file 28 | 29 | ## 0.11.1 (2011-05-25) 30 | 31 | * Fixes the condition where a bad session keeps being re-used in a session pool. 32 | It is now removed from the pool anytime a JMS exception has occurred 33 | 34 | ## 0.11.0 (2011-04-18) 35 | 36 | * Compatibility with JRuby 1.6 37 | * I hate doing this, but unfortunately there is a small breaking change in this release: 38 | * We can no longer pass symbols into the following methods: 39 | * jms_delivery_mode 40 | * jms_delivery_mode= 41 | * Just rename existing uses of the above methods to: 42 | * jms_delivery_mode_sym 43 | * jms_delivery_mode_sym= 44 | * Added Session Pool - requires GenePool as a dependency if used 45 | * Generate warning log entry for any parameters not known to the ConnectionFactory 46 | * Use java_import for all javax.jms classes 47 | * Rename all Java source files to match new names 48 | 49 | ## 0.10.1 (2011-02-21) 50 | 51 | * Fix persistence typo and add message test cases 52 | 53 | ## 0.10.0 (2011-02-10) 54 | 55 | * Refactoring interface 56 | 57 | ## 0.9.0 (2011-01-23) 58 | 59 | * Revised API with cleaner interface 60 | * Publish GEM 61 | 62 | ## 0.8.0 (2011-01-22) 63 | 64 | * Release to the wild for general use 65 | 66 | ## 2008, 2009, 2010 67 | 68 | * Previously known as jms4jruby 69 | * Running in production at an enterprise processing a million messages a day 70 | -------------------------------------------------------------------------------- /lib/jms/map_message.rb: -------------------------------------------------------------------------------- 1 | # Interface javax.jms.MapMessage 2 | module JMS::MapMessage 3 | # Since each is defined, add support for: inject, map, include?, and find_all? 4 | # <=> also allows support for: min, max, and sort 5 | include Enumerable 6 | 7 | # Return Map Message as a hash 8 | def to_h 9 | h = {} 10 | each_pair { |key, value| h[key] = value } 11 | h 12 | end 13 | 14 | # Return Map Message as a hash 15 | def data 16 | to_h 17 | end 18 | 19 | # Copy values from supplied hash into this MapMessage 20 | # Converts Ruby types to Java native Data types as follows: 21 | # Fixnum => long 22 | # Float => double 23 | # Bignum => long 24 | # true => boolean 25 | # false => boolean 26 | # nil => null 27 | # Otherwise it calls ::to_s on the supplied data type 28 | def data=(data) 29 | data.each_pair do |key, val| 30 | case 31 | when val.class == Fixnum # 1 32 | setLong(key.to_s, val) 33 | when val.class == Float #1.1 34 | setDouble(key.to_s, val) 35 | when val.class == Bignum # 11111111111111111 36 | setLong(key.to_s, val) 37 | when (val.class == TrueClass) || (val.class == FalseClass) 38 | setBoolean(key.to_s, val) 39 | when val.class == NilClass 40 | setObject(key.to_s, val) 41 | else 42 | setString(key.to_s, val.to_s) 43 | end 44 | end 45 | end 46 | 47 | # Return each name value pair 48 | def each(&proc) 49 | # When key and value are expected separately. Should actually be calling each_pair anyway 50 | if proc.arity == 2 51 | each_pair(proc) 52 | else 53 | enum = getMapNames 54 | while enum.has_more_elements 55 | key = enum.next_element 56 | proc.call [key, getObject(key)] 57 | end 58 | end 59 | end 60 | 61 | # Return each name value pair 62 | def each_pair(&proc) 63 | enum = getMapNames 64 | while enum.has_more_elements 65 | key = enum.next_element 66 | proc.call key, getObject(key) 67 | end 68 | end 69 | 70 | # Does map include specified key 71 | def include?(key) 72 | # Ensure a Ruby true is returned 73 | item_exists(key) == true 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/session_pool_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | require 'timeout' 3 | 4 | class SessionPoolTest < Minitest::Test 5 | describe JMS::SessionPool do 6 | before do 7 | @config, @queue_name, @topic_name = read_config 8 | @pool_params = { 9 | pool_size: 10, 10 | pool_warn_timeout: 5.0 11 | } 12 | end 13 | 14 | it 'create a session pool' do 15 | JMS::Connection.start(@config) do |connection| 16 | pool = connection.create_session_pool(@pool_params) 17 | pool.session do |session| 18 | assert session 19 | assert session.is_a?(javax.jms.Session) 20 | end 21 | pool.close 22 | end 23 | end 24 | 25 | it 'remove bad session from pool' do 26 | JMS::Connection.start(@config) do |connection| 27 | pool = connection.create_session_pool(@pool_params.merge(pool_size: 1)) 28 | s = nil 29 | r = 30 | begin 31 | pool.session do |session| 32 | assert session 33 | assert session.is_a?(javax.jms.Session) 34 | s = session 35 | s.close 36 | s.create_map_message 37 | false 38 | end 39 | rescue javax.jms.IllegalStateException 40 | true 41 | end 42 | assert r == true 43 | 44 | # Now verify that the previous closed session was removed from the pool 45 | pool.session do |session| 46 | assert session 47 | assert session.is_a?(javax.jms.Session) 48 | assert s != session 49 | session.create_map_message 50 | end 51 | end 52 | end 53 | 54 | it 'allow multiple sessions to be used concurrently' do 55 | JMS::Connection.start(@config) do |connection| 56 | pool = connection.create_session_pool(@pool_params) 57 | pool.session do |session| 58 | assert session 59 | assert session.is_a?(javax.jms.Session) 60 | pool.session do |session2| 61 | assert session2 62 | assert session2.is_a?(javax.jms.Session) 63 | assert session != session2 64 | end 65 | end 66 | end 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /examples/invm/log4j.properties: -------------------------------------------------------------------------------- 1 | # 2 | # This file controls most of the logging in ActiveMQ which is mainly based around 3 | # the commons logging API. 4 | # 5 | log4j.rootLogger=INFO, logfile, console 6 | log4j.logger.org.apache.activemq.spring=WARN 7 | log4j.logger.org.apache.activemq.web.handler=WARN 8 | log4j.logger.org.springframework=WARN 9 | log4j.logger.org.apache.xbean=WARN 10 | log4j.logger.org.apache.camel=INFO 11 | 12 | # When debugging or reporting problems to the ActiveMQ team, 13 | # comment out the above lines and uncomment the next. 14 | 15 | #log4j.rootLogger=DEBUG, logfile, console 16 | 17 | # Or for more fine grained debug logging uncomment one of these 18 | #log4j.logger.org.apache.activemq=DEBUG 19 | #log4j.logger.org.apache.camel=DEBUG 20 | 21 | # Console appender 22 | log4j.appender.console=org.apache.log4j.ConsoleAppender 23 | log4j.appender.console.layout=org.apache.log4j.PatternLayout 24 | log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p: %m [%c %t]%n 25 | log4j.appender.console.threshold=INFO 26 | 27 | # File appender 28 | log4j.appender.logfile=org.apache.log4j.RollingFileAppender 29 | log4j.appender.logfile.file=activemq.log 30 | log4j.appender.logfile.maxFileSize=10240KB 31 | log4j.appender.logfile.maxBackupIndex=5 32 | log4j.appender.logfile.append=true 33 | log4j.appender.logfile.layout=org.apache.log4j.PatternLayout 34 | log4j.appender.logfile.layout.ConversionPattern=%d{ISO8601} %-5p: %m [%c %t]%n 35 | # use some of the following patterns to see MDC logging data 36 | # 37 | # %X{activemq.broker} 38 | # %X{activemq.connector} 39 | # %X{activemq.destination} 40 | # 41 | # e.g. 42 | # 43 | # log4j.appender.logfile.layout.ConversionPattern=%d | %-20.20X{activemq.connector} | %-5p | %m | %c | %t%n 44 | 45 | ########### 46 | # Audit log 47 | ########### 48 | 49 | log4j.additivity.org.apache.activemq.audit=false 50 | log4j.logger.org.apache.activemq.audit=INFO, audit 51 | 52 | log4j.appender.audit=org.apache.log4j.FileAppender 53 | log4j.appender.audit.file=activemq-audit.log 54 | log4j.appender.logfile.maxFileSize=10240KB 55 | log4j.appender.logfile.maxBackupIndex=5 56 | log4j.appender.audit.append=true 57 | log4j.appender.audit.layout=org.apache.log4j.PatternLayout 58 | log4j.appender.audit.layout.ConversionPattern=%d{ISO8601} %-5p: %m [%c %t]%n 59 | -------------------------------------------------------------------------------- /lib/jms/message_listener_impl.rb: -------------------------------------------------------------------------------- 1 | module JMS 2 | 3 | private 4 | 5 | # For internal use only by JMS::Connection 6 | class MessageListenerImpl 7 | include JMS::MessageListener 8 | include SemanticLogger::Loggable 9 | 10 | # Parameters: 11 | # :statistics Capture statistics on how many messages have been read 12 | # true : This method will capture statistics on the number of messages received 13 | # and the time it took to process them. 14 | # The timer starts when the listener instance is created and finishes when either the last message was received, 15 | # or when Destination::statistics is called. In this case MessageConsumer::statistics 16 | # can be called several times during processing without affecting the end time. 17 | # Also, the start time and message count is not reset until MessageConsumer::each 18 | # is called again with statistics: true 19 | # 20 | # The statistics gathered are returned when statistics: true and async: false 21 | def initialize(params={}, &proc) 22 | @proc = proc 23 | 24 | if params[:statistics] 25 | @message_count = 0 26 | @start_time = Time.now 27 | end 28 | end 29 | 30 | # Method called for every message received on the queue 31 | # Per the JMS specification, this method will be called sequentially for each message on the queue. 32 | # This method will not be called again until its prior invocation has completed. 33 | # Must be onMessage() since on_message() does not work for interface methods that must be implemented 34 | def onMessage(message) 35 | begin 36 | if @message_count 37 | @message_count += 1 38 | @last_time = Time.now 39 | end 40 | logger.measure_debug('Message processed') do 41 | @proc.call message 42 | end 43 | rescue SyntaxError, NameError => exc 44 | logger.error "Ignoring poison message:\n#{message.inspect}", exc 45 | rescue StandardError => exc 46 | logger.error "Ignoring poison message:\n#{message.inspect}", exc 47 | rescue Exception => exc 48 | logger.error "Ignoring poison message:\n#{message.inspect}", exc 49 | end 50 | end 51 | 52 | # Return Statistics gathered for this listener 53 | def statistics 54 | raise(ArgumentError, 'First call MessageConsumer::on_message with statistics: true before calling MessageConsumer::statistics()') unless @message_count 55 | duration = (@last_time || Time.now) - @start_time 56 | { 57 | messages: @message_count, 58 | duration: duration, 59 | messages_per_second: (@message_count/duration).to_i 60 | } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/jms/message.rb: -------------------------------------------------------------------------------- 1 | # Extend JMS Message Interface with Ruby methods 2 | # 3 | # A Message is the item that can be put on a queue, or obtained from a queue. 4 | # 5 | # A Message consists of 3 major parts: 6 | # - Header 7 | # Accessible as attributes of the Message class 8 | # - Properties 9 | # Accessible via [] and []= methods 10 | # - Data 11 | # The actual data portion of the message 12 | # See the specific message types for details on how to access the data 13 | # portion of the message 14 | # 15 | # For further help on javax.jms.Message 16 | # http://download.oracle.com/javaee/6/api/index.html?javax/jms/Message.html 17 | # 18 | # Interface javax.jms.Message 19 | module JMS::Message 20 | 21 | # Methods directly exposed from the Java class: 22 | 23 | # call-seq: 24 | # acknowledge 25 | # 26 | # Acknowledges all consumed messages of the session of this consumed message 27 | # 28 | 29 | # call-seq: 30 | # clear_body 31 | # 32 | # Clears out the message body 33 | # 34 | 35 | # call-seq: 36 | # clear_properties 37 | # 38 | # Clears out the properties of this message 39 | # 40 | 41 | # Return the JMS Delivery Mode as a Ruby symbol 42 | # :persistent 43 | # :non_persistent 44 | # nil if unknown 45 | def jms_delivery_mode_sym 46 | case jms_delivery_mode 47 | when JMS::DeliveryMode::PERSISTENT 48 | :persistent 49 | when JMS::DeliveryMode::NON_PERSISTENT 50 | :non_persistent 51 | else 52 | nil 53 | end 54 | end 55 | 56 | # Set the JMS Delivery Mode from a Ruby Symbol 57 | # Valid values for mode 58 | # :persistent 59 | # :non_persistent 60 | def jms_delivery_mode_sym=(mode) 61 | val = 62 | case mode 63 | when :persistent 64 | JMS::DeliveryMode::PERSISTENT 65 | when :non_persistent 66 | JMS::DeliveryMode::NON_PERSISTENT 67 | else 68 | raise "Unknown delivery mode symbol: #{mode}" 69 | end 70 | self.setJMSDeliveryMode(val) 71 | end 72 | 73 | # Return the attributes (header fields) of the message as a Hash 74 | def attributes 75 | { 76 | jms_correlation_id: jms_correlation_id, 77 | jms_delivery_mode_sym: jms_delivery_mode_sym, 78 | jms_destination: jms_destination.nil? ? nil : jms_destination.to_string, 79 | jms_expiration: jms_expiration, 80 | jms_message_id: jms_message_id, 81 | jms_priority: jms_priority, 82 | jms_redelivered: jms_redelivered?, 83 | jms_reply_to: jms_reply_to, 84 | jms_timestamp: jms_timestamp, 85 | jms_type: jms_type, 86 | } 87 | end 88 | 89 | # Methods for manipulating the message properties 90 | 91 | # Get the value of a property 92 | def [](key) 93 | getObjectProperty key.to_s 94 | end 95 | 96 | # Set a property 97 | def []=(key, value) 98 | setObjectProperty(key.to_s, value) 99 | end 100 | 101 | # Does message include specified property? 102 | def include?(key) 103 | # Ensure a Ruby true is returned 104 | property_exists(key) == true 105 | end 106 | 107 | # Return Properties as a hash 108 | def properties 109 | h = {} 110 | properties_each_pair { |k, v| h[k]=v } 111 | h 112 | end 113 | 114 | # Set Properties from an existing hash 115 | def properties=(h) 116 | clear_properties 117 | h.each_pair { |k, v| setObjectProperty(k.to_s, v) } 118 | h 119 | end 120 | 121 | # Return each name value pair 122 | def properties_each_pair(&proc) 123 | enum = getPropertyNames 124 | while enum.has_more_elements 125 | key = enum.next_element 126 | proc.call key, getObjectProperty(key) 127 | end 128 | end 129 | 130 | def inspect 131 | "#{self.class.name}: #{data}\nAttributes: #{attributes.inspect}\nProperties: #{properties.inspect}" 132 | end 133 | 134 | end 135 | -------------------------------------------------------------------------------- /test/connection_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class ConnectionTest < Minitest::Test 4 | describe JMS::Connection do 5 | before do 6 | @config, @queue_name, @topic_name = read_config 7 | end 8 | 9 | it 'Create Connection to the Broker/Server' do 10 | connection = JMS::Connection.new(@config) 11 | JMS.logger.info connection.to_s 12 | assert connection 13 | connection.close 14 | end 15 | 16 | it 'Create and start Connection to the Broker/Server with block' do 17 | JMS::Connection.start(@config) do |connection| 18 | assert connection 19 | end 20 | end 21 | 22 | it 'Create and start Connection to the Broker/Server with block and start one session' do 23 | JMS::Connection.session(@config) do |session| 24 | assert session 25 | end 26 | end 27 | 28 | it 'Start and stop connection' do 29 | connection = JMS::Connection.new(@config) 30 | assert connection 31 | assert_nil connection.start 32 | 33 | assert_nil connection.stop 34 | assert_nil connection.close 35 | end 36 | 37 | it 'Create a session from the connection' do 38 | connection = JMS::Connection.new(@config) 39 | session = connection.create_session 40 | assert session 41 | assert_equal session.transacted?, false 42 | assert_nil session.close 43 | 44 | assert_nil connection.stop 45 | assert_nil connection.close 46 | end 47 | 48 | it 'Create a session with a block' do 49 | connection = JMS::Connection.new(@config) 50 | connection.session do |session| 51 | assert session 52 | assert_equal session.transacted?, false 53 | end 54 | 55 | assert_nil connection.stop 56 | assert_nil connection.close 57 | end 58 | 59 | it 'create a session without a block and throw exception' do 60 | connection = JMS::Connection.new(@config) 61 | 62 | assert_raises(ArgumentError) { connection.session } 63 | 64 | assert_nil connection.stop 65 | assert_nil connection.close 66 | end 67 | 68 | it 'Create a session from the connection with params' do 69 | connection = JMS::Connection.new(@config) 70 | session = connection.create_session(transacted: true, options: JMS::Session::AUTO_ACKNOWLEDGE) 71 | assert session 72 | assert_equal session.transacted?, true 73 | # When session is transacted, options are ignore, so ack mode must be transacted 74 | assert_equal session.acknowledge_mode, JMS::Session::SESSION_TRANSACTED 75 | assert_nil session.close 76 | 77 | assert_nil connection.stop 78 | assert_nil connection.close 79 | end 80 | 81 | it 'Create a session from the connection with block and params' do 82 | JMS::Connection.start(@config) do |connection| 83 | connection.session(transacted: true, options: JMS::Session::CLIENT_ACKNOWLEDGE) do |session| 84 | assert session 85 | assert_equal session.transacted?, true 86 | # When session is transacted, options are ignore, so ack mode must be transacted 87 | assert_equal session.acknowledge_mode, JMS::Session::SESSION_TRANSACTED 88 | end 89 | end 90 | end 91 | 92 | it 'Create a session from the connection with block and params opposite test' do 93 | JMS::Connection.start(@config) do |connection| 94 | connection.session(transacted: false, options: JMS::Session::AUTO_ACKNOWLEDGE) do |session| 95 | assert session 96 | assert_equal session.transacted?, false 97 | assert_equal session.acknowledge_mode, JMS::Session::AUTO_ACKNOWLEDGE 98 | end 99 | end 100 | end 101 | 102 | describe 'additional capabilities' do 103 | it 'start an on_message handler' do 104 | JMS::Connection.start(@config) do |connection| 105 | value = nil 106 | connection.on_message(transacted: true, queue_name: :temporary) do |message| 107 | value = 'received' 108 | end 109 | end 110 | end 111 | end 112 | 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/session_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | require 'yaml' 3 | 4 | class SessionTest < Minitest::Test 5 | describe 'JMS::Session' do 6 | before do 7 | @config, @queue_name, @topic_name = read_config 8 | end 9 | 10 | it 'create a session' do 11 | JMS::Connection.session(@config) do |session| 12 | assert session 13 | end 14 | end 15 | 16 | it 'create automatic messages' do 17 | JMS::Connection.session(@config) do |session| 18 | assert session 19 | # Create Text Message 20 | assert_equal session.message("Hello").java_kind_of?(JMS::TextMessage), true 21 | 22 | # Create Map Message 23 | assert_equal session.message('hello' => 'world').java_kind_of?(JMS::MapMessage), true 24 | end 25 | end 26 | 27 | it 'create explicit messages' do 28 | JMS::Connection.session(@config) do |session| 29 | assert session 30 | # Create Text Message 31 | assert_equal session.create_text_message("Hello").java_kind_of?(JMS::TextMessage), true 32 | 33 | # Create Map Message 34 | assert_equal session.create_map_message.java_kind_of?(JMS::MapMessage), true 35 | end 36 | end 37 | 38 | it 'create temporary destinations in blocks' do 39 | JMS::Connection.session(@config) do |session| 40 | assert session 41 | 42 | # Temporary Queue 43 | session.destination(queue_name: :temporary) do |destination| 44 | assert_equal destination.java_kind_of?(JMS::TemporaryQueue), true 45 | end 46 | 47 | # Temporary Topic 48 | session.create_destination(topic_name: :temporary) do |destination| 49 | assert_equal destination.java_kind_of?(JMS::TemporaryTopic), true 50 | end 51 | end 52 | end 53 | 54 | it 'create temporary destinations' do 55 | JMS::Connection.session(@config) do |session| 56 | assert session 57 | 58 | # Temporary Queue 59 | destination = session.create_destination(queue_name: :temporary) 60 | assert_equal destination.java_kind_of?(JMS::TemporaryQueue), true 61 | destination.delete 62 | 63 | # Temporary Topic 64 | destination = session.create_destination(topic_name: :temporary) 65 | assert_equal destination.java_kind_of?(JMS::TemporaryTopic), true 66 | destination.delete 67 | end 68 | end 69 | 70 | it 'create destinations in blocks' do 71 | JMS::Connection.session(@config) do |session| 72 | assert session 73 | 74 | # Temporary Queue 75 | session.destination(queue_name: @queue_name) do |destination| 76 | assert_equal destination.java_kind_of?(JMS::Queue), true 77 | end 78 | 79 | # Temporary Topic 80 | session.create_destination(topic_name: @topic_name) do |destination| 81 | assert_equal destination.java_kind_of?(JMS::Topic), true 82 | end 83 | end 84 | end 85 | 86 | it 'create destinations' do 87 | JMS::Connection.session(@config) do |session| 88 | assert session 89 | 90 | # Queue 91 | queue = session.create_destination(queue_name: @queue_name) 92 | assert_equal queue.java_kind_of?(JMS::Queue), true 93 | 94 | # Topic 95 | topic = session.create_destination(topic_name: @topic_name) 96 | assert_equal topic.java_kind_of?(JMS::Topic), true 97 | end 98 | end 99 | 100 | it 'create destinations using direct methods' do 101 | JMS::Connection.session(@config) do |session| 102 | assert session 103 | 104 | # Queue 105 | queue = session.queue(@queue_name) 106 | assert_equal queue.java_kind_of?(JMS::Queue), true 107 | 108 | # Temporary Queue 109 | queue = session.temporary_queue 110 | assert_equal queue.java_kind_of?(JMS::TemporaryQueue), true 111 | queue.delete 112 | 113 | # Topic 114 | topic = session.topic(@topic_name) 115 | assert_equal topic.java_kind_of?(JMS::Topic), true 116 | 117 | # Temporary Topic 118 | topic = session.temporary_topic 119 | assert_equal topic.java_kind_of?(JMS::TemporaryTopic), true 120 | topic.delete 121 | end 122 | end 123 | 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/message_test.rb: -------------------------------------------------------------------------------- 1 | require_relative 'test_helper' 2 | 3 | class MessageTest < Minitest::Test 4 | describe 'JMS::Message' do 5 | before do 6 | @config, @queue_name, @topic_name = read_config 7 | end 8 | 9 | it 'produce and consume messages to/from a temporary queue' do 10 | JMS::Connection.session(@config) do |session| 11 | assert session 12 | data = nil 13 | session.producer(queue_name: :temporary) do |producer| 14 | # Send Message 15 | producer.send(session.message('Hello World')) 16 | 17 | # Consume Message 18 | session.consume(destination: producer.destination, timeout: 1000) do |message| 19 | assert_equal message.java_kind_of?(JMS::TextMessage), true 20 | data = message.data 21 | end 22 | end 23 | assert_equal data, 'Hello World' 24 | end 25 | end 26 | 27 | it 'produce, browse and consume messages to/from a queue' do 28 | JMS::Connection.session(@config) do |session| 29 | assert session 30 | data = :timed_out 31 | browse_data = :timed_out 32 | session.producer(queue_name: @queue_name) do |producer| 33 | # Send Message 34 | producer.send(session.message('Hello World')) 35 | 36 | # Browse Message 37 | session.browse(queue_name: @queue_name, timeout: 1000) do |message| 38 | assert_equal message.java_kind_of?(JMS::TextMessage), true 39 | browse_data = message.data 40 | end 41 | 42 | # Consume Message 43 | session.consume(queue_name: @queue_name, timeout: 1000) do |message| 44 | assert_equal message.java_kind_of?(JMS::TextMessage), true 45 | data = message.data 46 | end 47 | end 48 | assert_equal 'Hello World', data 49 | assert_equal 'Hello World', browse_data 50 | end 51 | end 52 | 53 | it 'support setting persistence using symbols and the java constants' do 54 | JMS::Connection.session(@config) do |session| 55 | message = session.message('Hello World') 56 | message.jms_delivery_mode_sym = :non_persistent 57 | assert_equal message.jms_delivery_mode_sym, :non_persistent 58 | message.jms_delivery_mode_sym = :persistent 59 | assert_equal message.jms_delivery_mode_sym, :persistent 60 | end 61 | end 62 | 63 | it 'produce and consume non-persistent messages' do 64 | JMS::Connection.session(@config) do |session| 65 | assert session 66 | data = nil 67 | session.producer(queue_name: :temporary) do |producer| 68 | message = session.message('Hello World') 69 | message.jms_delivery_mode_sym = :non_persistent 70 | assert_equal :non_persistent, message.jms_delivery_mode_sym 71 | assert_equal false, message.persistent? 72 | 73 | # Send Message 74 | producer.send(message) 75 | 76 | # Consume Message 77 | session.consume(destination: producer.destination, timeout: 1000) do |message| 78 | assert_equal message.java_kind_of?(JMS::TextMessage), true 79 | data = message.data 80 | #assert_equal :non_persistent, message.jms_delivery_mode 81 | #assert_equal false, message.persistent? 82 | end 83 | end 84 | assert_equal data, 'Hello World' 85 | end 86 | end 87 | 88 | it 'produce and consume persistent messages' do 89 | JMS::Connection.session(@config) do |session| 90 | assert session 91 | data = nil 92 | session.producer(queue_name: @queue_name) do |producer| 93 | message = session.message('Hello World') 94 | message.jms_delivery_mode_sym = :persistent 95 | assert_equal :persistent, message.jms_delivery_mode_sym 96 | assert_equal true, message.persistent? 97 | 98 | # Send Message 99 | producer.send(message) 100 | 101 | # Consume Message 102 | session.consume(destination: producer.destination, timeout: 1000) do |message| 103 | assert_equal message.java_kind_of?(JMS::TextMessage), true 104 | data = message.data 105 | assert_equal :persistent, message.jms_delivery_mode_sym 106 | assert_equal true, message.persistent? 107 | end 108 | end 109 | assert_equal data, 'Hello World' 110 | end 111 | end 112 | 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /test/jms.yml: -------------------------------------------------------------------------------- 1 | # This YAML file contains the configuration options for several different 2 | # JMS Providers 3 | # 4 | # The Examples that ship with jruby-jms will use the entry 'default' unless 5 | # overriden at the command line. For example: 6 | # jruby producer.rb activemq 7 | # 8 | --- 9 | # Which JMS Provider to use by default 10 | 11 | #default: hornetq222 12 | #default: hornetq2_4_0 13 | #default: activemq543 14 | #default: activemq551 15 | default: activemq5_11_1 16 | #default: webspheremq6 17 | #default: webspheremq7 18 | 19 | activemq543: 20 | :factory: org.apache.activemq.ActiveMQConnectionFactory 21 | :broker_url: tcp://localhost:61616 22 | :require_jars: 23 | - ~/jms/apache-activemq-5.4.3/activemq-all-5.4.3.jar 24 | :queue_name: TestQueue 25 | :topic_name: TestTopic 26 | 27 | activemq551: 28 | :factory: org.apache.activemq.ActiveMQConnectionFactory 29 | :broker_url: tcp://localhost:61616 30 | :require_jars: 31 | - ~/jms/apache-activemq-5.5.1/activemq-all-5.5.1.jar 32 | - ~/jms/apache-activemq-5.5.1/lib/optional/slf4j-log4j12-1.5.11.jar 33 | - ~/jms/apache-activemq-5.5.1/lib/optional/log4j-1.2.14.jar 34 | :queue_name: TestQueue 35 | :topic_name: TestTopic 36 | 37 | activemq590: 38 | :factory: org.apache.activemq.ActiveMQConnectionFactory 39 | :broker_url: tcp://localhost:61616 40 | :require_jars: 41 | - /usr/local/Cellar/activemq/5.9.0/libexec/activemq-all-5.9.0.jar 42 | - /usr/local/Cellar/activemq/5.9.0/libexec/lib/optional/log4j-1.2.17.jar 43 | :queue_name: TestQueue 44 | :topic_name: TestTopic 45 | 46 | activemq5_11_1: 47 | :factory: org.apache.activemq.ActiveMQConnectionFactory 48 | :broker_url: tcp://localhost:61616 49 | :require_jars: 50 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 51 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 52 | :queue_name: TestQueue 53 | :topic_name: TestTopic 54 | 55 | hornetq2_4_0: 56 | # Connect to a local HornetQ Broker using JNDI 57 | :jndi_name: /ConnectionFactory 58 | :jndi_context: 59 | java.naming.factory.initial: org.jnp.interfaces.NamingContextFactory 60 | java.naming.provider.url: jnp://localhost:1099 61 | java.naming.factory.url.pkgs: org.jboss.naming:org.jnp.interfaces 62 | java.naming.security.principal: guest 63 | java.naming.security.credentials: guest 64 | :require_jars: 65 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-commons.jar 66 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-core-client.jar 67 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-jms-client.jar 68 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/jboss-jms-api.jar 69 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/jnp-client.jar 70 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/netty.jar 71 | :queue_name: TestQueue 72 | :topic_name: TestTopic 73 | 74 | hornetq222: 75 | # Connect to a local HornetQ Broker using JNDI 76 | :jndi_name: /ConnectionFactory 77 | :jndi_context: 78 | java.naming.factory.initial: org.jnp.interfaces.NamingContextFactory 79 | java.naming.provider.url: jnp://localhost:1099 80 | java.naming.factory.url.pkgs: org.jboss.naming:org.jnp.interfaces 81 | java.naming.security.principal: guest 82 | java.naming.security.credentials: guest 83 | :require_jars: 84 | - ~/jms/hornetq-2.2.2.Final/lib/hornetq-core-client.jar 85 | - ~/jms/hornetq-2.2.2.Final/lib/netty.jar 86 | - ~/jms/hornetq-2.2.2.Final/lib/hornetq-jms-client.jar 87 | - ~/jms/hornetq-2.2.2.Final/lib/jboss-jms-api.jar 88 | - ~/jms/hornetq-2.2.2.Final/lib/jnp-client.jar 89 | :queue_name: TestQueue 90 | :topic_name: TestTopic 91 | 92 | webspheremq7: 93 | :factory: com.ibm.mq.jms.MQConnectionFactory 94 | :queue_manager: MYQM 95 | :host_name: 127.0.0.1 96 | :channel: SRVCONCHA 97 | :port: 61414 98 | # Transport Type: com.ibm.mq.jms.JMSC::MQJMS_TP_CLIENT_MQ_TCPIP 99 | :transport_type: 1 100 | :username: mqm 101 | :require_jars: 102 | - ~/jms/libs-ibmqm7/com.ibm.mqjms.jar 103 | - ~/jms/libs-ibmqm7/jms.jar 104 | - ~/jms/libs-ibmqm7/com.ibm.mq.jmqi.jar 105 | - ~/jms/libs-ibmqm7/dhbcore.jar 106 | :queue_name: TestQueue 107 | :topic_name: TestTopic 108 | 109 | webspheremq6: 110 | :factory: com.ibm.mq.jms.MQConnectionFactory 111 | :queue_manager: MYQM 112 | :host_name: 127.0.0.1 113 | :channel: SRVCONCHA 114 | :port: 61414 115 | # Transport Type: com.ibm.mq.jms.JMSC::MQJMS_TP_CLIENT_MQ_TCPIP 116 | :transport_type: 1 117 | :username: mqm 118 | :require_jars: 119 | - ~/jms/libs-ibmqm6/com.ibm.mqjms.jar 120 | :queue_name: TestQueue 121 | :topic_name: TestTopic 122 | -------------------------------------------------------------------------------- /lib/jms/message_consumer.rb: -------------------------------------------------------------------------------- 1 | # Interface javax.jms.MessageConsumer 2 | module JMS::MessageConsumer 3 | # Obtain a message from the Destination or Topic 4 | # In JMS terms, the message is received from the Destination 5 | # :timeout follows the rules for MQSeries: 6 | # -1 : Wait forever 7 | # 0 : Return immediately if no message is available 8 | # x : Wait for x milli-seconds for a message to be received from the broker 9 | # Note: Messages may still be on the queue, but the broker has not supplied any messages 10 | # in the time interval specified 11 | # Default: 0 12 | # :buffered_message - consume Oracle AQ buffered message 13 | # Default: false 14 | def get(params={}) 15 | timeout = params[:timeout] || 0 16 | buffered_message = params[:buffered_message] || false 17 | if timeout == -1 18 | if buffered_message 19 | self.bufferReceive 20 | else 21 | self.receive 22 | end 23 | elsif timeout == 0 24 | if buffered_message 25 | self.bufferReceiveNoWait 26 | else 27 | self.receiveNoWait 28 | end 29 | else 30 | if buffered_message 31 | self.bufferReceive(timeout) 32 | else 33 | self.receive(timeout) 34 | end 35 | end 36 | end 37 | 38 | # For each message available to be consumed call the supplied block 39 | # Returns the statistics gathered when statistics: true, otherwise nil 40 | # 41 | # Parameters: 42 | # :timeout How to timeout waiting for messages on the Queue or Topic 43 | # -1 : Wait forever 44 | # 0 : Return immediately if no message is available 45 | # x : Wait for x milli-seconds for a message to be received from the broker 46 | # Note: Messages may still be on the queue, but the broker has not supplied any messages 47 | # in the time interval specified 48 | # Default: 0 49 | # 50 | # :statistics Capture statistics on how many messages have been read 51 | # true : This method will capture statistics on the number of messages received 52 | # and the time it took to process them. 53 | # The statistics can be reset by calling MessageConsumer::each again 54 | # with statistics: true 55 | # 56 | # The statistics gathered are returned when statistics: true and async: false 57 | def each(params={}, &block) 58 | raise(ArgumentError, 'Destination::each requires a code block to be executed for each message received') unless block 59 | 60 | message_count = nil 61 | start_time = nil 62 | 63 | if params[:statistics] 64 | message_count = 0 65 | start_time = Time.now 66 | end 67 | 68 | # Receive messages according to timeout 69 | while message = self.get(params) do 70 | block.call(message) 71 | message_count += 1 if message_count 72 | end 73 | 74 | unless message_count.nil? 75 | duration = Time.now - start_time 76 | { 77 | messages: message_count, 78 | duration: duration, 79 | messages_per_second: duration > 0 ? (message_count/duration).to_i : 0, 80 | ms_per_msg: message_count > 0 ? (duration*1000.0)/message_count : 0 81 | } 82 | end 83 | end 84 | 85 | # Receive messages in a separate thread when they arrive 86 | # Allows messages to be recieved in a separate thread. I.e. Asynchronously 87 | # This method will return to the caller before messages are processed. 88 | # It is then the callers responsibility to keep the program active so that messages 89 | # can then be processed. 90 | # 91 | # Parameters: 92 | # :statistics Capture statistics on how many messages have been read 93 | # true : This method will capture statistics on the number of messages received 94 | # and the time it took to process them. 95 | # The timer starts when each() is called and finishes when either the last message was received, 96 | # or when Destination::statistics is called. In this case MessageConsumer::statistics 97 | # can be called several times during processing without affecting the end time. 98 | # Also, the start time and message count is not reset until MessageConsumer::each 99 | # is called again with statistics: true 100 | # 101 | # The statistics gathered are returned when statistics: true and async: false 102 | # 103 | def on_message(params={}, &proc) 104 | raise(ArgumentError, 'MessageConsumer::on_message requires a code block to be executed for each message received') unless proc 105 | 106 | # Turn on Java class persistence: https://github.com/jruby/jruby/wiki/Persistence 107 | self.class.__persistent__ = true 108 | 109 | @listener = JMS::MessageListenerImpl.new(params, &proc) 110 | self.setMessageListener(@listener) 111 | end 112 | 113 | # Return the current statistics for a running MessageConsumer::on_message 114 | def on_message_statistics 115 | stats = @listener.statistics if @listener 116 | raise(ArgumentError, 'First call MessageConsumer::on_message with statistics: true before calling MessageConsumer::statistics()') unless stats 117 | stats 118 | end 119 | 120 | end 121 | -------------------------------------------------------------------------------- /examples/jms.yml: -------------------------------------------------------------------------------- 1 | # This YAML file contains the configuration options for several different 2 | # JMS Providers 3 | # 4 | # The Examples that ship with jruby-jms will use the entry 'activemq' unless 5 | # overriden at the command line. For example: 6 | # jruby producer.rb activemq 7 | # 8 | 9 | --- 10 | # Active MQ Centralized Broker 11 | activemq: 12 | :factory: org.apache.activemq.ActiveMQConnectionFactory 13 | :broker_url: tcp://127.0.0.1:61616 14 | :username: system 15 | :password: manager 16 | :require_jars: 17 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 18 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 19 | 20 | # ActiveMQ In VM Broker (Supports messaging within a JVM instance) 21 | activemq-invm: 22 | :factory: org.apache.activemq.ActiveMQConnectionFactory 23 | :broker_url: vm://mybroker 24 | :object_message_serialization_defered: true 25 | :require_jars: 26 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 27 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 28 | 29 | # ActiveMQ with failover to slave instance 30 | activemq-ha: 31 | :factory: org.apache.activemq.ActiveMQConnectionFactory 32 | :broker_url: failover://(tcp://msg1:61616,tcp://msg2:61616)?randomize=false&timeout=30000&initialReconnectDelay=100&useExponentialBackOff=true 33 | :require_jars: 34 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 35 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 36 | 37 | # JBoss 4 Messaging 38 | jboss: 39 | :jndi_name: ConnectionFactory 40 | :jndi_context: 41 | java.naming.factory.initial: org.jnp.interfaces.NamingContextFactory 42 | java.naming.provider.url: jnp://localhost:1099 43 | java.naming.security.principal: user 44 | java.naming.security.credentials: pwd 45 | :require_jars: 46 | - ~/Applications/jboss-messaging-client/1.4.0.SP3/javassist.jar 47 | - ~/Applications/jboss-messaging-client/1.4.0.SP3/jboss-aop-jdk50.jar 48 | - ~/Applications/jboss-messaging-client/1.4.0.SP3/jboss-messaging-client.jar 49 | - ~/Applications/jboss-messaging-client/1.4.0.SP3/jbossall-client.jar 50 | - ~/Applications/jboss-messaging-client/1.4.0.SP3/trove.jar 51 | 52 | # Apache Qpid 53 | qpid: 54 | :jndi_name: local 55 | :jndi_context: 56 | java.naming.factory.initial: org.apache.qpid.jndi.PropertiesFileInitialContextFactory 57 | connectionfactory.local: amqp://guest:guest@clientid/testpath?brokerlist='tcp://localhost:5672' 58 | :require_jars: 59 | - ~/Applications/javax.jms.jar 60 | - ~/Applications/qpid-0.8/lib/backport-util-concurrent-2.2.jar 61 | - ~/Applications/qpid-0.8/lib/commons-collections-3.2.jar 62 | - ~/Applications/qpid-0.8/lib/commons-lang-2.2.jar 63 | - ~/Applications/qpid-0.8/lib/mina-core-1.0.1.jar 64 | - ~/Applications/qpid-0.8/lib/qpid-client-0.8.jar 65 | - ~/Applications/qpid-0.8/lib/qpid-common-0.8.jar 66 | - ~/Applications/qpid-0.8/lib/slf4j-api-1.6.1.jar 67 | - ~/Applications/qpid-0.8/lib/log4j-1.2.12.jar 68 | - ~/Applications/qpid-0.8/lib/slf4j-log4j12-1.6.1.jar 69 | 70 | # HornetQ Broker 71 | hornetq: 72 | # Connect to a local HornetQ Broker using JNDI 73 | :jndi_name: /ConnectionFactory 74 | :jndi_context: 75 | java.naming.factory.initial: org.jnp.interfaces.NamingContextFactory 76 | java.naming.provider.url: jnp://localhost:1099 77 | java.naming.factory.url.pkgs: org.jboss.naming:org.jnp.interfaces 78 | java.naming.security.principal: guest 79 | java.naming.security.credentials: guest 80 | :require_jars: 81 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-commons.jar 82 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-core-client.jar 83 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/hornetq-jms-client.jar 84 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/jboss-jms-api.jar 85 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/jnp-client.jar 86 | - /usr/local/Cellar/hornetq/2.4.0/libexec/lib/netty.jar 87 | :queue_name: TestQueue 88 | :topic_name: TestTopic 89 | 90 | # Tibco EMS 91 | ems: 92 | :jndi_name: TestFactory 93 | :jndi_context: 94 | java.naming.factory.initial: com.tibco.tibjms.naming.TibjmsInitialContextFactory 95 | java.naming.provider.url: tcp://localhost:7222 96 | :require_jars: 97 | - C:\tibco\ems\8.0\lib\jms-2.0.jar 98 | - C:\tibco\ems\8.0\lib\tibjms.jar 99 | - C:\tibco\ems\8.0\lib\tibcrypt.jar 100 | 101 | # IBM WebSphere MQ 102 | wmq: 103 | :factory: com.ibm.mq.jms.MQQueueConnectionFactory 104 | :queue_manager: LOCAL 105 | :host_name: localhost 106 | :channel: MY.CLIENT.CHL 107 | :port: 1414 108 | # Transport Type: com.ibm.mq.jms.JMSC::MQJMS_TP_CLIENT_MQ_TCPIP 109 | :transport_type: 1 110 | :username: mqm 111 | :require_jars: 112 | - /opt/mqm/lib/com.ibm.mqjms.jar 113 | 114 | # Oracle AQ 9 115 | oracleaq: 116 | :factory: 'JMS::OracleAQConnectionFactory' 117 | :url: 'jdbc:oracle:thin:@hostname:1521:instanceid' 118 | :username: 'aquser' 119 | :password: 'mypassword' 120 | :require_jars: 121 | - ~/Applications/oraclestreams/ojdbc6.jar 122 | - ~/Applications/oraclestreams/jmscommon.jar 123 | - ~/Applications/oraclestreams/aqapi.jar 124 | - ~/Applications/oraclestreams/xdb.jar 125 | - ~/Applications/oraclestreams/jta.jar 126 | 127 | oracleaq_simple: 128 | :factory: 'JMS::OracleAQConnectionFactory' 129 | :url: 'jdbc:oracle:thin:aquser/mypassword@hostname:1521:instanceid' 130 | :require_jars: 131 | - ~/Applications/oraclestreams/ojdbc6.jar 132 | - ~/Applications/oraclestreams/jmscommon.jar 133 | - ~/Applications/oraclestreams/aqapi.jar 134 | - ~/Applications/oraclestreams/xdb.jar 135 | - ~/Applications/oraclestreams/jta.jar 136 | 137 | oracleaq_jndi: 138 | :jndi_name: ConnectionFactory 139 | :jndi_context: 140 | java.naming.factory.initial: oracle.jms.AQjmsInitialContextFactory 141 | java.naming.security.principal: aquser 142 | java.naming.security.credentials: mypassword 143 | db_url: jdbc:oracle:thin:@hostname:1521:instanceid 144 | :require_jars: 145 | - ~/Applications/oraclestreams/ojdbc6.jar 146 | - ~/Applications/oraclestreams/jmscommon.jar 147 | - ~/Applications/oraclestreams/aqapi.jar 148 | - ~/Applications/oraclestreams/xdb.jar 149 | - ~/Applications/oraclestreams/jta.jar 150 | -------------------------------------------------------------------------------- /lib/jms/session_pool.rb: -------------------------------------------------------------------------------- 1 | require 'gene_pool' 2 | 3 | module JMS 4 | # Since a Session can only be used by one thread at a time, we could create 5 | # a Session for every thread. That could result in excessive unused Sessions. 6 | # An alternative is to create a pool of sessions that can be shared by 7 | # multiple threads. 8 | # 9 | # Each thread can request a session and then return it once it is no longer 10 | # needed by that thread. The only way to get a session is pass a block so that 11 | # the Session is automatically returned to the pool upon completion of the block. 12 | # 13 | # Parameters: 14 | # see regular session parameters from: JMS::Connection#initialize 15 | # 16 | # Additional parameters for controlling the session pool itself 17 | # :pool_size Maximum Pool Size. Default: 10 18 | # The pool only grows as needed and will never exceed 19 | # :pool_size 20 | # :pool_timeout Number of seconds to wait before raising a TimeoutError 21 | # if no sessions are available in the poo 22 | # Default: 60 23 | # :pool_warn_timeout Number of seconds to wait before logging a warning when a 24 | # session in the pool is not available 25 | # Default: 5 26 | # :pool_name Name of the pool as it shows up in the logger. 27 | # Default: 'JMS::SessionPool' 28 | # Example: 29 | # session_pool = connection.create_session_pool(config) 30 | # session_pool.session do |session| 31 | # .... 32 | # end 33 | class SessionPool 34 | def initialize(connection, params={}) 35 | # Save Session params since it will be used every time a new session is 36 | # created in the pool 37 | session_params = params.nil? ? {} : params.dup 38 | logger = SemanticLogger[session_params[:pool_name] || self.class] 39 | 40 | # Use GenePool can create and manage the pool of sessions 41 | @pool = GenePool.new( 42 | name: '', 43 | pool_size: session_params[:pool_size] || 10, 44 | warn_timeout: session_params[:pool_warn_timeout] || 5, 45 | timeout: session_params[:pool_timeout] || 60, 46 | close_proc: nil, 47 | logger: logger 48 | ) do 49 | session = connection.create_session(session_params) 50 | # Turn on Java class persistence: https://github.com/jruby/jruby/wiki/Persistence 51 | session.class.__persistent__ = true 52 | session 53 | end 54 | 55 | # Handle connection failures 56 | connection.on_exception do |jms_exception| 57 | logger.error "JMS Connection Exception has occurred: #{jms_exception.inspect}" 58 | end 59 | end 60 | 61 | # Obtain a session from the pool and pass it to the supplied block 62 | # The session is automatically returned to the pool once the block completes 63 | # 64 | # In the event a JMS Exception is thrown the session will be closed and removed 65 | # from the pool to prevent re-using sessions that are no longer valid 66 | def session(&block) 67 | s = nil 68 | begin 69 | s = @pool.checkout 70 | block.call(s) 71 | rescue javax.jms.JMSException => e 72 | s.close rescue nil 73 | @pool.remove(s) 74 | s = nil # Do not check back in since we have removed it 75 | raise e 76 | ensure 77 | @pool.checkin(s) if s 78 | end 79 | end 80 | 81 | # Obtain a session from the pool and create a MessageConsumer. 82 | # Pass both into the supplied block. 83 | # Once the block is complete the consumer is closed and the session is 84 | # returned to the pool. 85 | # 86 | # Parameters: 87 | # queue_name: [String] Name of the Queue to return 88 | # [Symbol] Create temporary queue 89 | # Mandatory unless :topic_name is supplied 90 | # Or, 91 | # topic_name: [String] Name of the Topic to write to or subscribe to 92 | # [Symbol] Create temporary topic 93 | # Mandatory unless :queue_name is supplied 94 | # Or, 95 | # destination: [javaxJms::Destination] Destination to use 96 | # 97 | # selector: Filter which messages should be returned from the queue 98 | # Default: All messages 99 | # no_local: Determine whether messages published by its own connection 100 | # should be delivered to it 101 | # Default: false 102 | # 103 | # Example 104 | # session_pool.consumer(queue_name: 'MyQueue') do |session, consumer| 105 | # message = consumer.receive(timeout) 106 | # puts message.data if message 107 | # end 108 | def consumer(params, &block) 109 | session do |s| 110 | begin 111 | consumer = s.consumer(params) 112 | block.call(s, consumer) 113 | ensure 114 | consumer.close if consumer 115 | end 116 | end 117 | end 118 | 119 | # Obtain a session from the pool and create a MessageProducer. 120 | # Pass both into the supplied block. 121 | # Once the block is complete the producer is closed and the session is 122 | # returned to the pool. 123 | # 124 | # Parameters: 125 | # queue_name: [String] Name of the Queue to return 126 | # [Symbol] Create temporary queue 127 | # Mandatory unless :topic_name is supplied 128 | # Or, 129 | # topic_name: [String] Name of the Topic to write to or subscribe to 130 | # [Symbol] Create temporary topic 131 | # Mandatory unless :queue_name is supplied 132 | # Or, 133 | # destination: [javaxJms::Destination] Destination to use 134 | # 135 | # Example 136 | # session_pool.producer(queue_name: 'ExampleQueue') do |session, producer| 137 | # producer.send(session.message("Hello World")) 138 | # end 139 | def producer(params, &block) 140 | session do |s| 141 | begin 142 | producer = s.producer(params) 143 | block.call(s, producer) 144 | ensure 145 | producer.close if producer 146 | end 147 | end 148 | end 149 | 150 | # Immediately Close all sessions in the pool and release from the pool 151 | # 152 | # Note: This is an immediate close, active sessions will be aborted 153 | # 154 | # Note: Once closed a session pool cannot be re-used. A new instance must 155 | # be created 156 | def close 157 | @pool.each { |s| s.close } 158 | end 159 | 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jruby-jms 2 | ![](https://img.shields.io/gem/dt/jruby-jms.svg) ![](https://img.shields.io/badge/status-production%20ready-blue.svg) 3 | 4 | * http://github.com/reidmorrison/jruby-jms 5 | 6 | jruby-jms is a complete JRuby API into Java Messaging Specification (JMS) V1.1. 7 | 8 | Note: jruby-jms is for JRuby only. 9 | 10 | ### Design 11 | 12 | jruby-jms attempts to "rubify" the Java JMS API without 13 | compromising performance. It does this by sprinkling "Ruby-goodness" into the 14 | existing JMS Java interfaces, I.e. By adding Ruby methods to the existing 15 | classes and interfaces. Since jruby-jms exposes the JMS 16 | Java classes directly there is no performance impact that would have been 17 | introduced had the entire API been wrapped with an extra Ruby layer. 18 | 19 | In this way, using regular Ruby constructs a Ruby program can easily 20 | interact with JMS in a highly performant way. Also, in this way you are not 21 | limited to whatever the Ruby wrapper would have exposed, since the entire JMS 22 | API is available to you at any time. 23 | 24 | ### Install 25 | 26 | Add to Gemfile if using bundler: 27 | 28 | ```ruby 29 | gem 'jruby-jms' 30 | ``` 31 | 32 | Install using bundler: 33 | 34 | bundle 35 | 36 | If not using Bundler: 37 | 38 | gem install jruby-jms 39 | 40 | ### Documentation 41 | 42 | * [API Reference](http://www.rubydoc.info/gems/jruby-jms) 43 | 44 | ### Simplification 45 | 46 | One of the difficulties with the regular JMS API is that it uses completely 47 | separate classes for Topics and Queues in JMS 1.1. This means that once a 48 | program writes to a Queue for example, that without modifying the source code 49 | the program it cannot write to a topic. 50 | 51 | jruby-jms fixes this issue by allowing you to have a Consumer or Producer that 52 | is independent of whether it is producing or consuming to/from 53 | or a Topic or a Queue. The complexity of which JMS class is used is taken care 54 | of transparently by jruby-jms. 55 | 56 | ## Concepts & Terminology 57 | 58 | ### Java Message Service (JMS) API 59 | 60 | The JMS API is a standard interface part of Java EE 6 as a way for programs to 61 | send and receive messages through a messaging and queuing system. 62 | 63 | For more information on the JMS API: http://download.oracle.com/javaee/6/api/index.html?javax/jms/package-summary.html 64 | 65 | ### Broker / Queue Manager 66 | 67 | Depending on which JMS provider you are using they refer to their centralized 68 | server as either a Broker or Queue Manager. The Broker or Queue Manager is the 69 | centralized "server" through which all messages pass through. 70 | 71 | Some Brokers support an in-JVM broker instance so that messages can be passed 72 | between producers and consumers within the same Java Virtual Machine (JVM) 73 | instance. This removes the need to make any network calls. Useful 74 | for passing messages between threads in the same JVM. 75 | 76 | ### Connection 77 | 78 | In order to connect to any broker the Client JMS application must create a 79 | connection. In traditional JMS a ConnectionFactory is used to create connections. 80 | In jruby-jms the `JMS::Connection` takes care of the complexities of dealing with 81 | the factory class, just pass the required parameters to Connection.new and 82 | jruby-jms takes care of the rest. 83 | 84 | ### Queue 85 | 86 | A queue is used to hold messages. The queue must be defined prior to the message 87 | being sent and is used to hold the messages. The consumer does not have to be 88 | active in order to send messages. 89 | 90 | ### Topic 91 | 92 | Instead of sending messages to a single queue, a topic can be used to publish 93 | messages and allow multiple consumers to register for messages that match the 94 | topic they are interested in 95 | 96 | ### Producer 97 | 98 | Producers write messages to queues or topics 99 | 100 | ActiveMQ Example: 101 | 102 | ```ruby 103 | require 'jms' 104 | 105 | # Connect to ActiveMQ 106 | config = { 107 | :factory: org.apache.activemq.ActiveMQConnectionFactory 108 | :broker_url: tcp://localhost:61616 109 | :require_jars: 110 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 111 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 112 | } 113 | 114 | JMS::Connection.session(config) do |session| 115 | session.producer(queue_name: 'ExampleQueue') do |producer| 116 | producer.send(session.message("Hello World")) 117 | end 118 | end 119 | ``` 120 | 121 | ### Consumer 122 | 123 | Consumers read message from a queue or topic 124 | 125 | ActiveMQ Example: 126 | 127 | ```ruby 128 | require 'rubygems' 129 | require 'jms' 130 | 131 | # Connect to ActiveMQ 132 | config = { 133 | :factory: org.apache.activemq.ActiveMQConnectionFactory 134 | :broker_url: tcp://localhost:61616 135 | :require_jars: 136 | - /usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar 137 | - /usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar 138 | } 139 | 140 | JMS::Connection.session(config) do |session| 141 | session.consume(queue_name: 'ExampleQueue', timeout: 1000) do |message| 142 | p message 143 | end 144 | end 145 | ``` 146 | 147 | ## More Examples 148 | 149 | There are several more examples available at https://github.com/reidmorrison/jruby-jms/tree/master/examples 150 | 151 | ## Threading 152 | 153 | A `JMS::Connection` instance can be shared between threads, whereas a session, 154 | consumer, producer, and any artifacts created by the session should only be 155 | used by one thread at a time. 156 | 157 | For consumers, it is recommended to create a session for each thread and leave 158 | that thread blocked on `Consumer#receive`. Or, even better use `Connection#on_message` 159 | which will create a session, within which any message received from the specified 160 | queue or topic will be passed to the block. 161 | 162 | ## Logging 163 | 164 | jruby-jms uses [Semantic Logger](https://github.com/rocketjob/semantic_logger) for logging since it is 165 | fully thread-aware, uses in-memory queue based logging for performance, and has several other useful features. 166 | 167 | To enable Semantic Logger in a rails logger, include the gem [rails_semantic_logger](https://github.com/rocketjob/rails_semantic_logger) 168 | 169 | For standalone installations: 170 | 171 | ```ruby 172 | SemanticLogger.add_appender(file_name: 'test.log', formatter: :color) 173 | SemanticLogger.default_level = :info 174 | ``` 175 | 176 | ## Dependencies 177 | 178 | ### JMS V1.1 Provider 179 | 180 | In order to communicate with a JMS V 1.1 provider jruby-jms needs the jar files supplied 181 | by the JMS provider. As in the examples above the jar files can be specified in 182 | the configuration element `:require_jars`. Otherwise, the jars must be explicitly 183 | required in the Ruby code: 184 | 185 | ```ruby 186 | require '/usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar' 187 | require '/usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar' 188 | ``` 189 | 190 | ### JRuby 191 | 192 | jruby-jms has been tested against JRuby 1.7, and JRuby 9.0.0.0 193 | 194 | ## Versioning 195 | 196 | This project uses [Semantic Versioning](http://semver.org/). 197 | 198 | ## Author 199 | 200 | [Reid Morrison](https://github.com/reidmorrison) 201 | 202 | [Contributors](https://github.com/reidmorrison/jruby-jms/graphs/contributors) 203 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2008, 2009, 2010, 2011 J. Reid Morrison, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/jms/session.rb: -------------------------------------------------------------------------------- 1 | # For each thread that will be processing messages concurrently a separate 2 | # session is required. All sessions can share a single connection to the same 3 | # JMS Provider. 4 | # 5 | # Interface javax.jms.Session 6 | # 7 | # See: http://download.oracle.com/javaee/6/api/javax/jms/Session.html 8 | # 9 | # Other methods still directly accessible through this class: 10 | # 11 | # create_browser(queue, message_selector) 12 | # Creates a QueueBrowser object to peek at the messages on the specified queue using a message selector. 13 | # 14 | # create_bytes_message() 15 | # Creates a BytesMessage object 16 | # 17 | # create_consumer(destination) 18 | # Creates a MessageConsumer for the specified destination 19 | # See: Connection::consumer 20 | # 21 | # Example: 22 | # destination = session.create_destination(queue_name: "MyQueue") 23 | # session.create_consumer(destination) 24 | # 25 | # create_consumer(destination, message_selector) 26 | # Creates a MessageConsumer for the specified destination, using a message selector 27 | # 28 | # create_consumer(destination, message_selector, boolean NoLocal) 29 | # Creates MessageConsumer for the specified destination, using a message selector 30 | # 31 | # create_durable_subscriber(Topic topic, java.lang.String name) 32 | # Creates a durable subscriber to the specified topic 33 | # 34 | # create_durable_subscriber(Topic topic, java.lang.String name, java.lang.String messageSelector, boolean noLocal) 35 | # Creates a durable subscriber to the specified topic, using a message selector and specifying whether messages published by its own connection should be delivered to it. 36 | # 37 | # create_map_Message() 38 | # Creates a MapMessage object 39 | # 40 | # create_message() 41 | # Creates a Message object 42 | # 43 | # create_object_message() 44 | # Creates an ObjectMessage object 45 | # 46 | # create_object_message(java.io.Serializable object) 47 | # Creates an initialized ObjectMessage object 48 | # 49 | # create_producer(destination) 50 | # Creates a MessageProducer to send messages to the specified destination 51 | # 52 | # create_queue(queue_name) 53 | # Creates a queue identity given a Queue name 54 | # 55 | # create_stream_message() 56 | # Creates a StreamMessage object 57 | # 58 | # create_temporary_queue() 59 | # Creates a TemporaryQueue object 60 | # 61 | # create_temporary_topic() 62 | # Creates a TemporaryTopic object 63 | # 64 | # create_text_message() 65 | # Creates a TextMessage object 66 | # 67 | # create_text_message(text) 68 | # Creates an initialized TextMessage object 69 | # 70 | # create_topic(topic_name) 71 | # Creates a topic identity given a Topic name 72 | # 73 | # acknowledge_mode() 74 | # Returns the acknowledgement mode of the session 75 | # 76 | # message_listener() 77 | # Returns the session's distinguished message listener (optional). 78 | # 79 | # transacted? 80 | # Indicates whether the session is in transacted mode 81 | # 82 | # recover() 83 | # Stops message delivery in this session, and restarts message delivery with the oldest unacknowledged message 84 | # 85 | # rollback() 86 | # Rolls back any messages done in this transaction and releases any locks currently held 87 | # 88 | # message_listener=(MessageListener listener) 89 | # Sets the session's distinguished message listener (optional) 90 | # 91 | # unsubscribe(name) 92 | # Unsubscribes a durable subscription that has been created by a client 93 | # 94 | # Interface javax.jms.Session 95 | module JMS::Session 96 | # Create a new message instance based on the type of the data being supplied 97 | # String (:to_str) => TextMessage 98 | # Hash (:each_pair) => MapMessage 99 | # Duck typing is used to determine the type. If the class responds 100 | # to :to_str then it is considered a String. Similarly if it responds to 101 | # :each_pair it is considered to be a Hash 102 | # 103 | # If automated duck typing is not desired, the type of the message can be specified 104 | # by setting the parameter 'type' to any one of: 105 | # text: Creates a Text Message 106 | # map: Creates a Map Message 107 | # bytes: Creates a Bytes Message 108 | def message(data, type=nil) 109 | jms_message = nil 110 | type ||= 111 | if data.respond_to?(:to_str, false) 112 | :text 113 | elsif data.respond_to?(:each_pair, false) 114 | :map 115 | else 116 | raise "Unknown data type #{data.class.to_s} in Message" 117 | end 118 | 119 | case type 120 | when :text 121 | jms_message = self.createTextMessage 122 | jms_message.text = data.to_str 123 | when :map 124 | jms_message = self.createMapMessage 125 | jms_message.data = data 126 | when :bytes 127 | jms_message = self.createBytesMessage 128 | jms_message.write_bytes(data.to_java_bytes) 129 | else 130 | raise "Invalid type #{type} requested" 131 | end 132 | jms_message 133 | end 134 | 135 | # Create the destination based on the parameter supplied 136 | # 137 | # The idea behind this method is to allow the decision as to whether 138 | # one is sending to a topic or destination to be transparent to the code. 139 | # The supplied parameters can be externalized into say a YAML file 140 | # so that today it writes to a queue, later it can be changed to write 141 | # to a topic so that multiple parties can receive the same messages. 142 | # 143 | # Note: For Temporary Queues and Topics, remember to delete them when done 144 | # or just use ::destination instead with a block and it will take care 145 | # of deleting them for you 146 | # 147 | # To create a queue: 148 | # session.create_destination(queue_name: 'name of queue') 149 | # 150 | # To create a temporary queue: 151 | # session.create_destination(queue_name: :temporary) 152 | # 153 | # To create a queue: 154 | # session.create_destination('queue://queue_name') 155 | # 156 | # To create a topic: 157 | # session.create_destination(topic_name: 'name of queue') 158 | # 159 | # To create a temporary topic: 160 | # session.create_destination(topic_name: :temporary) 161 | # 162 | # To create a topic: 163 | # session.create_destination('topic://topic_name') 164 | # 165 | # Create the destination based on the parameter supplied 166 | # 167 | # Parameters: 168 | # queue_name: [String] Name of the Queue to return 169 | # [Symbol] Create temporary queue 170 | # Mandatory unless :topic_name is supplied 171 | # Or, 172 | # topic_name: [String] Name of the Topic to write to or subscribe to 173 | # [Symbol] Create temporary topic 174 | # Mandatory unless :queue_name is supplied 175 | # Or, 176 | # destination: [javaxJms::Destination] Destination to use 177 | # Returns the result of the supplied block 178 | def create_destination(params) 179 | # Allow a Java JMS destination object to be passed in 180 | return params[:destination] if params[:destination] && params[:destination].java_kind_of?(JMS::Destination) 181 | 182 | queue_name = nil 183 | topic_name = nil 184 | 185 | if params.is_a? String 186 | queue_name = params['queue://'.length..-1] if params.start_with?('queue://') 187 | topic_name = params['topic://'.length..-1] if params.start_with?('topic://') 188 | else 189 | # :q_name is deprecated 190 | queue_name = params[:queue_name] || params[:q_name] 191 | topic_name = params[:topic_name] 192 | end 193 | 194 | unless queue_name || topic_name 195 | raise(ArgumentError, 'Missing mandatory parameter :queue_name or :topic_name to Session::producer, Session::consumer, or Session::browser') 196 | end 197 | 198 | if queue_name 199 | queue_name == :temporary ? create_temporary_queue : create_queue(queue_name) 200 | else 201 | topic_name == :temporary ? create_temporary_topic : create_topic(topic_name) 202 | end 203 | end 204 | 205 | # Create a queue or topic to send or receive messages from 206 | # 207 | # A block must be supplied so that if it is a temporary topic or queue 208 | # it will be deleted after the block is complete 209 | # 210 | # To create a queue: 211 | # session.destination(queue_name: 'name of queue') 212 | # 213 | # To create a temporary queue: 214 | # session.destination(queue_name: :temporary) 215 | # 216 | # To create a topic: 217 | # session.destination(topic_name: 'name of queue') 218 | # 219 | # To create a temporary topic: 220 | # session.destination(topic_name: :temporary) 221 | # 222 | # Create the destination based on the parameter supplied 223 | # 224 | # Parameters: 225 | # queue_name: [String] Name of the Queue to return 226 | # [Symbol] Create temporary queue 227 | # Mandatory unless :topic_name is supplied 228 | # Or, 229 | # topic_name: [String] Name of the Topic to write to or subscribe to 230 | # [Symbol] Create temporary topic 231 | # Mandatory unless :queue_name is supplied 232 | # Or, 233 | # destination: [javaxJms::Destination] Destination to use 234 | # 235 | # Returns the result of the supplied block 236 | def destination(params={}, &block) 237 | raise(ArgumentError, 'Missing mandatory Block when calling JMS::Session#destination') unless block 238 | dest = nil 239 | begin 240 | dest = create_destination(params) 241 | block.call(dest) 242 | ensure 243 | # Delete Temporary Queue / Topic 244 | dest.delete if dest && dest.respond_to?(:delete) 245 | end 246 | end 247 | 248 | # Return the queue matching the queue name supplied 249 | # Call the Proc if supplied 250 | def queue(queue_name, &block) 251 | q = create_queue(queue_name) 252 | block.call(q) if block 253 | q 254 | end 255 | 256 | # Return a temporary queue 257 | # The temporary queue is deleted once the block completes 258 | # If no block is supplied then it should be deleted by the caller 259 | # when no longer needed 260 | def temporary_queue(&block) 261 | q = create_temporary_queue 262 | if block 263 | begin 264 | block.call(q) 265 | ensure 266 | # Delete Temporary queue on completion of block 267 | q.delete if q 268 | q = nil 269 | end 270 | end 271 | q 272 | end 273 | 274 | # Return the topic matching the topic name supplied 275 | # Call the Proc if supplied 276 | def topic(topic_name, &block) 277 | t = create_topic(topic_name) 278 | block.call(t) if block 279 | t 280 | end 281 | 282 | # Return a temporary topic 283 | # The temporary topic is deleted once the block completes 284 | # If no block is supplied then it should be deleted by the caller 285 | # when no longer needed 286 | def temporary_topic(&block) 287 | t = create_temporary_topic 288 | if block 289 | begin 290 | block.call(t) 291 | ensure 292 | # Delete Temporary topic on completion of block 293 | t.delete if t 294 | t = nil 295 | end 296 | end 297 | t 298 | end 299 | 300 | # Return a producer for the queue name supplied 301 | # A producer supports sending messages to a Queue or a Topic 302 | # 303 | # Call the Proc if supplied, then automatically close the producer 304 | # 305 | # Parameters: 306 | # queue_name: [String] Name of the Queue to return 307 | # [Symbol] Create temporary queue 308 | # Mandatory unless :topic_name is supplied 309 | # Or, 310 | # topic_name: [String] Name of the Topic to write to or subscribe to 311 | # [Symbol] Create temporary topic 312 | # Mandatory unless :queue_name is supplied 313 | # Or, 314 | # destination: [javaxJms::Destination] Destination to use 315 | def producer(params, &block) 316 | p = self.create_producer(self.create_destination(params)) 317 | if block 318 | begin 319 | block.call(p) 320 | ensure 321 | p.close 322 | p = nil 323 | end 324 | end 325 | p 326 | end 327 | 328 | # Return a consumer for the destination 329 | # A consumer can read messages from the queue or topic 330 | # 331 | # Call the block if supplied, then automatically close the consumer 332 | # 333 | # Parameters: 334 | # queue_name: [String] Name of the Queue to return 335 | # [Symbol] Create temporary queue 336 | # Mandatory unless :topic_name is supplied 337 | # Or, 338 | # topic_name: [String] Name of the Topic to write to or subscribe to 339 | # [Symbol] Create temporary topic 340 | # Mandatory unless :queue_name is supplied 341 | # Or, 342 | # destination: [javaxJms::Destination] Destination to use 343 | # 344 | # selector: Filter which messages should be returned from the queue 345 | # Default: All messages 346 | # no_local: Determine whether messages published by its own connection 347 | # should be delivered to it 348 | # Default: false 349 | def consumer(params, &block) 350 | destination = create_destination(params) 351 | c = 352 | if params[:no_local] 353 | create_consumer(destination, params[:selector] || '', params[:no_local]) 354 | elsif params[:selector] 355 | create_consumer(destination, params[:selector]) 356 | else 357 | create_consumer(destination) 358 | end 359 | 360 | if block 361 | begin 362 | block.call(c) 363 | ensure 364 | c.close 365 | c = nil 366 | end 367 | end 368 | c 369 | end 370 | 371 | # Consume all messages for the destination 372 | # A consumer can read messages from the queue or topic 373 | # 374 | # Parameters: 375 | # queue_name: [String] Name of the Queue to return 376 | # [Symbol] Create temporary queue 377 | # Mandatory unless :topic_name is supplied 378 | # Or, 379 | # topic_name: [String] Name of the Topic to write to or subscribe to 380 | # [Symbol] Create temporary topic 381 | # Mandatory unless :queue_name is supplied 382 | # Or, 383 | # destination: [javaxJms::Destination] Destination to use 384 | # 385 | # selector: Filter which messages should be returned from the queue 386 | # Default: All messages 387 | # no_local: Determine whether messages published by its own connection 388 | # should be delivered to it 389 | # Default: false 390 | # 391 | # :timeout Follows the rules for MQSeries: 392 | # -1 : Wait forever 393 | # 0 : Return immediately if no message is available 394 | # x : Wait for x milli-seconds for a message to be received from the broker 395 | # Note: Messages may still be on the queue, but the broker has not supplied any messages 396 | # in the time interval specified 397 | # Default: 0 398 | # 399 | def consume(params, &block) 400 | begin 401 | c = self.consumer(params) 402 | c.each(params, &block) 403 | ensure 404 | c.close if c 405 | end 406 | end 407 | 408 | # Return a browser for the destination 409 | # A browser can read messages non-destructively from the queue 410 | # It cannot browse Topics! 411 | # 412 | # Call the Proc if supplied, then automatically close the consumer 413 | # 414 | # Parameters: 415 | # queue_name: [String] Name of the Queue to return 416 | # [Symbol] Create temporary queue 417 | # Mandatory unless :topic_name is supplied 418 | # Or, 419 | # destination: [javaxJms::Destination] Destination to use 420 | # 421 | # selector: Filter which messages should be returned from the queue 422 | # Default: All messages 423 | def browser(params, &block) 424 | raise(ArgumentError, 'Session::browser requires a code block to be executed') unless block 425 | 426 | destination = create_destination(params) 427 | b = nil 428 | if params[:selector] 429 | b = create_browser(destination, params[:selector]) 430 | else 431 | b = create_browser(destination) 432 | end 433 | 434 | if block 435 | begin 436 | block.call(b) 437 | ensure 438 | b.close 439 | b = nil 440 | end 441 | end 442 | b 443 | end 444 | 445 | # Browse the specified queue, calling the Proc supplied for each message found 446 | # 447 | # Parameters: 448 | # queue_name: [String] Name of the Queue to return 449 | # [Symbol] Create temporary queue 450 | # Mandatory unless :topic_name is supplied 451 | # Or, 452 | # destination: [javaxJms::Destination] Destination to use 453 | # 454 | # selector: Filter which messages should be returned from the queue 455 | # Default: All messages 456 | def browse(params={}, &block) 457 | self.browser(params) { |b| b.each(params, &block) } 458 | end 459 | end 460 | -------------------------------------------------------------------------------- /lib/jms/connection.rb: -------------------------------------------------------------------------------- 1 | require 'semantic_logger' 2 | # Module: Java Messaging System (JMS) Interface 3 | module JMS 4 | # Every JMS session must have at least one Connection instance 5 | # A Connection instance represents a connection between this client application 6 | # and the JMS Provider (server/queue manager/broker). 7 | # A connection is distinct from a Session, in that multiple Sessions can share a 8 | # single connection. Also, unit of work control (commit/rollback) is performed 9 | # at the Session level. 10 | # 11 | # Since many JRuby applications will only have one connection and one session 12 | # several convenience methods have been added to support creating both the 13 | # Session and Connection objects automatically. 14 | # 15 | # For Example, to read all messages from a queue and then terminate: 16 | # require 'rubygems' 17 | # require 'jms' 18 | # 19 | # JMS::Connection.create_session( 20 | # factory: 'org.apache.activemq.ActiveMQConnectionFactory', 21 | # broker_url: 'tcp://localhost:61616', 22 | # require_jars: [ 23 | # '/usr/local/Cellar/activemq/5.11.1/libexec/activemq-all-5.11.1.jar', 24 | # '/usr/local/Cellar/activemq/5.11.1/libexec/lib/optional/log4j-1.2.17.jar' 25 | # ] 26 | # ) do |session| 27 | # session.consumer(:queue_name=>'TEST') do |consumer| 28 | # if message = consumer.receive_no_wait 29 | # puts "Data Received: #{message.data}" 30 | # else 31 | # puts 'No message available' 32 | # end 33 | # end 34 | # end 35 | # 36 | # The above code creates a Connection and then a Session. Once the block completes 37 | # the session is closed and the Connection disconnected. 38 | # 39 | # See: http://download.oracle.com/javaee/6/api/javax/jms/Connection.html 40 | # 41 | class Connection 42 | include SemanticLogger::Loggable 43 | 44 | # Create a connection to the JMS provider, start the connection, 45 | # call the supplied code block, then close the connection upon completion 46 | # 47 | # Returns the result of the supplied block 48 | def self.start(params = {}, &block) 49 | raise(ArgumentError, 'Missing mandatory Block when calling JMS::Connection.start') unless block 50 | connection = Connection.new(params) 51 | connection.start 52 | begin 53 | block.call(connection) 54 | ensure 55 | connection.close 56 | end 57 | end 58 | 59 | # Connect to a JMS Broker, create and start the session, 60 | # then call the code block passing in the session. 61 | # Both the Session and Connection are closed on termination of the block 62 | # 63 | # Shortcut convenience method to both connect to the broker and create a session 64 | # Useful when only a single session is required in the current thread 65 | # 66 | # Note: It is important that each thread have its own session to support transactions 67 | # This method will also start the session immediately so that any 68 | # consumers using this session will start immediately 69 | def self.session(params = {}, &block) 70 | self.start(params) do |connection| 71 | connection.session(params, &block) 72 | end 73 | end 74 | 75 | # Load the required jar files for this JMS Provider and 76 | # load JRuby extensions for those classes 77 | # 78 | # Rather than copying the JMS jar files into the JRuby lib, load them 79 | # on demand. JRuby JMS extensions are only loaded once the jar files have been 80 | # loaded. 81 | # 82 | # Can be called multiple times if required, although it would not be performant 83 | # to do so regularly. 84 | # 85 | # Parameter: jar_list is an Array of the path and filenames to jar files 86 | # to load for this JMS Provider 87 | # 88 | # Returns nil 89 | def fetch_dependencies(jar_list) 90 | jar_list.each do |jar| 91 | logger.debug "Loading Jar File:#{jar}" 92 | begin 93 | require jar 94 | rescue Exception => exc 95 | logger.error "Failed to Load Jar File:#{jar}", exc 96 | end 97 | end if jar_list 98 | 99 | require 'jms/mq_workaround' 100 | require 'jms/imports' 101 | require 'jms/message_listener_impl' 102 | require 'jms/message' 103 | require 'jms/text_message' 104 | require 'jms/map_message' 105 | require 'jms/bytes_message' 106 | require 'jms/object_message' 107 | require 'jms/session' 108 | require 'jms/message_consumer' 109 | require 'jms/message_producer' 110 | require 'jms/queue_browser' 111 | end 112 | 113 | # Create a connection to the JMS provider 114 | # 115 | # Note: Connection::start must be called before any consumers will be 116 | # able to receive messages 117 | # 118 | # In JMS we need to start by obtaining the JMS Factory class that is supplied 119 | # by the JMS Vendor. 120 | # 121 | # There are 3 ways to establish a connection to a JMS Provider 122 | # 1. Supply the name of the JMS Providers Factory Class 123 | # 2. Supply an instance of the JMS Provider class itself 124 | # 3. Use a JNDI lookup to return the JMS Provider Factory class 125 | # Parameters: 126 | # factory: [String] Name of JMS Provider Factory class 127 | # [Class] JMS Provider Factory class itself 128 | # 129 | # jndi_name: [String] Name of JNDI entry at which the Factory can be found 130 | # jndi_context: Mandatory if jndi lookup is being used, contains details 131 | # on how to connect to JNDI server etc. 132 | # 133 | # require_jars: [Array] An optional array of Jar file names to load for the specified 134 | # JMS provider. By using this option it is not necessary 135 | # to put all the JMS Provider specific jar files into the 136 | # environment variable CLASSPATH prior to starting JRuby 137 | # 138 | # username: [String] Username to connect to JMS provider with 139 | # password: [String] Password to use when to connecting to the JMS provider 140 | # Note: :password is ignored if :username is not supplied 141 | # 142 | # :factory and :jndi_name are mutually exclusive, both cannot be supplied at the 143 | # same time. :factory takes precedence over :jndi_name 144 | # 145 | # JMS Provider specific properties can be set if the JMS Factory itself 146 | # has setters for those properties. 147 | # 148 | # For some known examples, see: [Example jms.yml](https://github.com/reidmorrison/jruby-jms/blob/master/examples/jms.yml) 149 | def initialize(params = {}) 150 | # Used by #on_message 151 | @sessions = [] 152 | @consumers = [] 153 | 154 | options = params.dup 155 | 156 | # Load Jar files on demand so that they do not need to be in the CLASSPATH 157 | # of JRuby lib directory 158 | fetch_dependencies(options.delete(:require_jars)) 159 | 160 | connection_factory = nil 161 | factory = options.delete(:factory) 162 | if factory 163 | # If factory check if oracle is needed. 164 | require('jms/oracle_a_q_connection_factory') if factory.include?('AQjmsFactory') 165 | 166 | # If factory is a string, then it is the name of a class, not the class itself 167 | factory = eval(factory) if factory.respond_to?(:to_str) 168 | connection_factory = factory.new 169 | elsif jndi_name = options[:jndi_name] 170 | raise(ArgumentError, 'Missing mandatory parameter :jndi_context in call to Connection::connect') unless jndi_context = options[:jndi_context] 171 | if jndi_context['java.naming.factory.initial'].include?('AQjmsInitialContextFactory') 172 | require 'jms/oracle_a_q_connection_factory' 173 | end 174 | 175 | jndi = javax.naming.InitialContext.new(java.util.Hashtable.new(jndi_context)) 176 | begin 177 | connection_factory = jndi.lookup jndi_name 178 | ensure 179 | jndi.close 180 | end 181 | else 182 | raise(ArgumentError, 'Missing mandatory parameter :factory or :jndi_name missing in call to Connection::connect') 183 | end 184 | options.delete(:jndi_name) 185 | options.delete(:jndi_context) 186 | 187 | logger.debug "Using Factory: #{connection_factory.java_class}" if connection_factory.respond_to? :java_class 188 | options.each_pair do |key, val| 189 | next if [:username, :password].include?(key) 190 | 191 | method = key.to_s+'=' 192 | if connection_factory.respond_to? method 193 | connection_factory.send method, val 194 | logger.debug " #{key} = #{connection_factory.send key.to_sym}" if connection_factory.respond_to? key.to_sym 195 | else 196 | logger.warn "#{connection_factory.java_class} does not understand option: :#{key}=#{val}, ignoring :#{key}" if connection_factory.respond_to? :java_class 197 | end 198 | end 199 | 200 | # Check for username and password 201 | if options[:username] 202 | @jms_connection = connection_factory.create_connection(options[:username], options[:password]) 203 | else 204 | @jms_connection = connection_factory.create_connection 205 | end 206 | end 207 | 208 | # Start (or restart) delivery of incoming messages over this connection. 209 | # By default no messages are delivered until this method is called explicitly 210 | # Delivery of messages to any asynchronous Destination::each() call will only 211 | # start after Connection::start is called, or Connection.start is used 212 | def start 213 | @jms_connection.start 214 | end 215 | 216 | # Temporarily stop delivery of incoming messages on this connection 217 | # Useful during a hot code update or other changes that need to be completed 218 | # without any new messages being processed 219 | # Call start() to resume receiving messages 220 | def stop 221 | @jms_connection.stop 222 | end 223 | 224 | # Create a session over this connection. 225 | # It is recommended to create separate sessions for each thread 226 | # If a block of code is passed in, it will be called and then the session is automatically 227 | # closed on completion of the code block 228 | # 229 | # Parameters: 230 | # transacted: [true|false] 231 | # Determines whether transactions are supported within this session. 232 | # I.e. Whether commit or rollback can be called 233 | # Default: false 234 | # Note: :options below are ignored if this value is set to :true 235 | # 236 | # options: any of the JMS::Session constants: 237 | # Note: :options are ignored if transacted: true 238 | # JMS::Session::AUTO_ACKNOWLEDGE 239 | # With this acknowledgment mode, the session automatically acknowledges 240 | # a client's receipt of a message either when the session has successfully 241 | # returned from a call to receive or when the message listener the session has 242 | # called to process the message successfully returns. 243 | # JMS::Session::CLIENT_ACKNOWLEDGE 244 | # With this acknowledgment mode, the client acknowledges a consumed 245 | # message by calling the message's acknowledge method. 246 | # JMS::Session::DUPS_OK_ACKNOWLEDGE 247 | # This acknowledgment mode instructs the session to lazily acknowledge 248 | # the delivery of messages. 249 | # JMS::Session::SESSION_TRANSACTED 250 | # This value is returned from the method getAcknowledgeMode if the 251 | # session is transacted. 252 | # Default: JMS::Session::AUTO_ACKNOWLEDGE 253 | # 254 | def session(params={}, &block) 255 | raise(ArgumentError, 'Missing mandatory Block when calling JMS::Connection#session') unless block 256 | session = self.create_session(params) 257 | begin 258 | block.call(session) 259 | ensure 260 | session.close 261 | end 262 | end 263 | 264 | # Create a session over this connection. 265 | # It is recommended to create separate sessions for each thread 266 | # 267 | # Note: Remember to call close on the returned session when it is no longer 268 | # needed. Rather use JMS::Connection#session with a block whenever 269 | # possible 270 | # 271 | # Parameters: 272 | # transacted: true or false 273 | # Determines whether transactions are supported within this session. 274 | # I.e. Whether commit or rollback can be called 275 | # Default: false 276 | # Note: :options below are ignored if this value is set to :true 277 | # 278 | # options: any of the JMS::Session constants: 279 | # Note: :options are ignored if transacted: true 280 | # JMS::Session::AUTO_ACKNOWLEDGE 281 | # With this acknowledgment mode, the session automatically acknowledges 282 | # a client's receipt of a message either when the session has successfully 283 | # returned from a call to receive or when the message listener the session has 284 | # called to process the message successfully returns. 285 | # JMS::Session::CLIENT_ACKNOWLEDGE 286 | # With this acknowledgment mode, the client acknowledges a consumed 287 | # message by calling the message's acknowledge method. 288 | # JMS::Session::DUPS_OK_ACKNOWLEDGE 289 | # This acknowledgment mode instructs the session to lazily acknowledge 290 | # the delivery of messages. 291 | # JMS::Session::SESSION_TRANSACTED 292 | # This value is returned from the method getAcknowledgeMode if the 293 | # session is transacted. 294 | # Default: JMS::Session::AUTO_ACKNOWLEDGE 295 | # 296 | def create_session(params={}) 297 | transacted = params[:transacted] || false 298 | options = params[:options] || JMS::Session::AUTO_ACKNOWLEDGE 299 | @jms_connection.create_session(transacted, options) 300 | end 301 | 302 | # Close connection with the JMS Provider 303 | # First close any consumers or sessions that are active as a result of JMS::Connection::on_message 304 | def close 305 | @consumers.each { |consumer| consumer.close } if @consumers 306 | @consumers = [] 307 | 308 | @sessions.each { |session| session.close } if @sessions 309 | @session=[] 310 | 311 | @jms_connection.close if @jms_connection 312 | end 313 | 314 | # Gets the client identifier for this connection. 315 | def client_id 316 | @jms_connection.getClientID 317 | end 318 | 319 | # Sets the client identifier for this connection. 320 | def client_id=(client_id) 321 | @jms_connection.setClientID(client_id) 322 | end 323 | 324 | # Returns the ExceptionListener object for this connection 325 | # Returned class implements interface JMS::ExceptionListener 326 | def exception_listener 327 | @jms_connection.getExceptionListener 328 | end 329 | 330 | # Sets an exception listener for this connection 331 | # See ::on_exception to set a Ruby Listener 332 | # Returns: nil 333 | def exception_listener=(listener) 334 | @jms_connection.setExceptionListener(listener) 335 | end 336 | 337 | # Whenever an exception occurs the supplied block is called 338 | # This is important when Connection::on_message has been used, since 339 | # failures to the connection would be lost otherwise 340 | # 341 | # For details on the supplied parameter when the block is called, 342 | # see: http://download.oracle.com/javaee/6/api/javax/jms/JMSException.html 343 | # 344 | # Example: 345 | # connection.on_exception do |jms_exception| 346 | # puts "JMS Exception has occurred: #{jms_exception}" 347 | # end 348 | # 349 | # Returns: nil 350 | def on_exception(&block) 351 | @jms_connection.setExceptionListener(block) 352 | end 353 | 354 | # Gets the metadata for this connection 355 | # see: http://download.oracle.com/javaee/6/api/javax/jms/ConnectionMetaData.html 356 | def meta_data 357 | @jms_connection.getMetaData 358 | end 359 | 360 | # Return a string describing the JMS provider and version 361 | def to_s 362 | md = @jms_connection.getMetaData 363 | "JMS::Connection provider: #{md.getJMSProviderName} v#{md.getProviderVersion}, JMS v#{md.getJMSVersion}" 364 | end 365 | 366 | # Receive messages in a separate thread when they arrive 367 | # 368 | # Allows messages to be received Asynchronously in a separate thread. 369 | # This method will return to the caller before messages are processed. 370 | # It is then the callers responsibility to keep the program active so that messages 371 | # can then be processed. 372 | # 373 | # Session Parameters: 374 | # transacted: true or false 375 | # Determines whether transactions are supported within this session. 376 | # I.e. Whether commit or rollback can be called 377 | # Default: false 378 | # Note: :options below are ignored if this value is set to :true 379 | # 380 | # options: any of the JMS::Session constants: 381 | # Note: :options are ignored if transacted: true 382 | # JMS::Session::AUTO_ACKNOWLEDGE 383 | # With this acknowledgment mode, the session automatically acknowledges 384 | # a client's receipt of a message either when the session has successfully 385 | # returned from a call to receive or when the message listener the session has 386 | # called to process the message successfully returns. 387 | # JMS::Session::CLIENT_ACKNOWLEDGE 388 | # With this acknowledgment mode, the client acknowledges a consumed 389 | # message by calling the message's acknowledge method. 390 | # JMS::Session::DUPS_OK_ACKNOWLEDGE 391 | # This acknowledgment mode instructs the session to lazily acknowledge 392 | # the delivery of messages. 393 | # JMS::Session::SESSION_TRANSACTED 394 | # This value is returned from the method getAcknowledgeMode if the 395 | # session is transacted. 396 | # Default: JMS::Session::AUTO_ACKNOWLEDGE 397 | # 398 | # :session_count : Number of sessions to create, each with their own consumer which 399 | # in turn will call the supplied code block. 400 | # Note: The supplied block must be thread safe since it will be called 401 | # by several threads at the same time. 402 | # I.e. Don't change instance variables etc. without the 403 | # necessary semaphores etc. 404 | # Default: 1 405 | # 406 | # Consumer Parameters: 407 | # queue_name: String: Name of the Queue to return 408 | # Symbol: temporary: Create temporary queue 409 | # Mandatory unless :topic_name is supplied 410 | # Or, 411 | # topic_name: String: Name of the Topic to write to or subscribe to 412 | # Symbol: temporary: Create temporary topic 413 | # Mandatory unless :queue_name is supplied 414 | # Or, 415 | # destination:Explicit javaxJms::Destination to use 416 | # 417 | # selector: Filter which messages should be returned from the queue 418 | # Default: All messages 419 | # 420 | # no_local: Determine whether messages published by its own connection 421 | # should be delivered to the supplied block 422 | # Default: false 423 | # 424 | # :statistics Capture statistics on how many messages have been read 425 | # true : This method will capture statistics on the number of messages received 426 | # and the time it took to process them. 427 | # The timer starts when each() is called and finishes when either the last message was received, 428 | # or when Destination::statistics is called. In this case MessageConsumer::statistics 429 | # can be called several times during processing without affecting the end time. 430 | # Also, the start time and message count is not reset until MessageConsumer::each 431 | # is called again with statistics: true 432 | # 433 | # Usage: For transacted sessions the block supplied must return either true or false: 434 | # true => The session is committed 435 | # false => The session is rolled back 436 | # Any Exception => The session is rolled back 437 | # 438 | # Note: Separately invoke Connection#on_exception so that connection failures can be handled 439 | # since on_message will Not be called if the connection is lost 440 | # 441 | def on_message(params, &block) 442 | raise 'JMS::Connection must be connected prior to calling JMS::Connection::on_message' unless @sessions && @consumers 443 | 444 | consumer_count = params[:session_count] || 1 445 | consumer_count.times do 446 | session = self.create_session(params) 447 | consumer = session.consumer(params) 448 | if session.transacted? 449 | consumer.on_message(params) do |message| 450 | begin 451 | block.call(message) ? session.commit : session.rollback 452 | rescue => exc 453 | session.rollback 454 | throw exc 455 | end 456 | end 457 | else 458 | consumer.on_message(params, &block) 459 | end 460 | @consumers << consumer 461 | @sessions << session 462 | end 463 | end 464 | 465 | # Return the statistics for every active Connection#on_message consumer 466 | # in an Array 467 | # 468 | # For details on the contents of each element in the array, see: Consumer#on_message_statistics 469 | def on_message_statistics 470 | @consumers.collect { |consumer| consumer.on_message_statistics } 471 | end 472 | 473 | # Since a Session can only be used by one thread at a time, we could create 474 | # a Session for every thread. That could result in excessive unused Sessions. 475 | # An alternative is to create a pool of sessions that can be shared by 476 | # multiple threads. 477 | # 478 | # Each thread can request a session and then return it once it is no longer 479 | # needed by that thread. The only way to get a session is to pass a block so that 480 | # the Session is automatically returned to the pool upon completion of the block. 481 | # 482 | # Parameters: 483 | # see regular session parameters from: JMS::Connection#initialize 484 | # 485 | # Additional parameters for controlling the session pool itself 486 | # :pool_size Maximum Pool Size. Default: 10 487 | # The pool only grows as needed and will never exceed 488 | # :pool_size 489 | # :pool_warn_timeout Number of seconds to wait before logging a warning when a 490 | # session in the pool is not available. Measured in seconds 491 | # Default: 5.0 492 | # :pool_name Name of the pool as it shows up in the logger. 493 | # Default: 'JMS::SessionPool' 494 | # Example: 495 | # session_pool = connection.create_session_pool(config) 496 | # 497 | # session_pool.session do |session| 498 | # producer.send(session.message("Hello World")) 499 | # end 500 | def create_session_pool(params={}) 501 | JMS::SessionPool.new(self, params) 502 | end 503 | 504 | end 505 | 506 | end 507 | --------------------------------------------------------------------------------