├── project ├── build.properties └── plugins.sbt ├── .scala-steward.conf ├── site ├── src │ └── main │ │ └── resources │ │ └── microsite │ │ ├── img │ │ ├── favicon.png │ │ ├── navbar_brand.png │ │ ├── navbar_brand2x.png │ │ ├── sidebar_brand.png │ │ └── sidebar_brand2x.png │ │ └── data │ │ └── menu.yml └── docs │ ├── providers │ ├── index.md │ ├── ibm-mq │ │ └── index.md │ └── active-mq-artemis │ │ └── index.md │ └── programs │ ├── auto-ack-consumer │ └── index.md │ ├── ack-consumer │ └── index.md │ ├── tx-consumer │ └── index.md │ ├── index.md │ └── producer │ └── index.md ├── .gitignore ├── .github └── workflows │ ├── scala-steward.yml │ ├── clean.yml │ └── ci.yml ├── scripts ├── definitions.mqsc └── broker-00.xml ├── .mergify.yml ├── tests └── src │ ├── main │ └── resources │ │ └── log4j2.xml │ └── test │ └── scala │ └── jms4s │ ├── jms │ ├── IbmMQJmsSpec.scala │ ├── ActiveMQArtemisJmsSpec.scala │ └── JmsSpec.scala │ ├── IbmMQJmsClientSpec.scala │ ├── ActiveMQArtemisJmsClientSpec.scala │ ├── basespec │ ├── providers │ │ ├── IbmMQBaseSpec.scala │ │ └── ActiveMQArtemisBaseSpec.scala │ └── Jms4sBaseSpec.scala │ └── JmsClientSpec.scala ├── .scalafmt.conf ├── CODE_OF_CONDUCT.md ├── LICENSE ├── docker-compose.yml ├── core └── src │ └── main │ └── scala │ └── jms4s │ ├── jms │ ├── utils │ │ └── TryUtils.scala │ ├── JmsMessageConsumer.scala │ ├── JmsDestination.scala │ ├── MessageFactory.scala │ ├── PooledConsumer.scala │ ├── JmsContext.scala │ └── JmsMessage.scala │ ├── model.scala │ ├── config │ └── config.scala │ ├── JmsClient.scala │ ├── JmsAutoAcknowledgerConsumer.scala │ ├── JmsAcknowledgerConsumer.scala │ ├── JmsTransactedConsumer.scala │ └── JmsProducer.scala ├── examples └── src │ └── main │ └── scala │ ├── AutoAckConsumerExample.scala │ ├── AckConsumerExample.scala │ ├── TransactedConsumerExample.scala │ └── ProducerExample.scala ├── README.md ├── active-mq-artemis └── src │ └── main │ └── scala │ └── jms4s │ └── activemq │ └── activeMQ.scala └── ibm-mq └── src └── main └── scala └── jms4s └── ibmmq └── ibmMQ.scala /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 2 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ 2 | {groupId = "org.scalameta", artifactId = "scalafmt-core"} 3 | ] 4 | -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fp-in-bo/jms4s/HEAD/site/src/main/resources/microsite/img/favicon.png -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/navbar_brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fp-in-bo/jms4s/HEAD/site/src/main/resources/microsite/img/navbar_brand.png -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/navbar_brand2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fp-in-bo/jms4s/HEAD/site/src/main/resources/microsite/img/navbar_brand2x.png -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/sidebar_brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fp-in-bo/jms4s/HEAD/site/src/main/resources/microsite/img/sidebar_brand.png -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/sidebar_brand2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fp-in-bo/jms4s/HEAD/site/src/main/resources/microsite/img/sidebar_brand2x.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | # vim 4 | *.sw? 5 | 6 | # Ignore [ce]tags files 7 | tags 8 | 9 | .bloop 10 | .metals 11 | 12 | FFDC/ 13 | mqjms.log.* 14 | .bsp/ -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.0") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 3 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") 4 | addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.4") 5 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.0") 6 | addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.23.0") 7 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Scala Steward 3 | 4 | # This workflow will launch at 00:00 every Sunday 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | scala-steward: 10 | runs-on: ubuntu-latest 11 | name: Launch Scala Steward 12 | steps: 13 | - name: Launch Scala Steward 14 | uses: scala-steward-org/scala-steward-action@v2.20.0 15 | with: 16 | github-token: ${{ secrets.SCALA_STEWARD_JMS4S }} 17 | -------------------------------------------------------------------------------- /scripts/definitions.mqsc: -------------------------------------------------------------------------------- 1 | DEFINE QLOCAL(DEV.QUEUE.1) REPLACE; 2 | DEFINE QLOCAL(DEV.QUEUE.2) REPLACE; 3 | DEFINE QLOCAL(DEV.QUEUE.3) REPLACE; 4 | 5 | DEFINE TOPIC ('DEV.CUSTOM.TOPIC.1') TYPE(LOCAL) TOPICSTR('dev1/') REPLACE; 6 | DEFINE TOPIC ('DEV.CUSTOM.TOPIC.2') TYPE(LOCAL) TOPICSTR('dev2/') REPLACE; 7 | 8 | SET AUTHREC PROFILE('SYSTEM.DEFAULT.MODEL.QUEUE') OBJTYPE(QUEUE) PRINCIPAL('app') AUTHADD(BROWSE,INQ,GET,PUT,SET) 9 | SET AUTHREC PROFILE('SYSTEM.BASE.TOPIC') OBJTYPE(TOPIC) PRINCIPAL('app') AUTHADD(SUB,RESUME,PUB) 10 | -------------------------------------------------------------------------------- /site/docs/providers/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Providers" 4 | position: 2 5 | --- 6 | 7 | # Providers 8 | 9 | Currently supported: 10 | 11 | - **[Active MQ Artemis](./active-mq-artemis/)** 12 | - **[IBM MQ](./ibm-mq/)** 13 | 14 | Supporting a new provider is a task which should be pretty straightforward. 15 | 16 | I you need a provider which is missing you can: 17 | 18 | - **Try contributing!** PRs are always welcome! 19 | - Raise an issue. Let us know, someone may eventually pick that up or we can guide you to a complete PR. 20 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatically merge scala-steward's PRs 3 | conditions: 4 | - head~=^update/ 5 | - author=scala-steward 6 | - status-success=Build Success (ubuntu-latest, 2.13.10, adopt-hotspot@8) 7 | actions: 8 | delete_head_branch: 9 | force: true 10 | queue: 11 | method: rebase 12 | name: default 13 | 14 | queue_rules: 15 | - name: default 16 | conditions: 17 | # Conditions to get out of the queue (= merged) 18 | - check-success=Build Success (ubuntu-latest, 2.13.10, adopt-hotspot@8) 19 | -------------------------------------------------------------------------------- /tests/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ${sys:root-level:-INFO} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.4.2" 2 | 3 | maxColumn = 120 4 | align = most 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings = JavaDoc 8 | lineEndings = preserve 9 | includeCurlyBraceInSelectChains = false 10 | danglingParentheses = true 11 | 12 | continuationIndent { 13 | callSite = 2 14 | defnSite = 2 15 | } 16 | newlines { 17 | alwaysBeforeTopLevelStatements = true 18 | } 19 | spaces { 20 | inImportCurlyBraces = true 21 | } 22 | optIn.annotationNewlines = true 23 | 24 | rewrite.rules = [ 25 | SortImports 26 | ,SortModifiers 27 | ,RedundantParens 28 | ,RedundantBraces 29 | ,PreferCurlyFors 30 | ] 31 | -------------------------------------------------------------------------------- /site/src/main/resources/microsite/data/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Programs 3 | url: programs/ 4 | menu_section: programs 5 | 6 | nested_options: 7 | - title: Transacted Consumer 8 | url: programs/tx-consumer 9 | - title: Acknowledger Consumer 10 | url: programs/ack-consumer 11 | - title: Auto Acknowledger Consumer 12 | url: programs/auto-ack-consumer 13 | - title: Producer 14 | url: programs/producer 15 | 16 | - title: Providers 17 | url: providers/ 18 | menu_section: providers 19 | 20 | nested_options: 21 | - title: IBM MQ 22 | url: providers/ibm-mq 23 | - title: Active MQ Artemis 24 | url: providers/active-mq-artemis 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other such characteristics. 4 | 5 | Everyone is expected to follow the [Scala Code of Conduct] when discussing the project on the available communication channels. If you are being harassed, please contact us immediately so that we can support you. 6 | 7 | ## Moderation 8 | 9 | Any questions, concerns, or moderation requests please contact a member of the project. 10 | 11 | [Scala Code of Conduct]: https://www.scala-lang.org/conduct/ 12 | -------------------------------------------------------------------------------- /site/docs/providers/ibm-mq/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "IBM MQ" 4 | --- 5 | 6 | # IBM MQ 7 | 8 | Creating a jms client for an IBM MQ queue manager is as easy as: 9 | 10 | ```scala mdoc 11 | import cats.data.NonEmptyList 12 | import cats.effect.{ IO, Resource } 13 | import jms4s.ibmmq.ibmMQ 14 | import jms4s.ibmmq.ibmMQ._ 15 | import jms4s.JmsClient 16 | import org.typelevel.log4cats.Logger 17 | 18 | def jmsClientResource(implicit L: Logger[IO]): Resource[IO, JmsClient[IO]] = 19 | ibmMQ.makeJmsClient[IO]( 20 | Config( 21 | qm = QueueManager("YOUR.QM"), 22 | endpoints = NonEmptyList.one(Endpoint("localhost", 1414)), 23 | channel = Channel("YOUR.CHANNEL"), 24 | username = Some(Username("YOU")), 25 | password = Some(Password("PW")), 26 | clientId = ClientId("YOUR.APP") 27 | ) 28 | ) 29 | ``` -------------------------------------------------------------------------------- /site/docs/providers/active-mq-artemis/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Active MQ Artemis" 4 | --- 5 | 6 | # Active MQ Artemis 7 | 8 | Creating a jms client for an Active MQ Artemis cluster is as easy as: 9 | 10 | ```scala mdoc 11 | import cats.data.NonEmptyList 12 | import cats.effect.{ IO, Resource } 13 | import jms4s.activemq.activeMQ 14 | import jms4s.activemq.activeMQ._ 15 | import org.typelevel.log4cats.Logger 16 | import jms4s.JmsClient 17 | 18 | def jmsClientResource(implicit L: Logger[IO]): Resource[IO, JmsClient[IO]] = 19 | activeMQ.makeJmsClient[IO]( 20 | Config( 21 | endpoints = NonEmptyList.one(Endpoint("localhost", 61616)), 22 | username = Some(Username("YOU")), 23 | password = Some(Password("PW")), 24 | clientId = ClientId("YOUR.APP") 25 | ) 26 | ) 27 | ``` 28 | ## Why not ActiveMQ 5 "Classic"? 29 | ActiveMQ 5 "Classic" is only supporting JMS 1.1, which is missing a bunch of features we really need to offer. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Functional Programming in Bologna 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /scripts/broker-00.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/jms/IbmMQJmsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import jms4s.basespec.providers.IbmMQBaseSpec 25 | 26 | class IbmMQJmsSpec extends JmsSpec with IbmMQBaseSpec 27 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/IbmMQJmsClientSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import jms4s.basespec.providers.IbmMQBaseSpec 25 | 26 | class IbmMQJmsClientSpec extends JmsClientSpec with IbmMQBaseSpec 27 | -------------------------------------------------------------------------------- /site/docs/programs/auto-ack-consumer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Auto-Acknowledger Consumer" 4 | --- 5 | 6 | # Auto Acknowledger Consumer 7 | 8 | A `JmsAutoAcknowledgerConsumer` is a consumer that will automatically acknowledge a message after its reception. 9 | Its only operation is: 10 | 11 | ```scala 12 | def handle(f: (JmsMessage, MessageFactory[F]) => F[AutoAckAction[F]]): F[Unit] 13 | ``` 14 | 15 | This is where the user of the API can specify its business logic, which can be any effectful operation. 16 | 17 | Creating a message is as effectful operation as well, and the `MessageFactory` argument will provide the only way in which a client can create a brand new message. This argument can be ignored if the client is only consuming messages. 18 | 19 | What `handle` expects is an `AutoAckAction[F]`, which can be either: 20 | - an `AckAction.noOp`, which will instructs the lib to do nothing since the message will be acknowledged regardless 21 | - an `AckAction.send` in all its forms, which can be used to send 1 or multiple messages to 1 or multiple destinations 22 | 23 | The consumer can be configured specifying a `concurrencyLevel`, which is used internally to scale the operations (receive and then process up to `concurrencyLevel`). 24 | 25 | A complete example is available in the [example project](https://github.com/fp-in-bo/jms4s/blob/main/examples/src/main/scala/AutoAckConsumerExample.scala). 26 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/jms/ActiveMQArtemisJmsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import jms4s.basespec.providers.ActiveMQArtemisBaseSpec 25 | 26 | class ActiveMQArtemisJmsSpec extends JmsSpec with ActiveMQArtemisBaseSpec 27 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/ActiveMQArtemisJmsClientSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import jms4s.basespec.providers.ActiveMQArtemisBaseSpec 25 | 26 | class ActiveMQArtemisJmsClientSpec extends JmsClientSpec with ActiveMQArtemisBaseSpec 27 | -------------------------------------------------------------------------------- /site/docs/programs/ack-consumer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Acknowledger Consumer" 4 | --- 5 | 6 | # Acknowledger Consumer 7 | 8 | A `JmsAcknowledgerConsumer` is a consumer which let the client decide whether confirm (a.k.a. ack) or reject (a.k.a. nack) a message after its reception. 9 | Its only operation is: 10 | 11 | ```scala 12 | def handle(f: (JmsMessage, MessageFactory[F]) => F[AckAction[F]]): F[Unit] 13 | ``` 14 | 15 | This is where the user of the API can specify its business logic, which can be any effectful operation. 16 | 17 | Creating a message is as effectful operation as well, and the `MessageFactory` argument will provide the only way in which a client can create a brand new message. This argument can be ignored if the client is only consuming messages. 18 | 19 | What `handle` expects is an `AckAction[F]`, which can be either: 20 | - an `AckAction.ack`, which will instructs the lib to confirm the message 21 | - an `AckAction.noAck`, which will instructs the lib to do nothing 22 | - an `AckAction.send` in all its forms, which can be used to instruct the lib to send 1 or multiple messages to 1 or multiple destinations 23 | 24 | The consumer can be configured specifying a `concurrencyLevel`, which is used internally to scale the operations (receive and then process up to `concurrencyLevel`). 25 | 26 | A complete example is available in the [example project](https://github.com/fp-in-bo/jms4s/blob/main/examples/src/main/scala/AckConsumerExample.scala). 27 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | ibmmq: 4 | image: ibmcom/mq:9.2.4.0-r1 # https://github.com/ibm-messaging/mq-container 5 | ports: 6 | - "1414:1414" 7 | - "9443:9443" # https://localhost:9443/ibmmq/console/ 8 | volumes: 9 | - ./scripts/definitions.mqsc:/etc/mqm/definitions.mqsc:ro 10 | environment: 11 | - LICENSE=accept 12 | - MQ_QMGR_NAME=QM1 13 | # Users 14 | # Userid: admin Groups: mqm Password: passw0rd 15 | # Userid: app Groups: mqclient Password: 16 | 17 | # Queues 18 | # DEV.QUEUE.1 19 | # DEV.QUEUE.2 20 | # DEV.QUEUE.3 21 | # DEV.DEAD.LETTER.QUEUE - Set as the Queue Manager's Dead Letter Queue. 22 | 23 | # Channels 24 | # DEV.ADMIN.SVRCONN - Set to only allow the admin user to connect into it and a Userid + Password must be supplied. 25 | # DEV.APP.SVRCONN - Does not allow Administrator users to connect. 26 | 27 | # Listener 28 | # DEV.LISTENER.TCP - Listening on Port 1414. 29 | 30 | # Topic 31 | # DEV.BASE.TOPIC - With a topic string of dev/ 32 | 33 | activemq: 34 | image: vromero/activemq-artemis:2.16.0 # https://github.com/vromero/activemq-artemis-docker/blob/master/README.md 35 | ports: 36 | - "8161:8161" # http://localhost:8161/console 37 | - "61616:61616" 38 | volumes: 39 | - ./scripts/broker-00.xml:/var/lib/artemis/etc-override/broker-00.xml:ro 40 | environment: 41 | - ARTEMIS_USERNAME=admin 42 | - ARTEMIS_PASSWORD=passw0rd 43 | -------------------------------------------------------------------------------- /site/docs/programs/tx-consumer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Transacted Consumer" 4 | --- 5 | 6 | # Transacted Consumer 7 | 8 | A `JmsTransactedConsumer` is a consumer that will use a local transaction to receive a message and which lets the client decide whether to commit or rollback it. 9 | Its only operation is: 10 | 11 | ```scala 12 | def handle(f: (JmsMessage, MessageFactory[F]) => F[TransactionAction[F]]): F[Unit] 13 | ``` 14 | 15 | This is where the user of the API can specify its business logic, which can be any effectful operation. 16 | 17 | Creating a message is as effectful operation as well, and the `MessageFactory` argument will provide the only way in which a client can create a brand new message. This argument can be ignored if the client is only consuming messages. 18 | 19 | What `handle` expects is a `TransactionAction[F]`, which can be either: 20 | - a `TransactionAction.commit`, which will instructs the lib to commit the local transaction 21 | - a `TransactionAction.rollback`, which will instructs the lib to rollback the local transaction 22 | - a `TransactionAction.send` in all its forms, which can be used to send 1 or multiple messages to 1 or multiple destinations and then commit the local transaction 23 | 24 | The consumer can be configured specifying a `concurrencyLevel`, which is used internally to scale the operations (receive and then process up to `concurrencyLevel`). 25 | 26 | A complete example is available in the [example project](https://github.com/fp-in-bo/jms4s/blob/main/examples/src/main/scala/TransactedConsumerExample.scala). 27 | -------------------------------------------------------------------------------- /site/docs/programs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Program" 4 | position: 1 5 | --- 6 | 7 | # Program 8 | 9 | A program is a high level interface that will hide all the interaction with low level JMS apis. 10 | This library is targetting JMS 2.0. 11 | 12 | There are different types of programs for consuming and/or producing, each of them is parameterized on the effect type (eg. `IO`). 13 | 14 | The data they consume/produce is kept as-is, thus the de/serialization is left to the user of the api (e.g. json, xml, text). 15 | 16 | Currently only `javax.jms.TextMessage` is supported, but we designed the api in order to easily support new message types as soon as we see demand or contributions. 17 | 18 | - **[JmsTransactedConsumer](./tx-consumer/)**: A consumer that supports local transactions, also producing messages to destinations. 19 | - **[JmsAcknowledgerConsumer](./ack-consumer/)**: A consumer that leaves the choice to acknowledge (or not to) message consumption to the user, also producing messages to destinations. 20 | - **[JmsAutoAcknowledgerConsumer](./auto-ack-consumer/)**: A consumer that acknowledges message consumption automatically, also producing messages to destinations. 21 | - **[JmsProducer](./producer/)**: Producing messages to one or more destinations. 22 | 23 | ## Concurrency with JMS? 24 | 25 | The core of all JMS is the entity named [`JMSContext`](https://docs.oracle.com/javaee/7/api/javax/jms/JMSContext.html), introduced with JMS 2.0. 26 | 27 | The only safe way to scale things a bit is to create a "root" context (which will open a physical connection) and from that creating multiple other contexts. 28 | 29 | This library will keep a pool of contexts so that the user can scale up to pre-defined concurrency level. 30 | -------------------------------------------------------------------------------- /site/docs/programs/producer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Producer" 4 | --- 5 | 6 | # Producer 7 | 8 | A `JmsProducer` is a producer that lets the client publish a message in queues/topics. 9 | 10 | - sendN: to send N messages to N Destinations. 11 | ```scala 12 | def sendN( 13 | makeN: MessageFactory[F] => F[NonEmptyList[(JmsMessage, DestinationName)]] 14 | ): F[Unit] 15 | ``` 16 | 17 | - sendNWithDelay: to send N messages to N Destinations with an optional delay. 18 | ```scala 19 | def sendNWithDelay( 20 | makeNWithDelay: MessageFactory[F] => F[NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))]] 21 | ): F[Unit] 22 | ``` 23 | 24 | - sendWithDelay: to send a message to a Destination. 25 | ```scala 26 | def sendWithDelay( 27 | make1WithDelay: MessageFactory[F] => F[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 28 | ): F[Unit] 29 | ``` 30 | - send: to send a message to a Destination. 31 | ```scala 32 | def send( 33 | make1: MessageFactory[F] => F[(JmsMessage, DestinationName)] 34 | ): F[Unit] 35 | ``` 36 | 37 | For each operation, the client has to provide a function that knows how to build a `JmsMessage` given a `MessageFactory`. 38 | This may appear counter-intuitive at first, but the reason behind this design is that creating a `JmsMessage` is an operation that involves interacting with JMS APIs, and we want to provide a high-level API so that the user can't do things wrong. 39 | 40 | A complete example is available in the [example project](https://github.com/fp-in-bo/jms4s/blob/main/examples/src/main/scala/ProducerExample.scala). 41 | 42 | ### A note on concurrency 43 | 44 | A `JmsProducer` can be used concurrently, performing up to `concurrencyLevel` concurrent operation. 45 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/utils/TryUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms.utils 23 | 24 | import scala.util.{ Success, Try } 25 | 26 | object TryUtils { 27 | 28 | implicit class TryUtils[T](val underlying: Try[Option[T]]) extends AnyVal { 29 | 30 | def toOpt: Option[T] = 31 | underlying match { 32 | case Success(t) => { 33 | t match { 34 | case Some(null) => None 35 | case None => None 36 | case x => x 37 | } 38 | } 39 | case _ => None 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/model.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import javax.jms.Session 25 | 26 | object model { 27 | 28 | sealed abstract class SessionType(val rawAcknowledgeMode: Int) extends Product with Serializable 29 | 30 | object SessionType { 31 | 32 | case object Transacted extends SessionType(Session.SESSION_TRANSACTED) 33 | 34 | case object ClientAcknowledge extends SessionType(Session.CLIENT_ACKNOWLEDGE) 35 | 36 | case object AutoAcknowledge extends SessionType(Session.AUTO_ACKNOWLEDGE) 37 | 38 | case object DupsOkAcknowledge extends SessionType(Session.DUPS_OK_ACKNOWLEDGE) 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/basespec/providers/IbmMQBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.basespec.providers 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, IO, Resource } 26 | import jms4s.JmsClient 27 | import jms4s.basespec.Jms4sBaseSpec 28 | import jms4s.ibmmq.ibmMQ 29 | import jms4s.ibmmq.ibmMQ._ 30 | 31 | trait IbmMQBaseSpec extends Jms4sBaseSpec { 32 | 33 | override def jmsClientRes(implicit A: Async[IO]): Resource[IO, JmsClient[IO]] = 34 | ibmMQ.makeJmsClient[IO]( 35 | Config( 36 | qm = QueueManager("QM1"), 37 | endpoints = NonEmptyList.one(Endpoint("localhost", 1414)), 38 | channel = Channel("DEV.APP.SVRCONN"), 39 | username = Some(Username("app")), 40 | password = None, 41 | clientId = ClientId("jms-specs") 42 | ) 43 | ) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/JmsMessageConsumer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.effect.{ Async, Spawn, Sync } 25 | import cats.syntax.all._ 26 | 27 | import scala.concurrent.duration.FiniteDuration 28 | 29 | import javax.jms.JMSConsumer 30 | 31 | class JmsMessageConsumer[F[_]: Async] private[jms4s] ( 32 | private[jms4s] val wrapped: JMSConsumer, 33 | private[jms4s] val pollingInterval: FiniteDuration 34 | ) { 35 | 36 | val receiveJmsMessage: F[JmsMessage] = 37 | for { 38 | recOpt <- Sync[F].blocking(Option(wrapped.receiveNoWait())) 39 | rec <- recOpt match { 40 | case Some(message) => Sync[F].pure(new JmsMessage(message)) 41 | case None => Spawn[F].cede >> Async[F].sleep(pollingInterval) >> receiveJmsMessage 42 | } 43 | } yield rec 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/config/config.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.config 23 | 24 | import cats.Order 25 | import jms4s.jms.JmsDestination.{ JmsQueue, JmsTopic } 26 | 27 | sealed trait DestinationName extends Product with Serializable 28 | 29 | case class QueueName(value: String) extends DestinationName 30 | case class TopicName(value: String) extends DestinationName 31 | 32 | case class TemporaryQueueName(destination: JmsQueue) extends DestinationName { 33 | def value: String = destination.name 34 | } 35 | 36 | case class TemporaryTopicName(destination: JmsTopic) extends DestinationName { 37 | def value: String = destination.name 38 | } 39 | 40 | object DestinationName { 41 | 42 | implicit val orderingDestinationName: Order[DestinationName] = Order.from[DestinationName] { 43 | case (x, y) => Order[String].compare(x.toString, y.toString) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/basespec/providers/ActiveMQArtemisBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.basespec.providers 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, IO, Resource } 26 | import jms4s.JmsClient 27 | import jms4s.activemq.activeMQ 28 | import jms4s.activemq.activeMQ._ 29 | import jms4s.basespec.Jms4sBaseSpec 30 | 31 | import scala.util.Random 32 | 33 | trait ActiveMQArtemisBaseSpec extends Jms4sBaseSpec { 34 | 35 | override def jmsClientRes(implicit A: Async[IO]): Resource[IO, JmsClient[IO]] = 36 | for { 37 | rnd <- Resource.eval(IO(Random.nextInt())) 38 | client <- activeMQ.makeJmsClient[IO]( 39 | Config( 40 | endpoints = NonEmptyList.one(Endpoint("localhost", 61616)), 41 | username = Some(Username("admin")), 42 | password = Some(Password("passw0rd")), 43 | clientId = ClientId("jms-specs" + rnd) 44 | ) 45 | ) 46 | } yield client 47 | } 48 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/JmsDestination.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.Show 25 | 26 | import javax.jms.{ Destination, Queue, Topic } 27 | 28 | sealed abstract class JmsDestination { 29 | private[jms4s] val wrapped: Destination 30 | def name: String 31 | 32 | override def toString: String = s"${getClass.getSimpleName}($name)" 33 | } 34 | 35 | object JmsDestination { 36 | 37 | class JmsQueue private[jms4s] (private[jms4s] val wrapped: Queue) extends JmsDestination { 38 | override def name: String = wrapped.getQueueName 39 | } 40 | 41 | class JmsTopic private[jms4s] (private[jms4s] val wrapped: Topic) extends JmsDestination { 42 | override def name: String = wrapped.getTopicName 43 | } 44 | 45 | class Other private[jms4s] (private[jms4s] val wrapped: Destination) extends JmsDestination { 46 | override def name: String = wrapped.toString 47 | } 48 | 49 | def fromDestination(destination: Destination): JmsDestination = 50 | destination match { 51 | case queue: Queue => new JmsQueue(queue) 52 | case topic: Topic => new JmsTopic(topic) 53 | case x => new Other(x) 54 | } 55 | 56 | implicit val showDestination: Show[JmsDestination] = Show.fromToString[JmsDestination] 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /examples/src/main/scala/AutoAckConsumerExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import cats.effect.{ ExitCode, IO, IOApp, Resource } 23 | import jms4s.JmsAutoAcknowledgerConsumer.AutoAckAction 24 | import jms4s.JmsClient 25 | import jms4s.config.{ QueueName, TopicName } 26 | import jms4s.jms.MessageFactory 27 | 28 | import scala.concurrent.duration._ 29 | 30 | class AutoAckConsumerExample extends IOApp { 31 | 32 | val jmsClient: Resource[IO, JmsClient[IO]] = null // see providers section! 33 | val inputQueue: QueueName = QueueName("YUOR.INPUT.QUEUE") 34 | val outputTopic: TopicName = TopicName("YUOR.OUTPUT.TOPIC") 35 | 36 | def yourBusinessLogic(text: String, mf: MessageFactory[IO]): IO[AutoAckAction[IO]] = 37 | if (text.toInt % 2 == 0) { 38 | mf.makeTextMessage("a brand new message").map(newMsg => AutoAckAction.send[IO](newMsg, outputTopic)) 39 | } else { 40 | IO.pure(AutoAckAction.noOp) 41 | } 42 | 43 | override def run(args: List[String]): IO[ExitCode] = { 44 | val consumerRes = for { 45 | client <- jmsClient 46 | consumer <- client.createAutoAcknowledgerConsumer(inputQueue, 10, 100.millis) 47 | } yield consumer 48 | 49 | consumerRes.use(_.handle { (jmsMessage, mf) => 50 | for { 51 | text <- jmsMessage.asTextF[IO] 52 | res <- yourBusinessLogic(text, mf) 53 | } yield res 54 | }.as(ExitCode.Success)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/src/main/scala/AckConsumerExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import cats.effect.{ ExitCode, IO, IOApp, Resource } 23 | import jms4s.JmsAcknowledgerConsumer.AckAction 24 | import jms4s.JmsClient 25 | import jms4s.config.{ QueueName, TopicName } 26 | import jms4s.jms.MessageFactory 27 | 28 | import scala.concurrent.duration._ 29 | 30 | class AckConsumerExample extends IOApp { 31 | 32 | val contextRes: Resource[IO, JmsClient[IO]] = null // see providers section! 33 | val inputQueue: QueueName = QueueName("YUOR.INPUT.QUEUE") 34 | val outputTopic: TopicName = TopicName("YUOR.OUTPUT.TOPIC") 35 | 36 | def yourBusinessLogic(text: String, mf: MessageFactory[IO]): IO[AckAction[IO]] = 37 | if (text.toInt % 2 == 0) 38 | mf.makeTextMessage("a brand new message").map(newMsg => AckAction.send(newMsg, outputTopic)) 39 | else if (text.toInt % 3 == 0) 40 | IO.pure(AckAction.noAck) 41 | else 42 | IO.pure(AckAction.ack) 43 | 44 | override def run(args: List[String]): IO[ExitCode] = { 45 | val consumerRes = for { 46 | client <- contextRes 47 | consumer <- client.createAcknowledgerConsumer(inputQueue, 10, 100.millis) 48 | } yield consumer 49 | 50 | consumerRes.use(_.handle { (jmsMessage, mf) => 51 | for { 52 | text <- jmsMessage.asTextF[IO] 53 | res <- yourBusinessLogic(text, mf) 54 | } yield res 55 | }.as(ExitCode.Success)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/src/main/scala/TransactedConsumerExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import cats.effect.{ ExitCode, IO, IOApp, Resource } 23 | import jms4s.JmsClient 24 | import jms4s.JmsTransactedConsumer._ 25 | import jms4s.config.{ QueueName, TopicName } 26 | import jms4s.jms.MessageFactory 27 | 28 | import scala.concurrent.duration._ 29 | 30 | class TransactedConsumerExample extends IOApp { 31 | 32 | val jmsClient: Resource[IO, JmsClient[IO]] = null // see providers section! 33 | val inputQueue: QueueName = QueueName("YUOR.INPUT.QUEUE") 34 | val outputTopic: TopicName = TopicName("YUOR.OUTPUT.TOPIC") 35 | 36 | def yourBusinessLogic(text: String, mf: MessageFactory[IO]): IO[TransactionAction[IO]] = 37 | if (text.toInt % 2 == 0) 38 | mf.makeTextMessage("a brand new message").map(newMsg => TransactionAction.send(newMsg, outputTopic)) 39 | else if (text.toInt % 3 == 0) 40 | IO.pure(TransactionAction.rollback) 41 | else IO.pure(TransactionAction.commit) 42 | 43 | override def run(args: List[String]): IO[ExitCode] = { 44 | val consumerRes = for { 45 | client <- jmsClient 46 | consumer <- client.createTransactedConsumer(inputQueue, 10, 100.millis) 47 | } yield consumer 48 | 49 | consumerRes.use(_.handle { (jmsMessage, mf) => 50 | for { 51 | text <- jmsMessage.asTextF[IO] 52 | res <- yourBusinessLogic(text, mf) 53 | } yield res 54 | }.as(ExitCode.Success)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jms4s - a functional wrapper for jms 2 | [![Build Status](https://travis-ci.com/fp-in-bo/jms4s.svg?branch=main)](https://travis-ci.com/fp-in-bo/jms4s) 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.fpinbo/jms4s_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.fpinbo/jms4s_2.12) 4 | ![Code of Consuct](https://img.shields.io/badge/Code%20of%20Conduct-Scala-blue.svg) 5 | [![Mergify Status][mergify-status]][mergify] 6 | [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org) 7 | 8 | [mergify]: https://mergify.io 9 | [mergify-status]: https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/fp-in-bo/jms4s&style=flat 10 | 11 | Nobody really wants to use jms, but if you have no choice or you're not like us you may find this useful. 12 | 13 | ## Supported features: 14 | 15 | - Consuming, returning a never-ending cancellable program that can concurrently consume from a queue 16 | - createQueueTransactedConsumer 17 | - createQueueAckConsumer 18 | - createQueueAutoAckConsumer 19 | 20 | - Publishing, returning a program which can publish messages 21 | - createQueuePublisher 22 | - createTopicPublisher 23 | 24 | - Consuming and Publishing within the same local transaction 25 | 26 | ## Is it _production ready™_? 27 | 28 | You're asking the wrong question. 29 | 30 | But yes, this is currenlty used to process millions of messages per day in large scale production systems. 31 | 32 | ## [Head on over to the microsite](https://fp-in-bo.github.io/jms4s) 33 | 34 | ## Quick Start 35 | 36 | To use jms4s in an existing SBT project with Scala 2.12 or a later version, add the following dependencies to your 37 | `build.sbt` depending on your provider: 38 | 39 | ```scala 40 | libraryDependencies ++= Seq( 41 | "dev.fpinbo" %% "jms4s-active-mq-artemis" % "", // if your provider is activemq-artemis 42 | "dev.fpinbo" %% "jms4s-ibm-mq" % "" // if your provider is ibmmq 43 | ) 44 | ``` 45 | 46 | ## Local dev 47 | 48 | ## run tests 49 | 50 | - `docker-compose up -d` 51 | - `sbt test` 52 | 53 | ### site 54 | 55 | - build site 56 | 57 | ``` 58 | docker run \ 59 | -v $PWD:/$PWD \ 60 | -v ~/.sbt:/root/.sbt \ 61 | -v ~/.ivy2:/root/.ivy2 \ 62 | -v ~/.m2:/root/.m2 \ 63 | -v ~/.coursier:/root/.coursier \ 64 | -w /$PWD \ 65 | -it k3vin/sbt-java8-jekyll \ 66 | sbt site/clean site/makeMicrosite 67 | ``` 68 | 69 | - run at localhost:4000/jms4s/ 70 | 71 | ``` 72 | docker run \ 73 | -v $PWD:/$PWD \ 74 | -w /$PWD/site/target/site \ 75 | -p 4000:4000 \ 76 | -it k3vin/sbt-java8-jekyll \ 77 | jekyll serve -b /jms4s --host 0.0.0.0 78 | ``` 79 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/MessageFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.syntax.all._ 25 | import cats.{ Applicative, MonadThrow } 26 | import jms4s.jms.JmsMessage.JmsTextMessage 27 | 28 | import scala.util.Try 29 | 30 | class MessageFactory[F[_]](private val context: JmsContext[F]) extends AnyVal { 31 | def makeTextMessage(value: String): F[JmsTextMessage] = context.createTextMessage(value) 32 | 33 | def cloneMessageF(original: JmsTextMessage)(implicit mt: MonadThrow[F]): F[JmsTextMessage] = 34 | attemptCloneMessage(original).flatMap(_.liftTo[F]) 35 | 36 | def attemptCloneMessage(original: JmsTextMessage)(implicit a: Applicative[F]): F[Either[Throwable, JmsTextMessage]] = 37 | original.getText 38 | .traverse(makeTextMessage) 39 | .map { 40 | _.flatMap(copied => copyMessageHeaders(original, copied).as(copied)) 41 | } 42 | .map(_.toEither) 43 | 44 | private def copyMessageHeaders(from: JmsMessage, to: JmsMessage): Try[Unit] = 45 | ( 46 | from.getJMSMessageId.traverse_(to.setJMSMessageID), 47 | from.getJMSTimestamp.traverse_(to.setJMSTimestamp), 48 | from.getJMSCorrelationId.traverse_(to.setJMSCorrelationId), 49 | from.getJMSReplyTo.traverse_(d => to.setJMSReplyTo(JmsDestination.fromDestination(d))), 50 | from.getJMSDestination.traverse_(d => to.setJMSDestination(JmsDestination.fromDestination(d))), 51 | from.getJMSDeliveryMode.traverse_(to.setJMSDeliveryMode), 52 | from.getJMSRedelivered.traverse_(to.setJMSRedelivered), 53 | from.getJMSType.traverse_(to.setJMSType), 54 | from.getJMSExpiration.traverse_(to.setJMSExpiration), 55 | from.getJMSPriority.traverse_(to.setJMSPriority), 56 | from.properties.traverse_(props => props.toList.traverse_ { case (k, v) => to.setObjectProperty(k, v) }) 57 | ).combineAll 58 | 59 | } 60 | 61 | object MessageFactory { 62 | def apply[F[_]](context: JmsContext[F]): MessageFactory[F] = new MessageFactory(context) 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/JmsClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.effect.{ Async, Resource } 25 | import cats.syntax.all._ 26 | import jms4s.config.{ DestinationName, TemporaryQueueName, TemporaryTopicName } 27 | import jms4s.jms._ 28 | import jms4s.model.SessionType 29 | 30 | import scala.concurrent.duration.FiniteDuration 31 | 32 | class JmsClient[F[_]: Async] private[jms4s] (private[jms4s] val context: JmsContext[F]) { 33 | 34 | def createTransactedConsumer( 35 | inputDestinationName: DestinationName, 36 | concurrencyLevel: Int, 37 | pollingInterval: FiniteDuration 38 | ): Resource[F, JmsTransactedConsumer[F]] = 39 | PooledConsumer 40 | .make[F](context, inputDestinationName, concurrencyLevel, pollingInterval, SessionType.Transacted) 41 | .map(JmsTransactedConsumer.make(_)) 42 | 43 | def createAutoAcknowledgerConsumer( 44 | inputDestinationName: DestinationName, 45 | concurrencyLevel: Int, 46 | pollingInterval: FiniteDuration 47 | ): Resource[F, JmsAutoAcknowledgerConsumer[F]] = 48 | PooledConsumer 49 | .make[F](context, inputDestinationName, concurrencyLevel, pollingInterval, SessionType.AutoAcknowledge) 50 | .map(JmsAutoAcknowledgerConsumer.make(_)) 51 | 52 | def createAcknowledgerConsumer( 53 | inputDestinationName: DestinationName, 54 | concurrencyLevel: Int, 55 | pollingInterval: FiniteDuration 56 | ): Resource[F, JmsAcknowledgerConsumer[F]] = 57 | PooledConsumer 58 | .make[F](context, inputDestinationName, concurrencyLevel, pollingInterval, SessionType.ClientAcknowledge) 59 | .map(JmsAcknowledgerConsumer.make(_)) 60 | 61 | def createProducer( 62 | concurrencyLevel: Int 63 | ): Resource[F, JmsProducer[F]] = 64 | JmsProducer.make[F](context, concurrencyLevel) 65 | 66 | def createTemporaryQueue: F[TemporaryQueueName] = 67 | context.createTemporaryQueue.map(TemporaryQueueName) 68 | 69 | def createTemporaryTopic: F[TemporaryTopicName] = 70 | context.createTemporaryTopic.map(TemporaryTopicName) 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/PooledConsumer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.effect.Resource 25 | import cats.effect.kernel.{ Async, Concurrent } 26 | import cats.effect.std.Queue 27 | import cats.syntax.all._ 28 | import fs2._ 29 | import jms4s.config.DestinationName 30 | import jms4s.model.SessionType 31 | 32 | import scala.concurrent.duration.FiniteDuration 33 | 34 | private[jms4s] class PooledConsumer[F[_]: Concurrent]( 35 | pool: Queue[F, (JmsContext[F], JmsMessageConsumer[F], MessageFactory[F])], 36 | concurrencyLevel: Int 37 | ) { 38 | 39 | def consume[A](f: (JmsMessage, JmsContext[F], MessageFactory[F]) => F[A]): Stream[F, A] = 40 | Stream 41 | .emit(Stream.resource(poolResources).evalMap { 42 | case (context, consumer, mf) => 43 | consumer.receiveJmsMessage 44 | .flatMap(received => f(received, context, mf)) 45 | }) 46 | .repeat 47 | .parJoin(concurrencyLevel) 48 | 49 | private def poolResources: Resource[F, (JmsContext[F], JmsMessageConsumer[F], MessageFactory[F])] = 50 | Resource.make(pool.take)(pool.offer) 51 | } 52 | 53 | object PooledConsumer { 54 | 55 | private[jms4s] def make[F[_]: Async]( 56 | context: JmsContext[F], 57 | inputDestinationName: DestinationName, 58 | concurrencyLevel: Int, 59 | pollingInterval: FiniteDuration, 60 | sessionType: SessionType 61 | ): Resource[F, PooledConsumer[F]] = 62 | for { 63 | pool <- Resource.eval( 64 | Queue.bounded[F, (JmsContext[F], JmsMessageConsumer[F], MessageFactory[F])](concurrencyLevel) 65 | ) 66 | _ <- (0 until concurrencyLevel).toList.traverse_ { _ => 67 | for { 68 | ctx <- context.createContext(sessionType) 69 | consumer <- ctx.createJmsConsumer(inputDestinationName, pollingInterval) 70 | _ <- Resource.eval(pool.offer((ctx, consumer, MessageFactory[F](ctx)))) 71 | } yield () 72 | } 73 | } yield new PooledConsumer(pool, concurrencyLevel) 74 | 75 | } 76 | -------------------------------------------------------------------------------- /active-mq-artemis/src/main/scala/jms4s/activemq/activeMQ.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.activemq 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, Resource, Sync } 26 | import cats.syntax.all._ 27 | import jms4s.JmsClient 28 | import jms4s.jms.JmsContext 29 | import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory 30 | import org.typelevel.log4cats.Logger 31 | 32 | object activeMQ { 33 | 34 | case class Config( 35 | endpoints: NonEmptyList[Endpoint], 36 | username: Option[Username] = None, 37 | password: Option[Password] = None, 38 | clientId: ClientId 39 | ) 40 | case class Username(value: String) extends AnyVal 41 | case class Password(value: String) extends AnyVal 42 | case class Endpoint(host: String, port: Int) 43 | case class ClientId(value: String) extends AnyVal 44 | 45 | def makeJmsClient[F[_]: Async: Logger](config: Config): Resource[F, JmsClient[F]] = 46 | for { 47 | context <- Resource.make( 48 | Logger[F].info(s"Opening context to MQ at ${hosts(config.endpoints)}...") *> 49 | Sync[F].blocking { 50 | val factory = new ActiveMQConnectionFactory(hosts(config.endpoints)) 51 | factory.setClientID(config.clientId.value) 52 | 53 | config.username.fold(factory.createContext())(username => 54 | factory.createContext(username.value, config.password.map(_.value).getOrElse("")) 55 | ) 56 | } 57 | )(c => 58 | Logger[F].info(s"Closing context $c to MQ at ${hosts(config.endpoints)}...") *> 59 | Sync[F].blocking(c.close()) *> 60 | Logger[F].info(s"Closed context $c to MQ at ${hosts(config.endpoints)}.") 61 | ) 62 | _ <- Resource.eval(Logger[F].info(s"Opened context $context.")) 63 | } yield new JmsClient[F](new JmsContext[F](context)) 64 | 65 | private def hosts(endpoints: NonEmptyList[Endpoint]): String = 66 | endpoints.map(e => s"tcp://${e.host}:${e.port}").toList.mkString(",") 67 | 68 | } 69 | -------------------------------------------------------------------------------- /ibm-mq/src/main/scala/jms4s/ibmmq/ibmMQ.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.ibmmq 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, Resource, Sync } 26 | import cats.syntax.all._ 27 | import com.ibm.mq.jms.MQConnectionFactory 28 | import com.ibm.msg.client.wmq.common.CommonConstants 29 | import jms4s.JmsClient 30 | import jms4s.jms.JmsContext 31 | import org.typelevel.log4cats.Logger 32 | 33 | object ibmMQ { 34 | 35 | case class Config( 36 | qm: QueueManager, 37 | endpoints: NonEmptyList[Endpoint], 38 | channel: Channel, 39 | username: Option[Username] = None, 40 | password: Option[Password] = None, 41 | clientId: ClientId 42 | ) 43 | case class Username(value: String) extends AnyVal 44 | case class Password(value: String) extends AnyVal 45 | case class Endpoint(host: String, port: Int) 46 | case class QueueManager(value: String) extends AnyVal 47 | case class Channel(value: String) extends AnyVal 48 | case class ClientId(value: String) extends AnyVal 49 | 50 | def makeJmsClient[F[_]: Logger: Async]( 51 | config: Config 52 | ): Resource[F, JmsClient[F]] = 53 | for { 54 | context <- Resource.make( 55 | Logger[F].info(s"Opening Context to MQ at ${hosts(config.endpoints)}...") >> 56 | Sync[F].blocking { 57 | val connectionFactory: MQConnectionFactory = new MQConnectionFactory() 58 | connectionFactory.setTransportType(CommonConstants.WMQ_CM_CLIENT) 59 | connectionFactory.setQueueManager(config.qm.value) 60 | connectionFactory.setConnectionNameList(hosts(config.endpoints)) 61 | connectionFactory.setChannel(config.channel.value) 62 | connectionFactory.setClientID(config.clientId.value) 63 | 64 | config.username.map { username => 65 | connectionFactory.createContext( 66 | username.value, 67 | config.password.map(_.value).orNull 68 | ) 69 | }.getOrElse(connectionFactory.createContext()) 70 | } 71 | )(c => 72 | Logger[F].info(s"Closing Context $c at ${hosts(config.endpoints)}...") *> 73 | Sync[F].blocking(c.close()) *> 74 | Logger[F].info(s"Closed Context $c.") 75 | ) 76 | _ <- Resource.eval(Logger[F].info(s"Opened Context $context at ${hosts(config.endpoints)}.")) 77 | } yield new JmsClient[F](new JmsContext[F](context)) 78 | 79 | private def hosts(endpoints: NonEmptyList[Endpoint]): String = 80 | endpoints.map(e => s"${e.host}(${e.port})").toList.mkString(",") 81 | 82 | } 83 | -------------------------------------------------------------------------------- /examples/src/main/scala/ProducerExample.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import cats.data.NonEmptyList 23 | import cats.effect.{ ExitCode, IO, IOApp, Resource } 24 | import jms4s.JmsClient 25 | import jms4s.config.{ DestinationName, TopicName } 26 | import jms4s.jms.JmsMessage.JmsTextMessage 27 | import jms4s.jms.MessageFactory 28 | 29 | import scala.concurrent.duration.{ FiniteDuration, _ } 30 | 31 | class ProducerExample extends IOApp { 32 | 33 | val jmsClient: Resource[IO, JmsClient[IO]] = null // see providers section! 34 | val outputTopic: TopicName = TopicName("YUOR.OUTPUT.TOPIC") 35 | val delay: FiniteDuration = 10.millis 36 | val messageStrings: NonEmptyList[String] = NonEmptyList.fromListUnsafe((0 until 10).map(i => s"$i").toList) 37 | 38 | override def run(args: List[String]): IO[ExitCode] = { 39 | val producerRes = for { 40 | client <- jmsClient 41 | producer <- client.createProducer(10) 42 | } yield producer 43 | 44 | producerRes.use { producer => 45 | { 46 | for { 47 | _ <- producer.sendN(makeN(messageStrings, outputTopic)) 48 | _ <- producer.sendNWithDelay(makeNWithDelay(messageStrings, outputTopic, delay)) 49 | _ <- producer.send(make1(messageStrings.head, outputTopic)) 50 | _ <- producer.sendWithDelay(make1WithDelay(messageStrings.head, outputTopic, delay)) 51 | } yield () 52 | }.as(ExitCode.Success) 53 | } 54 | } 55 | 56 | private def make1( 57 | text: String, 58 | destinationName: DestinationName 59 | ): MessageFactory[IO] => IO[(JmsTextMessage, DestinationName)] = { mFactory => 60 | mFactory 61 | .makeTextMessage(text) 62 | .map(message => (message, destinationName)) 63 | } 64 | 65 | private def makeN( 66 | texts: NonEmptyList[String], 67 | destinationName: DestinationName 68 | ): MessageFactory[IO] => IO[NonEmptyList[(JmsTextMessage, DestinationName)]] = { mFactory => 69 | texts.traverse { text => 70 | mFactory 71 | .makeTextMessage(text) 72 | .map(message => (message, destinationName)) 73 | } 74 | } 75 | 76 | private def make1WithDelay( 77 | text: String, 78 | destinationName: DestinationName, 79 | delay: FiniteDuration 80 | ): MessageFactory[IO] => IO[(JmsTextMessage, (DestinationName, Option[FiniteDuration]))] = { mFactory => 81 | mFactory 82 | .makeTextMessage(text) 83 | .map(message => (message, (destinationName, Some(delay)))) 84 | } 85 | 86 | private def makeNWithDelay( 87 | texts: NonEmptyList[String], 88 | destinationName: DestinationName, 89 | delay: FiniteDuration 90 | ): MessageFactory[IO] => IO[NonEmptyList[(JmsTextMessage, (DestinationName, Option[FiniteDuration]))]] = { mFactory => 91 | texts.traverse { text => 92 | mFactory 93 | .makeTextMessage(text) 94 | .map(message => (message, (destinationName, Some(delay)))) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/JmsAutoAcknowledgerConsumer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, Sync } 26 | import cats.syntax.all._ 27 | import jms4s.JmsAutoAcknowledgerConsumer.AutoAckAction 28 | import jms4s.JmsAutoAcknowledgerConsumer.AutoAckAction.Send 29 | import jms4s.config.DestinationName 30 | import jms4s.jms._ 31 | 32 | import scala.concurrent.duration.FiniteDuration 33 | 34 | trait JmsAutoAcknowledgerConsumer[F[_]] { 35 | def handle(f: (JmsMessage, MessageFactory[F]) => F[AutoAckAction[F]]): F[Unit] 36 | } 37 | 38 | object JmsAutoAcknowledgerConsumer { 39 | 40 | private[jms4s] def make[F[_]: Async](rawConsumer: PooledConsumer[F]): JmsAutoAcknowledgerConsumer[F] = 41 | (action: (JmsMessage, MessageFactory[F]) => F[AutoAckAction[F]]) => 42 | rawConsumer.consume { 43 | case (message, context, mf) => 44 | for { 45 | res: AutoAckAction[F] <- action(message, mf) 46 | _ <- res.fold( 47 | ifNoOp = Sync[F].unit, 48 | ifSend = (send: Send[F]) => 49 | send.messages.messagesAndDestinations.traverse_ { 50 | case (message, (name, Some(delay))) => context.send(name, message, delay) 51 | case (message, (name, None)) => context.send(name, message) 52 | } 53 | ) 54 | } yield () 55 | }.compile.drain 56 | 57 | sealed abstract class AutoAckAction[F[_]] extends Product with Serializable { 58 | def fold(ifNoOp: => F[Unit], ifSend: AutoAckAction.Send[F] => F[Unit]): F[Unit] 59 | } 60 | 61 | object AutoAckAction { 62 | 63 | private[jms4s] case class NoOp[F[_]]() extends AutoAckAction[F] { 64 | 65 | override def fold( 66 | ifNoOp: => F[Unit], 67 | ifSend: AutoAckAction.Send[F] => F[Unit] 68 | ): F[Unit] = ifNoOp 69 | } 70 | 71 | case class Send[F[_]](messages: ToSend[F]) extends AutoAckAction[F] { 72 | 73 | override def fold( 74 | ifNoOp: => F[Unit], 75 | ifSend: AutoAckAction.Send[F] => F[Unit] 76 | ): F[Unit] = 77 | ifSend(this) 78 | } 79 | 80 | private[jms4s] case class ToSend[F[_]]( 81 | messagesAndDestinations: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 82 | ) 83 | 84 | def noOp[F[_]]: AutoAckAction[F] = NoOp[F]() 85 | 86 | def sendN[F[_]]( 87 | messages: NonEmptyList[(JmsMessage, DestinationName)] 88 | ): AutoAckAction[F] = 89 | Send[F](ToSend[F](messages.map { case (message, name) => (message, (name, None)) })) 90 | 91 | def sendNWithDelay[F[_]]( 92 | messages: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 93 | ): AutoAckAction[F] = 94 | Send[F](ToSend[F](messages.map { case (message, (name, delay)) => (message, (name, delay)) })) 95 | 96 | def sendWithDelay[F[_]]( 97 | message: JmsMessage, 98 | destination: DestinationName, 99 | duration: Option[FiniteDuration] 100 | ): AutoAckAction[F] = 101 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, duration))))) 102 | 103 | def send[F[_]]( 104 | message: JmsMessage, 105 | destination: DestinationName 106 | ): AutoAckAction[F] = 107 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, None))))) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/JmsAcknowledgerConsumer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.{ Async, Sync } 26 | import cats.syntax.all._ 27 | import jms4s.JmsAcknowledgerConsumer.AckAction 28 | import jms4s.config.DestinationName 29 | import jms4s.jms._ 30 | 31 | import scala.concurrent.duration.FiniteDuration 32 | 33 | trait JmsAcknowledgerConsumer[F[_]] { 34 | def handle(f: (JmsMessage, MessageFactory[F]) => F[AckAction[F]]): F[Unit] 35 | } 36 | 37 | object JmsAcknowledgerConsumer { 38 | 39 | private[jms4s] def make[F[_]: Async](rawConsumer: PooledConsumer[F]): JmsAcknowledgerConsumer[F] = 40 | (action: (JmsMessage, MessageFactory[F]) => F[AckAction[F]]) => 41 | { 42 | rawConsumer.consume { 43 | case (message, context, mf) => 44 | for { 45 | res <- action(message, mf) 46 | _ <- res.fold( 47 | ifAck = Sync[F].blocking(message.wrapped.acknowledge()), 48 | ifNoAck = Sync[F].unit, 49 | ifSend = send => 50 | send.messages.messagesAndDestinations.traverse_ { 51 | case (message, (name, Some(delay))) => context.send(name, message, delay) 52 | case (message, (name, None)) => context.send(name, message) 53 | } *> Sync[F].blocking(message.wrapped.acknowledge()) 54 | ) 55 | } yield () 56 | } 57 | }.compile.drain 58 | 59 | sealed abstract class AckAction[F[_]] extends Product with Serializable { 60 | def fold(ifAck: => F[Unit], ifNoAck: => F[Unit], ifSend: AckAction.Send[F] => F[Unit]): F[Unit] 61 | } 62 | 63 | object AckAction { 64 | 65 | private[jms4s] case class Ack[F[_]]() extends AckAction[F] { 66 | 67 | override def fold( 68 | ifAck: => F[Unit], 69 | ifNoAck: => F[Unit], 70 | ifSend: AckAction.Send[F] => F[Unit] 71 | ): F[Unit] = ifAck 72 | } 73 | 74 | // if the client wants to ack groups of messages, it'll pass a sequence of NoAck and then a cumulative Ack 75 | private[jms4s] case class NoAck[F[_]]() extends AckAction[F] { 76 | 77 | override def fold( 78 | ifAck: => F[Unit], 79 | ifNoAck: => F[Unit], 80 | ifSend: AckAction.Send[F] => F[Unit] 81 | ): F[Unit] = ifNoAck 82 | } 83 | 84 | case class Send[F[_]](messages: ToSend[F]) extends AckAction[F] { 85 | 86 | override def fold( 87 | ifAck: => F[Unit], 88 | ifNoAck: => F[Unit], 89 | ifSend: AckAction.Send[F] => F[Unit] 90 | ): F[Unit] = 91 | ifSend(this) 92 | } 93 | 94 | private[jms4s] case class ToSend[F[_]]( 95 | messagesAndDestinations: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 96 | ) 97 | 98 | def ack[F[_]]: AckAction[F] = Ack() 99 | 100 | def noAck[F[_]]: AckAction[F] = NoAck() 101 | 102 | def sendN[F[_]]( 103 | messages: NonEmptyList[(JmsMessage, DestinationName)] 104 | ): AckAction[F] = 105 | Send[F](ToSend[F](messages.map { case (message, name) => (message, (name, None)) })) 106 | 107 | def sendNWithDelay[F[_]]( 108 | messages: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 109 | ): AckAction[F] = Send[F](ToSend(messages)) 110 | 111 | def sendWithDelay[F[_]]( 112 | message: JmsMessage, 113 | destination: DestinationName, 114 | duration: Option[FiniteDuration] 115 | ): AckAction[F] = 116 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, duration))))) 117 | 118 | def send[F[_]]( 119 | message: JmsMessage, 120 | destination: DestinationName 121 | ): AckAction[F] = 122 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, None))))) 123 | 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/JmsTransactedConsumer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect.Async 26 | import cats.syntax.all._ 27 | import jms4s.JmsTransactedConsumer.TransactionAction 28 | import jms4s.config.DestinationName 29 | import jms4s.jms._ 30 | 31 | import scala.concurrent.duration.FiniteDuration 32 | 33 | trait JmsTransactedConsumer[F[_]] { 34 | def handle(f: (JmsMessage, MessageFactory[F]) => F[TransactionAction[F]]): F[Unit] 35 | } 36 | 37 | object JmsTransactedConsumer { 38 | 39 | private[jms4s] def make[F[_]: Async](rawConsumer: PooledConsumer[F]): JmsTransactedConsumer[F] = 40 | (f: (JmsMessage, MessageFactory[F]) => F[TransactionAction[F]]) => 41 | rawConsumer.consume { 42 | case (received, context, mf) => 43 | for { 44 | txnAction <- f(received, mf) 45 | _ <- txnAction.fold( 46 | ifCommit = context.commit, 47 | ifRollback = context.rollback, 48 | ifSend = send => 49 | send.messages.messagesAndDestinations.traverse_ { 50 | case (message, (name, Some(delay))) => context.send(name, message, delay) 51 | case (message, (name, None)) => context.send(name, message) 52 | } *> context.commit 53 | ) 54 | } yield () 55 | }.compile.drain 56 | 57 | sealed abstract class TransactionAction[F[_]] extends Product with Serializable { 58 | def fold(ifCommit: => F[Unit], ifRollback: => F[Unit], ifSend: TransactionAction.Send[F] => F[Unit]): F[Unit] 59 | } 60 | 61 | object TransactionAction { 62 | 63 | private[jms4s] case class Commit[F[_]]() extends TransactionAction[F] { 64 | 65 | override def fold( 66 | ifCommit: => F[Unit], 67 | ifRollback: => F[Unit], 68 | ifSend: TransactionAction.Send[F] => F[Unit] 69 | ): F[Unit] = ifCommit 70 | } 71 | 72 | private[jms4s] case class Rollback[F[_]]() extends TransactionAction[F] { 73 | 74 | override def fold( 75 | ifCommit: => F[Unit], 76 | ifRollback: => F[Unit], 77 | ifSend: TransactionAction.Send[F] => F[Unit] 78 | ): F[Unit] = ifRollback 79 | } 80 | 81 | case class Send[F[_]](messages: ToSend[F]) extends TransactionAction[F] { 82 | 83 | override def fold( 84 | ifCommit: => F[Unit], 85 | ifRollback: => F[Unit], 86 | ifSend: TransactionAction.Send[F] => F[Unit] 87 | ): F[Unit] = 88 | ifSend(this) 89 | } 90 | 91 | private[jms4s] case class ToSend[F[_]]( 92 | messagesAndDestinations: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 93 | ) 94 | 95 | def commit[F[_]]: TransactionAction[F] = Commit[F]() 96 | 97 | def rollback[F[_]]: TransactionAction[F] = Rollback[F]() 98 | 99 | def sendN[F[_]]( 100 | messages: NonEmptyList[(JmsMessage, DestinationName)] 101 | ): TransactionAction[F] = 102 | Send[F](ToSend[F](messages.map { case (message, name) => (message, (name, None)) })) 103 | 104 | def sendNWithDelay[F[_]]( 105 | messages: NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 106 | ): TransactionAction[F] = 107 | Send[F](ToSend[F](messages.map { case (message, (name, delay)) => (message, (name, delay)) })) 108 | 109 | def sendWithDelay[F[_]]( 110 | message: JmsMessage, 111 | destination: DestinationName, 112 | duration: Option[FiniteDuration] 113 | ): TransactionAction[F] = 114 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, duration))))) 115 | 116 | def send[F[_]]( 117 | message: JmsMessage, 118 | destination: DestinationName 119 | ): TransactionAction[F] = 120 | Send[F](ToSend[F](NonEmptyList.one((message, (destination, None))))) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/JmsProducer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect._ 26 | import cats.effect.std.Queue 27 | import cats.syntax.all._ 28 | import jms4s.config.DestinationName 29 | import jms4s.jms._ 30 | import jms4s.model.SessionType 31 | 32 | import scala.concurrent.duration.FiniteDuration 33 | 34 | trait JmsProducer[F[_]] { 35 | 36 | def sendN( 37 | messageFactory: MessageFactory[F] => F[NonEmptyList[(JmsMessage, DestinationName)]] 38 | ): F[Unit] 39 | 40 | def sendNWithDelay( 41 | messageFactory: MessageFactory[F] => F[NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))]] 42 | ): F[Unit] 43 | 44 | def sendWithDelay( 45 | messageFactory: MessageFactory[F] => F[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 46 | ): F[Unit] 47 | 48 | def send(messageFactory: MessageFactory[F] => F[(JmsMessage, DestinationName)]): F[Unit] 49 | } 50 | 51 | private[jms4s] class ContextPool[F[_]: Sync](private val contextsPool: Queue[F, (JmsContext[F], MessageFactory[F])]) { 52 | 53 | def acquireAndUseContext[A](f: (JmsContext[F], MessageFactory[F]) => F[A]): F[A] = 54 | MonadCancel[F].bracket(contextsPool.take) { 55 | case (ctx, mf) => f(ctx, mf) 56 | }(usedCtx => contextsPool.offer(usedCtx)) 57 | } 58 | 59 | object ContextPool { 60 | 61 | def create[F[_]: Async](context: JmsContext[F], concurrencyLevel: Int): Resource[F, ContextPool[F]] = 62 | for { 63 | pool <- Resource.eval( 64 | Queue.bounded[F, (JmsContext[F], MessageFactory[F])](concurrencyLevel) 65 | ) 66 | _ <- (0 until concurrencyLevel).toList.traverse_ { _ => 67 | for { 68 | ctx <- context.createContext(SessionType.AutoAcknowledge) 69 | mf = MessageFactory[F](ctx) 70 | _ <- Resource.eval(pool.offer((ctx, mf))) 71 | } yield () 72 | } 73 | } yield new ContextPool(pool) 74 | } 75 | 76 | object JmsProducer { 77 | 78 | private[jms4s] def make[F[_]: Async]( 79 | context: JmsContext[F], 80 | concurrencyLevel: Int 81 | ): Resource[F, JmsProducer[F]] = 82 | for { 83 | pool <- ContextPool.create(context, concurrencyLevel) 84 | } yield new JmsProducer[F] { 85 | 86 | override def sendN( 87 | f: MessageFactory[F] => F[NonEmptyList[(JmsMessage, DestinationName)]] 88 | ): F[Unit] = 89 | pool.acquireAndUseContext { 90 | case (ctx, mf) => 91 | for { 92 | messagesWithDestinations <- f(mf) 93 | _ <- messagesWithDestinations.traverse_ { 94 | case (message, destinationName) => ctx.send(destinationName, message) 95 | } 96 | } yield () 97 | } 98 | 99 | override def sendNWithDelay( 100 | f: MessageFactory[F] => F[NonEmptyList[(JmsMessage, (DestinationName, Option[FiniteDuration]))]] 101 | ): F[Unit] = 102 | pool.acquireAndUseContext { 103 | case (ctx, mf) => 104 | for { 105 | messagesWithDestinationsAndDelayes <- f(mf) 106 | _ <- messagesWithDestinationsAndDelayes.traverse_ { 107 | case (message, (destinatioName, duration)) => 108 | duration.fold(ctx.send(destinatioName, message))(delay => 109 | ctx.send(destinatioName, message, delay) 110 | ) 111 | } 112 | 113 | } yield () 114 | } 115 | 116 | override def sendWithDelay( 117 | f: MessageFactory[F] => F[(JmsMessage, (DestinationName, Option[FiniteDuration]))] 118 | ): F[Unit] = 119 | pool.acquireAndUseContext { 120 | case (ctx, mf) => 121 | for { 122 | (message, (destinationName, delay)) <- f(mf) 123 | _ <- delay.fold(ctx.send(destinationName, message))(delay => ctx.send(destinationName, message, delay)) 124 | } yield () 125 | } 126 | 127 | override def send(f: MessageFactory[F] => F[(JmsMessage, DestinationName)]): F[Unit] = 128 | pool.acquireAndUseContext { 129 | case (ctx, mf) => 130 | for { 131 | (message, destination) <- f(mf) 132 | _ <- ctx.send(destination, message) 133 | } yield () 134 | } 135 | 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/JmsContext.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.effect.{ Async, Resource, Sync } 25 | import cats.syntax.all._ 26 | import jms4s.config._ 27 | import jms4s.jms.JmsDestination.{ JmsQueue, JmsTopic } 28 | import jms4s.jms.JmsMessage.JmsTextMessage 29 | import jms4s.model.SessionType 30 | import org.typelevel.log4cats.Logger 31 | 32 | import javax.jms.JMSContext 33 | import scala.concurrent.duration.FiniteDuration 34 | 35 | class JmsContext[F[_]: Async: Logger](private val context: JMSContext) { 36 | 37 | def createContext(sessionType: SessionType): Resource[F, JmsContext[F]] = 38 | Resource 39 | .make( 40 | for { 41 | _ <- Logger[F].info("Creating context") 42 | ctx <- Sync[F].blocking(context.createContext(sessionType.rawAcknowledgeMode)) 43 | _ <- Logger[F].info(s"Context $ctx successfully created") 44 | } yield ctx 45 | )(context => 46 | Logger[F].info(s"Releasing context $context") *> 47 | Sync[F].blocking(context.close()) 48 | ) 49 | .map(context => new JmsContext(context)) 50 | 51 | def send(destinationName: DestinationName, message: JmsMessage): F[Unit] = 52 | toJmsDestination(destinationName).flatMap(send(_, message)) 53 | 54 | def send(destinationName: DestinationName, message: JmsMessage, delay: FiniteDuration): F[Unit] = 55 | toJmsDestination(destinationName).flatMap(send(_, message, delay)) 56 | 57 | def send(jmsDestination: JmsDestination, message: JmsMessage): F[Unit] = 58 | for { 59 | _ <- Logger[F].trace(s"Sending message id=${message.getJMSMessageId} to $jmsDestination") 60 | _ <- Sync[F].blocking { 61 | context 62 | .createProducer() 63 | .send(jmsDestination.wrapped, message.wrapped) 64 | } 65 | _ <- Logger[F].trace(s"Sent message id=${message.getJMSMessageId} to $jmsDestination") 66 | } yield () 67 | 68 | def send(jmsDestination: JmsDestination, message: JmsMessage, delay: FiniteDuration): F[Unit] = 69 | for { 70 | _ <- Logger[F].trace( 71 | s"Sending message id=${message.getJMSMessageId} with delay=${delay.toMillis} to $jmsDestination" 72 | ) 73 | _ <- Sync[F].blocking { 74 | context 75 | .createProducer() 76 | .setDeliveryDelay(delay.toMillis) 77 | .send(jmsDestination.wrapped, message.wrapped) 78 | } 79 | _ <- Logger[F].trace( 80 | s"Sent message id=${message.getJMSMessageId} with delay=${delay.toMillis} to $jmsDestination" 81 | ) 82 | } yield () 83 | 84 | def createJmsConsumer( 85 | destinationName: DestinationName, 86 | pollingInterval: FiniteDuration 87 | ): Resource[F, JmsMessageConsumer[F]] = 88 | for { 89 | destination <- Resource.eval(toJmsDestination(destinationName)) 90 | consumer <- Resource.make( 91 | Logger[F].info(s"Creating consumer for destination $destination") *> 92 | Sync[F].blocking(context.createConsumer(destination.wrapped)) 93 | )(consumer => 94 | Logger[F].info(s"Closing consumer for destination $destination") *> 95 | Sync[F].blocking(consumer.close()) 96 | ) 97 | } yield new JmsMessageConsumer[F](consumer, pollingInterval) 98 | 99 | def createTextMessage(value: String): F[JmsTextMessage] = 100 | Sync[F].blocking(context.createTextMessage(value)).map(new JmsTextMessage(_)) 101 | 102 | def commit: F[Unit] = Sync[F].blocking(context.commit()) 103 | 104 | def rollback: F[Unit] = Sync[F].blocking(context.rollback()) 105 | 106 | private def createQueue(queue: QueueName): F[JmsQueue] = 107 | Sync[F].blocking(context.createQueue(queue.value)).map(new JmsQueue(_)) 108 | 109 | private def createTopic(topicName: TopicName): F[JmsTopic] = 110 | Sync[F].blocking(context.createTopic(topicName.value)).map(new JmsTopic(_)) 111 | 112 | def createTemporaryTopic: F[JmsTopic] = 113 | Sync[F].blocking(context.createTemporaryTopic()).map(new JmsTopic(_)) 114 | 115 | def createTemporaryQueue: F[JmsQueue] = 116 | Sync[F].blocking(context.createTemporaryQueue()).map(new JmsQueue(_)) 117 | 118 | private def toJmsDestination(destination: DestinationName): F[JmsDestination] = destination match { 119 | case qn: QueueName => createQueue(qn).widen[JmsDestination] 120 | case tn: TopicName => createTopic(tn).widen[JmsDestination] 121 | case TemporaryQueueName(destination) => destination.pure[F].widen[JmsDestination] 122 | case TemporaryTopicName(destination) => destination.pure[F].widen[JmsDestination] 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/basespec/Jms4sBaseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.basespec 23 | 24 | import cats.data.NonEmptyList 25 | import cats.effect._ 26 | import cats.implicits._ 27 | import fs2.concurrent.Channel 28 | import jms4s.JmsAutoAcknowledgerConsumer.AutoAckAction 29 | import jms4s.config.{ DestinationName, QueueName, TopicName } 30 | import jms4s.jms.JmsMessage.JmsTextMessage 31 | import jms4s.jms.{ JmsMessageConsumer, MessageFactory } 32 | import jms4s.{ JmsAutoAcknowledgerConsumer, JmsClient } 33 | import org.typelevel.log4cats.slf4j.Slf4jLogger 34 | import org.typelevel.log4cats.{ Logger, SelfAwareStructuredLogger } 35 | 36 | import scala.concurrent.duration.{ FiniteDuration, _ } 37 | 38 | trait Jms4sBaseSpec { 39 | implicit val logger: SelfAwareStructuredLogger[IO] = Slf4jLogger.getLogger[IO] 40 | 41 | def jmsClientRes(implicit cs: Async[IO]): Resource[IO, JmsClient[IO]] 42 | 43 | val body: String = "body" 44 | val nMessages: Int = 50 45 | val bodies: List[String] = (0 until nMessages).map(i => s"$i").toList 46 | val poolSize: Int = 2 47 | val timeout: FiniteDuration = 4.seconds // CI is slow... 48 | val pollingInterval: FiniteDuration = 100.millis 49 | val delay: FiniteDuration = 200.millis 50 | val delayWithTolerance: Duration = delay * 0.8 // it looks like activemq is not fully respecting delivery delay 51 | val topicName1: TopicName = TopicName("dev1/") 52 | val topicName2: TopicName = TopicName("dev2/") 53 | val inputQueueName: QueueName = QueueName("DEV.QUEUE.1") 54 | val outputQueueName1: QueueName = QueueName("DEV.QUEUE.2") 55 | val outputQueueName2: QueueName = QueueName("DEV.QUEUE.3") 56 | 57 | def receiveBodyAsTextOrFail(consumer: JmsMessageConsumer[IO]): IO[String] = 58 | consumer.receiveJmsMessage 59 | .flatMap(_.asTextF[IO]) 60 | 61 | def receiveMessage(consumer: JmsMessageConsumer[IO]): IO[JmsTextMessage] = 62 | consumer.receiveJmsMessage 63 | .flatMap(_.asJmsTextMessageF[IO]) 64 | 65 | def receiveUntil( 66 | consumer: JmsMessageConsumer[IO], 67 | received: Ref[IO, Set[String]], 68 | nMessages: Int 69 | ): IO[Set[String]] = 70 | receiveBodyAsTextOrFail(consumer) 71 | .flatMap(body => received.update(_ + body) *> received.get) 72 | .iterateUntil(_.size == nMessages) 73 | 74 | def receiveUntil( 75 | consumer: JmsAutoAcknowledgerConsumer[IO], 76 | nMessages: Long 77 | ): IO[List[JmsTextMessage]] = 78 | for { 79 | channel <- Channel.synchronous[IO, JmsTextMessage] 80 | fiber <- consumer.handle { 81 | case (msg, _) => 82 | msg 83 | .asJmsTextMessageF[IO] 84 | .flatMap(channel.send) 85 | .as(AutoAckAction.noOp) 86 | }.start 87 | _ <- Logger[IO].info(s"Collecting $nMessages messages from output queue...") 88 | count <- channel.stream 89 | .take(nMessages) 90 | .onFinalize(fiber.cancel) 91 | .compile 92 | .toList 93 | } yield count 94 | 95 | def messageFactory( 96 | message: JmsTextMessage, 97 | destinationName: DestinationName 98 | ): MessageFactory[IO] => IO[(JmsTextMessage, DestinationName)] = { mFactory: MessageFactory[IO] => 99 | message.asTextF[IO].flatMap { text => 100 | mFactory 101 | .makeTextMessage(text) 102 | .map(message => (message, destinationName)) 103 | } 104 | } 105 | 106 | def messageFactory( 107 | message: JmsTextMessage 108 | ): MessageFactory[IO] => IO[JmsTextMessage] = { mFactory: MessageFactory[IO] => 109 | message.asTextF[IO].flatMap(text => mFactory.makeTextMessage(text)) 110 | } 111 | 112 | def messageWithDelayFactory( 113 | message: (JmsTextMessage, (DestinationName, Option[FiniteDuration])) 114 | ): MessageFactory[IO] => IO[(JmsTextMessage, (DestinationName, Option[FiniteDuration]))] = { 115 | mFactory: MessageFactory[IO] => 116 | message._1.asTextF[IO].flatMap { text => 117 | mFactory 118 | .makeTextMessage(text) 119 | .map(m => (m, (message._2._1, message._2._2))) 120 | } 121 | } 122 | 123 | def messageFactory( 124 | messages: NonEmptyList[JmsTextMessage] 125 | ): MessageFactory[IO] => IO[NonEmptyList[JmsTextMessage]] = { mFactory: MessageFactory[IO] => 126 | messages 127 | .map(message => message.asTextF[IO].flatMap(text => mFactory.makeTextMessage(text))) 128 | .sequence 129 | } 130 | 131 | def messageFactory( 132 | messages: NonEmptyList[JmsTextMessage], 133 | destinationName: DestinationName 134 | ): MessageFactory[IO] => IO[NonEmptyList[(JmsTextMessage, DestinationName)]] = 135 | (mFactory: MessageFactory[IO]) => 136 | messages.map { message => 137 | IO.fromTry(message.getText) 138 | .flatMap(text => mFactory.makeTextMessage(text)) 139 | .map(message => (message, destinationName)) 140 | }.sequence 141 | } 142 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/jms/JmsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.effect.testing.scalatest.AsyncIOSpec 25 | import cats.effect.{ Clock, IO, Resource } 26 | import jms4s.basespec.Jms4sBaseSpec 27 | import jms4s.config.DestinationName 28 | import jms4s.model.SessionType 29 | import org.scalatest.freespec.AsyncFreeSpec 30 | 31 | import scala.concurrent.duration.DurationInt 32 | 33 | trait JmsSpec extends AsyncFreeSpec with AsyncIOSpec with Jms4sBaseSpec { 34 | 35 | private def contexts(destination: DestinationName) = 36 | for { 37 | client <- jmsClientRes 38 | context = client.context 39 | receiveConsumer <- context 40 | .createContext(SessionType.AutoAcknowledge) 41 | .flatMap(_.createJmsConsumer(destination, 50.millis)) 42 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 43 | msg <- Resource.eval(context.createTextMessage(body)) // TODO create from sendContext? 44 | } yield (receiveConsumer, sendContext, msg) 45 | 46 | private def contextsWithTempQueue = 47 | for { 48 | client <- jmsClientRes 49 | temporaryDestination <- Resource.eval(client.createTemporaryQueue) 50 | context = client.context 51 | receiveConsumer <- context 52 | .createContext(SessionType.AutoAcknowledge) 53 | .flatMap(_.createJmsConsumer(temporaryDestination, 50.millis)) 54 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 55 | msg <- Resource.eval(context.createTextMessage(body)) // TODO create from sendContext? 56 | } yield (receiveConsumer, temporaryDestination, sendContext, msg) 57 | 58 | private def contextsWithTempTopic = 59 | for { 60 | client <- jmsClientRes 61 | temporaryDestination <- Resource.eval(client.createTemporaryTopic) 62 | context = client.context 63 | receiveConsumer <- context 64 | .createContext(SessionType.AutoAcknowledge) 65 | .flatMap(_.createJmsConsumer(temporaryDestination, 50.millis)) 66 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 67 | msg <- Resource.eval(context.createTextMessage(body)) // TODO create from sendContext? 68 | } yield (receiveConsumer, temporaryDestination, sendContext, msg) 69 | 70 | "publish to a queue and then receive" in { 71 | contexts(inputQueueName).use { 72 | case (receiveConsumer, sendContext, msg) => 73 | for { 74 | _ <- sendContext.send(inputQueueName, msg) 75 | text <- receiveBodyAsTextOrFail(receiveConsumer) 76 | } yield assert(text == body) 77 | } 78 | } 79 | "publish to a queue and then receive with a delay" in { 80 | contexts(inputQueueName).use { 81 | case (consumer, sendContext, msg) => 82 | for { 83 | producerTimestamp <- Clock[IO].realTime 84 | _ <- sendContext.send(inputQueueName, msg, delay) 85 | msg <- consumer.receiveJmsMessage 86 | deliveryTime <- Clock[IO].realTime 87 | actualBody <- msg.asTextF[IO] 88 | actualDelay = (deliveryTime - producerTimestamp) 89 | } yield assert(actualDelay >= delayWithTolerance && actualBody == body) 90 | } 91 | } 92 | "publish to a topic and then receive" in { 93 | contexts(topicName1).use { 94 | case (consumer, sendContext, msg) => 95 | for { 96 | _ <- sendContext.send(topicName1, msg) 97 | rec <- receiveBodyAsTextOrFail(consumer) 98 | } yield assert(rec == body) 99 | } 100 | } 101 | 102 | "publish to a temporary queue and then receive" in { 103 | contextsWithTempQueue.use { 104 | case (receiveConsumer, jmsDestination, sendContext, msg) => 105 | for { 106 | _ <- sendContext.send(jmsDestination, msg) 107 | text <- receiveBodyAsTextOrFail(receiveConsumer) 108 | } yield assert(text == body) 109 | } 110 | } 111 | "publish to a temporary queue and then receive with a delay" in { 112 | contextsWithTempQueue.use { 113 | case (consumer, jmsDestination, sendContext, msg) => 114 | for { 115 | producerTimestamp <- Clock[IO].realTime 116 | _ <- sendContext.send(jmsDestination, msg, delay) 117 | msg <- consumer.receiveJmsMessage 118 | deliveryTime <- Clock[IO].realTime 119 | actualBody <- msg.asTextF[IO] 120 | actualDelay = (deliveryTime - producerTimestamp) 121 | } yield assert(actualDelay >= delayWithTolerance && actualBody == body) 122 | } 123 | } 124 | "publish to a temporary topic and then receive" in { 125 | contextsWithTempTopic.use { 126 | case (consumer, jmsDestination, sendContext, msg) => 127 | for { 128 | _ <- sendContext.send(jmsDestination, msg) 129 | rec <- receiveBodyAsTextOrFail(consumer) 130 | } yield assert(rec == body) 131 | } 132 | 133 | } 134 | 135 | "update and get a JMSMessage property" in { 136 | contexts(topicName1).use { 137 | case (_, _, msg) => 138 | for { 139 | _ <- IO.fromTry(msg.setJMSType("newType")) 140 | t = msg.getJMSType 141 | } yield assert(t.contains("newType")) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['*'] 13 | push: 14 | branches: ['*'] 15 | tags: [v*, v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 20 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 21 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 22 | 23 | jobs: 24 | build: 25 | name: Build and Test 26 | strategy: 27 | matrix: 28 | os: [ubuntu-latest] 29 | scala: [2.13.10, 2.12.18] 30 | java: [adopt-hotspot@8, adopt-hotspot@11] 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - name: Checkout current branch (full) 34 | uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 0 37 | 38 | - name: Setup Java (adopt-hotspot@8) 39 | if: matrix.java == 'adopt-hotspot@8' 40 | uses: actions/setup-java@v2 41 | with: 42 | distribution: adopt-hotspot 43 | java-version: 8 44 | 45 | - name: Setup Java (adopt-hotspot@11) 46 | if: matrix.java == 'adopt-hotspot@11' 47 | uses: actions/setup-java@v2 48 | with: 49 | distribution: adopt-hotspot 50 | java-version: 11 51 | 52 | - name: Cache sbt 53 | uses: actions/cache@v2 54 | with: 55 | path: | 56 | ~/.sbt 57 | ~/.ivy2/cache 58 | ~/.coursier/cache/v1 59 | ~/.cache/coursier/v1 60 | ~/AppData/Local/Coursier/Cache/v1 61 | ~/Library/Caches/Coursier/v1 62 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 63 | 64 | - name: Check that workflows are up to date 65 | run: sbt ++${{ matrix.scala }} githubWorkflowCheck 66 | 67 | - name: Start docker containers 68 | run: docker-compose up --renew-anon-volumes --force-recreate -d 69 | 70 | - name: Test 71 | run: sbt ++${{ matrix.scala }} test 72 | 73 | - name: Stop docker containers 74 | run: docker-compose down 75 | 76 | - if: matrix.scala == '2.12.18' 77 | uses: ruby/setup-ruby@v1 78 | with: 79 | ruby-version: 2.6 80 | 81 | - if: matrix.scala == '2.12.18' 82 | run: gem update --system 83 | 84 | - if: matrix.scala == '2.12.18' 85 | run: gem install sass 86 | 87 | - if: matrix.scala == '2.12.18' 88 | run: gem install jekyll -v 4 89 | 90 | - if: matrix.scala == '2.12.18' 91 | run: sbt ++${{ matrix.scala }} site/makeMicrosite 92 | 93 | - name: Compress target directories 94 | run: tar cf targets.tar target ibm-mq/target examples/target site/target tests/target active-mq-artemis/target core/target project/target 95 | 96 | - name: Upload target directories 97 | uses: actions/upload-artifact@v2 98 | with: 99 | name: target-${{ matrix.os }}-${{ matrix.scala }}-${{ matrix.java }} 100 | path: targets.tar 101 | 102 | publish: 103 | name: Publish Artifacts 104 | needs: [build] 105 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 106 | strategy: 107 | matrix: 108 | os: [ubuntu-latest] 109 | scala: [2.13.10] 110 | java: [adopt-hotspot@8] 111 | runs-on: ${{ matrix.os }} 112 | steps: 113 | - name: Checkout current branch (full) 114 | uses: actions/checkout@v2 115 | with: 116 | fetch-depth: 0 117 | 118 | - name: Setup Java (adopt-hotspot@8) 119 | if: matrix.java == 'adopt-hotspot@8' 120 | uses: actions/setup-java@v2 121 | with: 122 | distribution: adopt-hotspot 123 | java-version: 8 124 | 125 | - name: Setup Java (adopt-hotspot@11) 126 | if: matrix.java == 'adopt-hotspot@11' 127 | uses: actions/setup-java@v2 128 | with: 129 | distribution: adopt-hotspot 130 | java-version: 11 131 | 132 | - name: Cache sbt 133 | uses: actions/cache@v2 134 | with: 135 | path: | 136 | ~/.sbt 137 | ~/.ivy2/cache 138 | ~/.coursier/cache/v1 139 | ~/.cache/coursier/v1 140 | ~/AppData/Local/Coursier/Cache/v1 141 | ~/Library/Caches/Coursier/v1 142 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 143 | 144 | - name: Download target directories (2.13.10) 145 | uses: actions/download-artifact@v2 146 | with: 147 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 148 | 149 | - name: Inflate target directories (2.13.10) 150 | run: | 151 | tar xf targets.tar 152 | rm targets.tar 153 | 154 | - name: Download target directories (2.12.18) 155 | uses: actions/download-artifact@v2 156 | with: 157 | name: target-${{ matrix.os }}-2.12.18-${{ matrix.java }} 158 | 159 | - name: Inflate target directories (2.12.18) 160 | run: | 161 | tar xf targets.tar 162 | rm targets.tar 163 | 164 | - name: Import signing key 165 | run: echo $PGP_SECRET | base64 -d | gpg --import 166 | 167 | - run: sbt ++${{ matrix.scala }} release 168 | 169 | scalafmt: 170 | name: Scalafmt 171 | strategy: 172 | matrix: 173 | os: [ubuntu-latest] 174 | scala: [2.13.10, 2.12.18] 175 | java: [temurin@11] 176 | runs-on: ${{ matrix.os }} 177 | steps: 178 | - name: Checkout current branch (full) 179 | uses: actions/checkout@v2 180 | with: 181 | fetch-depth: 0 182 | 183 | - name: Setup Java (adopt-hotspot@8) 184 | if: matrix.java == 'adopt-hotspot@8' 185 | uses: actions/setup-java@v2 186 | with: 187 | distribution: adopt-hotspot 188 | java-version: 8 189 | 190 | - name: Setup Java (adopt-hotspot@11) 191 | if: matrix.java == 'adopt-hotspot@11' 192 | uses: actions/setup-java@v2 193 | with: 194 | distribution: adopt-hotspot 195 | java-version: 11 196 | 197 | - name: Cache sbt 198 | uses: actions/cache@v2 199 | with: 200 | path: | 201 | ~/.sbt 202 | ~/.ivy2/cache 203 | ~/.coursier/cache/v1 204 | ~/.cache/coursier/v1 205 | ~/AppData/Local/Coursier/Cache/v1 206 | ~/Library/Caches/Coursier/v1 207 | key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} 208 | 209 | - name: Scalafmt 210 | run: sbt ++${{ matrix.scala }} scalafmtCheckAll 211 | 212 | build-success: 213 | name: Build Success 214 | needs: [build, scalafmt] 215 | strategy: 216 | matrix: 217 | os: [ubuntu-latest] 218 | scala: [2.13.10] 219 | java: [adopt-hotspot@8] 220 | runs-on: ${{ matrix.os }} 221 | steps: 222 | - run: echo Build Succeded 223 | 224 | site: 225 | name: Deploy site 226 | needs: [build] 227 | if: always() && needs.build.result == 'success' && (github.ref == 'refs/heads/main') 228 | strategy: 229 | matrix: 230 | os: [ubuntu-latest] 231 | scala: [2.13.10] 232 | java: [adopt-hotspot@11] 233 | runs-on: ${{ matrix.os }} 234 | steps: 235 | - name: Download target directories (2.13.10) 236 | uses: actions/download-artifact@v2 237 | with: 238 | name: target-${{ matrix.os }}-2.13.10-${{ matrix.java }} 239 | 240 | - name: Inflate target directories (2.13.10) 241 | run: | 242 | tar xf targets.tar 243 | rm targets.tar 244 | 245 | - name: Download target directories (2.12.18) 246 | uses: actions/download-artifact@v2 247 | with: 248 | name: target-${{ matrix.os }}-2.12.18-${{ matrix.java }} 249 | 250 | - name: Inflate target directories (2.12.18) 251 | run: | 252 | tar xf targets.tar 253 | rm targets.tar 254 | 255 | - name: Deploy site 256 | uses: peaceiris/actions-gh-pages@v3 257 | with: 258 | publish_dir: ./site/target/site 259 | github_token: ${{ secrets.GITHUB_TOKEN }} 260 | -------------------------------------------------------------------------------- /core/src/main/scala/jms4s/jms/JmsMessage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s.jms 23 | 24 | import cats.syntax.all._ 25 | import cats.{ ApplicativeError, MonadThrow, Show } 26 | import jms4s.config.{ DestinationName, QueueName, TopicName } 27 | import jms4s.jms.JmsMessage.{ JmsTextMessage, UnsupportedMessage } 28 | import jms4s.jms.utils.TryUtils._ 29 | 30 | import javax.jms._ 31 | import scala.util.control.NoStackTrace 32 | import scala.util.{ Failure, Success, Try } 33 | 34 | class JmsMessage private[jms4s] (private[jms4s] val wrapped: Message) { 35 | 36 | def attemptAsJmsTextMessage: Try[JmsTextMessage] = wrapped match { 37 | case textMessage: TextMessage => Success(new JmsTextMessage(textMessage)) 38 | case _ => Failure(UnsupportedMessage(wrapped)) 39 | } 40 | 41 | def attemptAsText: Try[String] = attemptAsJmsTextMessage.flatMap(_.getText) 42 | 43 | def asJmsTextMessageF[F[_]](implicit a: ApplicativeError[F, Throwable]): F[JmsTextMessage] = 44 | ApplicativeError[F, Throwable].fromTry(attemptAsJmsTextMessage) 45 | 46 | def asTextF[F[_]](implicit a: ApplicativeError[F, Throwable]): F[String] = 47 | ApplicativeError[F, Throwable].fromTry(attemptAsText) 48 | 49 | /** 50 | * settings properties works only on newly created message, if invoked on an already existing message they will fail. 51 | * Clone the message first and then modify it. 52 | */ 53 | def setJMSCorrelationId(correlationId: String): Try[Unit] = Try(wrapped.setJMSCorrelationID(correlationId)) 54 | def setJMSReplyTo(destination: JmsDestination): Try[Unit] = Try(wrapped.setJMSReplyTo(destination.wrapped)) 55 | def setJMSType(`type`: String): Try[Unit] = Try(wrapped.setJMSType(`type`)) 56 | def setJMSMessageID(messageID: String): Try[Unit] = Try(wrapped.setJMSMessageID(messageID)) 57 | def setJMSTimestamp(timestamp: Long): Try[Unit] = Try(wrapped.setJMSTimestamp(timestamp)) 58 | def setJMSDestination(destination: JmsDestination): Try[Unit] = Try(wrapped.setJMSDestination(destination.wrapped)) 59 | def setJMSDeliveryMode(deliveryMode: Int): Try[Unit] = Try(wrapped.setJMSDeliveryMode(deliveryMode)) 60 | def setJMSRedelivered(redelivered: Boolean): Try[Unit] = Try(wrapped.setJMSRedelivered(redelivered)) 61 | def setJMSExpiration(expiration: Long): Try[Unit] = Try(wrapped.setJMSExpiration(expiration)) 62 | def setJMSPriority(priority: Int): Try[Unit] = Try(wrapped.setJMSPriority(priority)) 63 | 64 | def setJMSCorrelationIDAsBytes(correlationId: Array[Byte]): Try[Unit] = 65 | Try(wrapped.setJMSCorrelationIDAsBytes(correlationId)) 66 | 67 | def getJMSMessageId: Option[String] = Try(Option(wrapped.getJMSMessageID)).toOpt 68 | def getJMSTimestamp: Option[Long] = Try(Option(wrapped.getJMSTimestamp)).toOpt 69 | def getJMSCorrelationId: Option[String] = Try(Option(wrapped.getJMSCorrelationID)).toOpt 70 | def getJMSCorrelationIdAsBytes: Option[Array[Byte]] = Try(Option(wrapped.getJMSCorrelationIDAsBytes)).toOpt 71 | def getJMSReplyTo: Option[Destination] = Try(Option(wrapped.getJMSReplyTo)).toOpt 72 | 73 | def getJMSReplyToNameF[F[_]: MonadThrow]: F[DestinationName] = 74 | MonadThrow[F] 75 | .catchNonFatal(wrapped.getJMSReplyTo) 76 | .ensureOr(_ => new NoSuchElementException("ReplyTo is null"))(_ != null) 77 | .flatMap { 78 | case q: Queue => QueueName(q.getQueueName).pure.widen 79 | case t: Topic => TopicName(t.getTopicName).pure.widen 80 | case x => new Exception(s"Failure extracting JMSReplyTo, unsupported Destination: $x").raiseError 81 | } 82 | def getJMSDestination: Option[Destination] = Try(Option(wrapped.getJMSDestination)).toOpt 83 | def getJMSDeliveryMode: Option[Int] = Try(Option(wrapped.getJMSDeliveryMode)).toOpt 84 | def getJMSRedelivered: Option[Boolean] = Try(Option(wrapped.getJMSRedelivered)).toOpt 85 | def getJMSType: Option[String] = Try(Option(wrapped.getJMSType)).toOpt 86 | def getJMSExpiration: Option[Long] = Try(Option(wrapped.getJMSExpiration)).toOpt 87 | def getJMSPriority: Option[Int] = Try(Option(wrapped.getJMSPriority)).toOpt 88 | def getJMSDeliveryTime: Option[Long] = Try(Option(wrapped.getJMSDeliveryTime)).toOpt 89 | 90 | def getBooleanProperty(name: String): Option[Boolean] = 91 | Try(Option(wrapped.getBooleanProperty(name))).toOpt 92 | 93 | def getByteProperty(name: String): Option[Byte] = 94 | Try(Option(wrapped.getByteProperty(name))).toOpt 95 | 96 | def getDoubleProperty(name: String): Option[Double] = 97 | Try(Option(wrapped.getDoubleProperty(name))).toOpt 98 | 99 | def getFloatProperty(name: String): Option[Float] = 100 | Try(Option(wrapped.getFloatProperty(name))).toOpt 101 | 102 | def getIntProperty(name: String): Option[Int] = 103 | Try(Option(wrapped.getIntProperty(name))).toOpt 104 | 105 | def getLongProperty(name: String): Option[Long] = 106 | Try(Option(wrapped.getLongProperty(name))).toOpt 107 | 108 | def getShortProperty(name: String): Option[Short] = 109 | Try(Option(wrapped.getShortProperty(name))).toOpt 110 | 111 | def getStringProperty(name: String): Option[String] = 112 | Try(Option(wrapped.getStringProperty(name))).toOpt 113 | 114 | def getObjectProperty(name: String): Option[Any] = 115 | Try(Option(wrapped.getObjectProperty(name))).toOpt 116 | 117 | def setBooleanProperty(name: String, value: Boolean): Try[Unit] = Try(wrapped.setBooleanProperty(name, value)) 118 | def setByteProperty(name: String, value: Byte): Try[Unit] = Try(wrapped.setByteProperty(name, value)) 119 | def setDoubleProperty(name: String, value: Double): Try[Unit] = Try(wrapped.setDoubleProperty(name, value)) 120 | def setFloatProperty(name: String, value: Float): Try[Unit] = Try(wrapped.setFloatProperty(name, value)) 121 | def setIntProperty(name: String, value: Int): Try[Unit] = Try(wrapped.setIntProperty(name, value)) 122 | def setLongProperty(name: String, value: Long): Try[Unit] = Try(wrapped.setLongProperty(name, value)) 123 | def setShortProperty(name: String, value: Short): Try[Unit] = Try(wrapped.setShortProperty(name, value)) 124 | def setStringProperty(name: String, value: String): Try[Unit] = Try(wrapped.setStringProperty(name, value)) 125 | def setObjectProperty(name: String, value: Any): Try[Unit] = Try(wrapped.setObjectProperty(name, value)) 126 | 127 | def properties: Try[Map[String, Any]] = 128 | JmsMessage.properties(wrapped) 129 | } 130 | 131 | object JmsMessage { 132 | 133 | def properties(msg: Message): Try[Map[String, Any]] = 134 | Try { 135 | val propertyNames = msg.getPropertyNames 136 | val buf = collection.mutable.Map.empty[String, Any] 137 | while (propertyNames.hasMoreElements) { 138 | val propertyName = propertyNames.nextElement.asInstanceOf[String] 139 | buf += propertyName -> msg.getObjectProperty(propertyName) 140 | } 141 | buf.toMap 142 | } 143 | 144 | implicit val showMessage: Show[Message] = Show.show[Message] { message => 145 | def getStringContent: Try[String] = message match { 146 | case message: TextMessage => Try(message.getText) 147 | case _ => Failure(new RuntimeException()) 148 | } 149 | 150 | Try { 151 | s""" 152 | |${properties(message).map(_.mkString("\n")).getOrElse("")} 153 | |JMSMessageID ${message.getJMSMessageID} 154 | |JMSTimestamp ${message.getJMSTimestamp} 155 | |JMSCorrelationID ${message.getJMSCorrelationID} 156 | |JMSReplyTo ${message.getJMSReplyTo} 157 | |JMSDestination ${message.getJMSDestination} 158 | |JMSDeliveryMode ${message.getJMSDeliveryMode} 159 | |JMSRedelivered ${message.getJMSRedelivered} 160 | |JMSType ${message.getJMSType} 161 | |JMSExpiration ${message.getJMSExpiration} 162 | |JMSPriority ${message.getJMSPriority} 163 | |=============================================================================== 164 | |${getStringContent.getOrElse(s"Unsupported message type: $message")} 165 | """.stripMargin 166 | }.getOrElse("") 167 | } 168 | 169 | implicit val showJmsMessage: Show[JmsMessage] = Show.show[JmsMessage](_.wrapped.show) 170 | 171 | case class UnsupportedMessage(message: Message) 172 | extends Exception("Unsupported Message: " + message.show) 173 | with NoStackTrace 174 | 175 | class JmsTextMessage private[jms4s] (override private[jms4s] val wrapped: TextMessage) extends JmsMessage(wrapped) { 176 | 177 | def setText(text: String): Try[Unit] = 178 | Try(wrapped.setText(text)) 179 | 180 | def getText: Try[String] = Try(wrapped.getText) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /tests/src/test/scala/jms4s/JmsClientSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Functional Programming in Bologna 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | * this software and associated documentation files (the "Software"), to deal in 6 | * the Software without restriction, including without limitation the rights to 7 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | * the Software, and to permit persons to whom the Software is furnished to do so, 9 | * subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | package jms4s 23 | 24 | import cats.effect._ 25 | import cats.effect.testing.scalatest.AsyncIOSpec 26 | import cats.syntax.all._ 27 | import jms4s.JmsAcknowledgerConsumer.AckAction 28 | import jms4s.JmsAutoAcknowledgerConsumer.AutoAckAction 29 | import jms4s.JmsTransactedConsumer.TransactionAction 30 | import jms4s.basespec.Jms4sBaseSpec 31 | import jms4s.config.QueueName 32 | import jms4s.jms.JmsMessage.JmsTextMessage 33 | import jms4s.jms.{ JmsMessage, MessageFactory } 34 | import jms4s.model.SessionType 35 | import org.scalatest.freespec.AsyncFreeSpec 36 | 37 | import scala.concurrent.duration.DurationInt 38 | 39 | trait JmsClientSpec extends AsyncFreeSpec with AsyncIOSpec with Jms4sBaseSpec { 40 | 41 | s"publish $nMessages messages to a queue and then consume them concurrently with local transactions" in { 42 | val res = for { 43 | jmsClient <- jmsClientRes 44 | context = jmsClient.context 45 | consumer <- jmsClient.createTransactedConsumer(inputQueueName, poolSize, pollingInterval) 46 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 47 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 48 | } yield (consumer, sendContext, bodies.toSet, messages) 49 | 50 | res.use { 51 | case (consumer, context, bodies, messages) => 52 | for { 53 | _ <- messages.traverse_(msg => context.send(inputQueueName, msg)) 54 | _ <- logger.info(s"Pushed ${messages.size} messages.") 55 | received <- Ref.of[IO, Set[String]](Set()) 56 | consumerFiber <- consumer.handle { (message, _) => 57 | for { 58 | body <- message.asTextF[IO] 59 | _ <- received.update(_ + body) 60 | } yield TransactionAction.commit 61 | }.start 62 | _ <- logger.info(s"Consumer started. Collecting messages from the queue...") 63 | receivedMessages <- (received.get.iterateUntil(_.eqv(bodies)) >> received.get) 64 | .timeout(timeout) 65 | .guarantee(consumerFiber.cancel) 66 | } yield assert(receivedMessages == bodies) 67 | } 68 | } 69 | 70 | s"publish $nMessages messages, consume them concurrently with local transactions and then republishing to other queues" in { 71 | 72 | val res = for { 73 | jmsClient <- jmsClientRes 74 | context = jmsClient.context 75 | consumer <- jmsClient.createTransactedConsumer(inputQueueName, poolSize, pollingInterval) 76 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 77 | messages <- Resource.eval(bodies.traverse(i => sendContext.createTextMessage(i))) 78 | consumer1 <- context 79 | .createContext(SessionType.AutoAcknowledge) 80 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 81 | consumer2 <- context 82 | .createContext(SessionType.AutoAcknowledge) 83 | .flatMap(_.createJmsConsumer(outputQueueName2, pollingInterval)) 84 | } yield (consumer, sendContext, consumer1, consumer2, bodies.toSet, messages) 85 | 86 | res.use { 87 | case (consumer, sendContext, consumer1, consumer2, bodies, messages) => 88 | for { 89 | _ <- messages.traverse_(msg => sendContext.send(inputQueueName, msg)) 90 | _ <- logger.info(s"Pushed ${messages.size} messages.") 91 | consumerToProducerFiber <- consumer.handle { (message, mf) => 92 | for { 93 | text <- message.asTextF[IO] 94 | newm <- mf.makeTextMessage(text) 95 | } yield 96 | if (text.toInt % 2 == 0) 97 | TransactionAction.send[IO](newm, outputQueueName1) 98 | else 99 | TransactionAction.send[IO](newm, outputQueueName2) 100 | }.start 101 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queues...") 102 | received1 <- Ref.of[IO, Set[String]](Set()) 103 | received2 <- Ref.of[IO, Set[String]](Set()) 104 | receivedMessages <- (( 105 | receiveUntil(consumer1, received1, nMessages / 2), 106 | receiveUntil(consumer2, received2, nMessages / 2) 107 | ).parTupled.timeout(timeout) >> (received1.get, received2.get).mapN(_ ++ _)) 108 | .guarantee(consumerToProducerFiber.cancel) 109 | } yield assert(receivedMessages == bodies) 110 | } 111 | } 112 | 113 | s"publish $nMessages messages and then consume them concurrently with acknowledge" in { 114 | 115 | val res = for { 116 | jmsClient <- jmsClientRes 117 | context = jmsClient.context 118 | consumer <- jmsClient.createAcknowledgerConsumer(inputQueueName, poolSize, pollingInterval) 119 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 120 | messages <- Resource.eval(bodies.traverse(i => sendContext.createTextMessage(i))) 121 | } yield (consumer, sendContext, bodies.toSet, messages) 122 | 123 | res.use { 124 | case (consumer, sendContext, bodies, messages) => 125 | for { 126 | _ <- messages.traverse_(msg => sendContext.send(inputQueueName, msg)) 127 | _ <- logger.info(s"Pushed ${messages.size} messages.") 128 | received <- Ref.of[IO, Set[String]](Set()) 129 | consumerFiber <- consumer.handle { (message, _) => 130 | for { 131 | body <- message.asTextF[IO] 132 | _ <- received.update(_ + body) 133 | } yield AckAction.ack 134 | }.start 135 | _ <- logger.info(s"Consumer started. Collecting messages from the queue...") 136 | receivedMessages <- (received.get.iterateUntil(_.eqv(bodies)).timeout(timeout) >> received.get) 137 | .guarantee(consumerFiber.cancel) 138 | } yield assert(receivedMessages == bodies) 139 | } 140 | } 141 | 142 | s"publish $nMessages messages, consume them concurrently and then republishing to other queues, with acknowledge" in { 143 | 144 | val res = for { 145 | jmsClient <- jmsClientRes 146 | context = jmsClient.context 147 | consumer <- jmsClient.createAcknowledgerConsumer(inputQueueName, poolSize, pollingInterval) 148 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 149 | messages <- Resource.eval(bodies.traverse(i => sendContext.createTextMessage(i))) 150 | consumer1 <- context 151 | .createContext(SessionType.AutoAcknowledge) 152 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 153 | consumer2 <- context 154 | .createContext(SessionType.AutoAcknowledge) 155 | .flatMap(_.createJmsConsumer(outputQueueName2, pollingInterval)) 156 | } yield (consumer, sendContext, consumer1, consumer2, bodies.toSet, messages) 157 | 158 | res.use { 159 | case (consumer, sendContext, consumer1, consumer2, bodies, messages) => 160 | for { 161 | _ <- messages.traverse_(msg => sendContext.send(inputQueueName, msg)) 162 | _ <- logger.info(s"Pushed ${messages.size} messages.") 163 | consumerToProducerFiber <- consumer.handle { (message, mf) => 164 | for { 165 | text <- message.asTextF[IO] 166 | newm: JmsMessage.JmsTextMessage <- mf.makeTextMessage(text) 167 | } yield 168 | if (text.toInt % 2 == 0) 169 | AckAction.send[IO](newm, outputQueueName1) 170 | else 171 | AckAction.send[IO](newm, outputQueueName2) 172 | }.start 173 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queues...") 174 | received1 <- Ref.of[IO, Set[String]](Set()) 175 | received2 <- Ref.of[IO, Set[String]](Set()) 176 | receivedMessages <- (( 177 | receiveUntil(consumer1, received1, nMessages / 2), 178 | receiveUntil(consumer2, received2, nMessages / 2) 179 | ).parTupled.timeout(timeout) >> (received1.get, received2.get).mapN(_ ++ _)) 180 | .guarantee(consumerToProducerFiber.cancel) 181 | } yield assert(receivedMessages == bodies) 182 | } 183 | } 184 | 185 | s"publish $nMessages messages and then consume them concurrently with auto-acknowledge" in { 186 | 187 | val res = for { 188 | jmsClient <- jmsClientRes 189 | context = jmsClient.context 190 | consumer <- jmsClient.createAutoAcknowledgerConsumer(inputQueueName, poolSize, pollingInterval) 191 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 192 | messages <- Resource.eval(bodies.traverse(i => sendContext.createTextMessage(i))) 193 | } yield (consumer, sendContext, bodies.toSet, messages) 194 | 195 | res.use { 196 | case (consumer, sendContext, bodies, messages) => 197 | for { 198 | _ <- messages.traverse_(msg => sendContext.send(inputQueueName, msg)) 199 | _ <- logger.info(s"Pushed ${messages.size} messages.") 200 | received <- Ref.of[IO, Set[String]](Set()) 201 | consumerFiber <- consumer.handle { (message, _) => 202 | for { 203 | body <- message.asTextF[IO] 204 | _ <- received.update(_ + body) 205 | } yield AutoAckAction.noOp 206 | }.start 207 | _ <- logger.info(s"Consumer started. Collecting messages from the queue...") 208 | receivedMessages <- (received.get.iterateUntil(_.eqv(bodies)).timeout(timeout) >> received.get) 209 | .guarantee(consumerFiber.cancel) 210 | } yield assert(receivedMessages == bodies) 211 | } 212 | } 213 | 214 | s"publish $nMessages messages, consume them concurrently and then republishing to other queues, with auto-acknowledge" in { 215 | 216 | val res = for { 217 | jmsClient <- jmsClientRes 218 | context = jmsClient.context 219 | consumer <- jmsClient.createAutoAcknowledgerConsumer(inputQueueName, poolSize, pollingInterval) 220 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 221 | messages <- Resource.eval(bodies.traverse(i => sendContext.createTextMessage(i))) 222 | consumer1 <- context 223 | .createContext(SessionType.AutoAcknowledge) 224 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 225 | consumer2 <- context 226 | .createContext(SessionType.AutoAcknowledge) 227 | .flatMap(_.createJmsConsumer(outputQueueName2, pollingInterval)) 228 | 229 | } yield (consumer, sendContext, consumer1, consumer2, bodies.toSet, messages) 230 | 231 | res.use { 232 | case (consumer, sendContext, consumer1, consumer2, bodies, messages) => 233 | for { 234 | _ <- messages.traverse_(msg => sendContext.send(inputQueueName, msg)) 235 | _ <- logger.info(s"Pushed ${messages.size} messages.") 236 | consumerToProducerFiber <- consumer.handle { (message, _) => 237 | for { 238 | tm <- message.asJmsTextMessageF[IO] 239 | text <- tm.asTextF[IO] 240 | } yield 241 | if (text.toInt % 2 == 0) 242 | AutoAckAction.send[IO](tm, outputQueueName1) 243 | else AutoAckAction.send[IO](tm, outputQueueName2) 244 | }.start 245 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queues...") 246 | received1 <- Ref.of[IO, Set[String]](Set()) 247 | received2 <- Ref.of[IO, Set[String]](Set()) 248 | receivedMessages <- (( 249 | receiveUntil(consumer1, received1, nMessages / 2), 250 | receiveUntil(consumer2, received2, nMessages / 2) 251 | ).parTupled.timeout(timeout) >> (received1.get, received2.get).mapN(_ ++ _)) 252 | .guarantee(consumerToProducerFiber.cancel) 253 | } yield assert(receivedMessages == bodies) 254 | } 255 | } 256 | 257 | s"send $nMessages messages in a Queue with pooled producer and consume them" in { 258 | 259 | val res = for { 260 | jmsClient <- jmsClientRes 261 | context = jmsClient.context 262 | consumer <- context 263 | .createContext(SessionType.AutoAcknowledge) 264 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 265 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 266 | producer <- jmsClient.createProducer(poolSize) 267 | } yield (producer, consumer, bodies.toSet, messages) 268 | 269 | res.use { 270 | case (producer, consumer, bodies, messages) => 271 | for { 272 | _ <- messages.parTraverse_(msg => producer.send(messageFactory(msg, outputQueueName1))) 273 | _ <- logger.info(s"Pushed ${messages.size} messages.") 274 | _ <- logger.info(s"Consumer to Producer started.\nCollecting messages from output queue...") 275 | received <- Ref.of[IO, Set[String]](Set()) 276 | receivedMessages <- receiveUntil(consumer, received, nMessages).timeout(timeout) >> received.get 277 | } yield assert(receivedMessages == bodies) 278 | } 279 | } 280 | 281 | s"sendN $nMessages messages in a Queue with pooled producer and consume them" in { 282 | 283 | val res = for { 284 | jmsClient <- jmsClientRes 285 | context = jmsClient.context 286 | consumer <- context 287 | .createContext(SessionType.AutoAcknowledge) 288 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 289 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 290 | producer <- jmsClient.createProducer(poolSize) 291 | } yield (producer, consumer, bodies.toSet, messages) 292 | 293 | res.use { 294 | case (producer, consumer, bodies, messages) => 295 | for { 296 | _ <- messages.toNel.traverse_(msg => producer.sendN(messageFactory(msg, outputQueueName1))) 297 | _ <- logger.info(s"Pushed ${messages.size} messages.") 298 | _ <- logger.info(s"Consumer to Producer started.\nCollecting messages from output queue...") 299 | received <- Ref.of[IO, Set[String]](Set()) 300 | receivedMessages <- receiveUntil(consumer, received, nMessages).timeout(timeout) >> received.get 301 | } yield assert(receivedMessages == bodies) 302 | } 303 | } 304 | 305 | s"send $nMessages messages in a Topic with pooled producer and consume them" in { 306 | 307 | val res = for { 308 | jmsClient <- jmsClientRes 309 | context = jmsClient.context 310 | consumer <- context 311 | .createContext(SessionType.AutoAcknowledge) 312 | .flatMap(_.createJmsConsumer(topicName1, pollingInterval)) 313 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 314 | producer <- jmsClient.createProducer(poolSize) 315 | } yield (producer, consumer, bodies.toSet, messages) 316 | 317 | res.use { 318 | case (producer, consumer, bodies, messages) => 319 | for { 320 | _ <- messages.parTraverse_(msg => producer.send(messageFactory(msg, topicName1))) 321 | _ <- logger.info(s"Pushed ${messages.size} messages.") 322 | _ <- logger.info(s"Consumer to Producer started.\nCollecting messages from output queue...") 323 | received <- Ref.of[IO, Set[String]](Set()) 324 | receivedMessages <- receiveUntil(consumer, received, nMessages).timeout(timeout) >> received.get 325 | } yield assert(receivedMessages == bodies) 326 | } 327 | } 328 | 329 | s"sendN $nMessages messages in a Topic with pooled producer and consume them" in { 330 | 331 | val res = for { 332 | jmsClient <- jmsClientRes 333 | context = jmsClient.context 334 | consumer <- context 335 | .createContext(SessionType.AutoAcknowledge) 336 | .flatMap(_.createJmsConsumer(topicName1, pollingInterval)) 337 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 338 | producer <- jmsClient.createProducer(poolSize) 339 | } yield (producer, consumer, bodies.toSet, messages) 340 | 341 | res.use { 342 | case (producer, consumer, bodies, messages) => 343 | for { 344 | _ <- messages.toNel.fold(IO.unit)(ms => producer.sendN(messageFactory(ms, topicName1))) 345 | _ <- logger.info(s"Pushed ${messages.size} messages.") 346 | _ <- logger.info(s"Consumer to Producer started.\nCollecting messages from output queue...") 347 | received <- Ref.of[IO, Set[String]](Set()) 348 | receivedMessages <- receiveUntil(consumer, received, nMessages).timeout(timeout) >> received.get 349 | } yield assert(receivedMessages == bodies) 350 | } 351 | } 352 | 353 | s"send $nMessages messages in two Queues with pooled producer and consume them" in { 354 | 355 | val res = for { 356 | jmsClient <- jmsClientRes 357 | context = jmsClient.context 358 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 359 | producer <- jmsClient.createProducer(poolSize) 360 | consumer1 <- context 361 | .createContext(SessionType.AutoAcknowledge) 362 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 363 | consumer2 <- context 364 | .createContext(SessionType.AutoAcknowledge) 365 | .flatMap(_.createJmsConsumer(outputQueueName2, pollingInterval)) 366 | } yield (producer, consumer1, consumer2, bodies.toSet, messages) 367 | 368 | res.use { 369 | case (producer, consumer1, consumer2, bodies, messages) => 370 | for { 371 | _ <- messages.parTraverse_(msg => 372 | producer.send(messageFactory(msg, outputQueueName1)) *> producer.send( 373 | messageFactory(msg, outputQueueName2) 374 | ) 375 | ) 376 | _ <- logger.info(s"Pushed ${messages.size} messages.") 377 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queue...") 378 | firstBatch <- Ref.of[IO, Set[String]](Set()) 379 | firstBatchMessages <- receiveUntil(consumer1, firstBatch, nMessages).timeout(timeout) >> firstBatch.get 380 | secondBatch <- Ref.of[IO, Set[String]](Set()) 381 | secondBatchMessages <- receiveUntil(consumer2, secondBatch, nMessages).timeout( 382 | timeout 383 | ) >> secondBatch.get 384 | } yield assert(firstBatchMessages == bodies && secondBatchMessages == bodies) 385 | } 386 | } 387 | 388 | s"send $nMessages messages in two Topics with pooled producer and consume them" in { 389 | 390 | val res = for { 391 | jmsClient <- jmsClientRes 392 | context = jmsClient.context 393 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 394 | producer <- jmsClient.createProducer(poolSize) 395 | consumer1 <- context 396 | .createContext(SessionType.AutoAcknowledge) 397 | .flatMap(_.createJmsConsumer(topicName1, pollingInterval)) 398 | consumer2 <- context 399 | .createContext(SessionType.AutoAcknowledge) 400 | .flatMap(_.createJmsConsumer(topicName2, pollingInterval)) 401 | } yield (producer, consumer1, consumer2, bodies.toSet, messages) 402 | 403 | res.use { 404 | case (producer, consumer1, consumer2, bodies, messages) => 405 | for { 406 | _ <- messages.parTraverse_(msg => 407 | producer.send(messageFactory(msg, topicName1)) *> producer.send(messageFactory(msg, topicName2)) 408 | ) 409 | _ <- logger.info(s"Pushed ${messages.size} messages.") 410 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queue...") 411 | firstBatch <- Ref.of[IO, Set[String]](Set()) 412 | firstBatchMessages <- receiveUntil(consumer1, firstBatch, nMessages).timeout(timeout) >> firstBatch.get 413 | secondBatch <- Ref.of[IO, Set[String]](Set()) 414 | secondBatchMessages <- receiveUntil(consumer2, secondBatch, nMessages).timeout(timeout) >> secondBatch.get 415 | } yield assert(firstBatchMessages == bodies && secondBatchMessages == bodies) 416 | } 417 | } 418 | 419 | s"send a message with delay in a Queue with producer and consume it" in { 420 | val res = for { 421 | jmsClient <- jmsClientRes 422 | context = jmsClient.context 423 | consumer <- context 424 | .createContext(SessionType.AutoAcknowledge) 425 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 426 | message <- Resource.eval(context.createTextMessage(body)) 427 | producer <- jmsClient.createProducer(poolSize) 428 | } yield (producer, consumer, message) 429 | 430 | res.use { 431 | case (producer, consumer, message) => 432 | for { 433 | producerTimestamp <- Clock[IO].realTime 434 | _ <- producer.sendWithDelay(messageWithDelayFactory((message, (outputQueueName1, Some(delay))))) 435 | _ <- logger.info(s"Pushed message with body: $body.") 436 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queue...") 437 | receivedMessage <- receiveMessage(consumer).timeout(timeout) 438 | deliveryTime <- Clock[IO].realTime 439 | actualBody <- receivedMessage.asTextF[IO] 440 | actualDelay = (deliveryTime - producerTimestamp) 441 | } yield assert(actualDelay >= delayWithTolerance && actualBody == body) 442 | } 443 | } 444 | 445 | s"send more than poolSize ($poolSize) failing messages do not deadlock" in { 446 | val res = for { 447 | jmsClient <- jmsClientRes 448 | context = jmsClient.context 449 | consumer <- context 450 | .createContext(SessionType.AutoAcknowledge) 451 | .flatMap(_.createJmsConsumer(outputQueueName1, pollingInterval)) 452 | message <- Resource.eval(context.createTextMessage(body)) 453 | producer <- jmsClient.createProducer(poolSize) 454 | } yield (producer, consumer, message) 455 | 456 | res.use { 457 | case (producer, consumer, message) => 458 | for { 459 | _ <- (0 until poolSize).toList.traverse_ { _ => 460 | producer 461 | .send(_ => IO.raiseError(new RuntimeException("failed producing"))) 462 | .handleErrorWith(logger.error(_)("")) 463 | } 464 | _ <- producer 465 | .send(messageFactory(message, outputQueueName1)) 466 | .timeoutTo(timeout, IO(fail("in deadlock"))) 467 | 468 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queue...") 469 | _ <- receiveMessage(consumer).timeout(timeout) 470 | } yield succeed 471 | } 472 | } 473 | s"publish a message, consume it, update it(cloning) and then republishing to an other queue" in { 474 | 475 | val res = for { 476 | jmsClient <- jmsClientRes 477 | producer <- jmsClient.createProducer(1) 478 | consumer <- jmsClient.createTransactedConsumer(inputQueueName, poolSize, pollingInterval) 479 | consumer1 <- jmsClient.createAutoAcknowledgerConsumer(outputQueueName1, poolSize, pollingInterval) 480 | } yield (consumer, producer, consumer1) 481 | 482 | res.use { 483 | case (consumer, producer, downstreamConsumer) => 484 | for { 485 | _ <- producer.send { mf => 486 | mf.makeTextMessage("msg") 487 | .flatTap(_.setStringProperty("custom_prop1", "custom_value").liftTo[IO]) 488 | .map((_, inputQueueName)) 489 | } 490 | _ <- logger.info(s"Pushed ${bodies.size} messages.") 491 | sent <- Deferred[IO, JmsTextMessage] 492 | consumerToProducerFiber <- consumer.handle { (message, mf) => 493 | for { 494 | textMessage <- message.asJmsTextMessageF[IO] 495 | _ <- sent.complete(textMessage) 496 | newm <- mf.attemptCloneMessage(textMessage).flatMap(_.liftTo[IO]) 497 | _ <- newm.setStringProperty("custom_prop2", "value2").liftTo[IO] 498 | } yield TransactionAction.send[IO](newm, outputQueueName1) 499 | }.start 500 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queues...") //todo remove 501 | receivedMessages <- receiveUntil(downstreamConsumer, nMessages = 1) 502 | .timeout(timeout) 503 | .guarantee(consumerToProducerFiber.cancel) 504 | x <- sent.get 505 | } yield (x, receivedMessages.head) 506 | }.asserting { 507 | case (original, received) => 508 | assert { 509 | received.getText.contains_("msg") && 510 | received.getStringProperty("custom_prop1") == original.getStringProperty("custom_prop1") && 511 | received.getStringProperty("custom_prop2").contains("value2") 512 | } 513 | } 514 | 515 | } 516 | 517 | s"publish a message, consume it, clone it, and then republish delayed to the same queue (retry) " in { 518 | def sendToDownstream(mf: MessageFactory[IO], message: JmsTextMessage, q: QueueName): IO[TransactionAction[IO]] = 519 | for { 520 | text <- message.asTextF[IO] 521 | newMsg <- mf.makeTextMessage(text.toUpperCase) 522 | } yield TransactionAction.send[IO](newMsg, q) 523 | 524 | def cloneAndRetry(mf: MessageFactory[IO], message: JmsTextMessage, q: QueueName): IO[TransactionAction[IO]] = 525 | for { 526 | newMsg <- mf.cloneMessageF(message) 527 | _ <- newMsg.setBooleanProperty("visited", true).liftTo[IO] 528 | } yield TransactionAction.sendWithDelay[IO](newMsg, q, Some(100.millis)) 529 | 530 | val res = for { 531 | jmsClient <- jmsClientRes 532 | producer <- jmsClient.createProducer(1) 533 | consumer <- jmsClient.createTransactedConsumer(inputQueueName, poolSize, pollingInterval) 534 | consumer1 <- jmsClient.createAutoAcknowledgerConsumer(outputQueueName1, poolSize, pollingInterval) 535 | } yield (consumer, producer, consumer1) 536 | 537 | res.use { 538 | case (consumer, producer, downstreamConsumer) => 539 | for { 540 | _ <- producer.send { mf => 541 | mf.makeTextMessage("msg") 542 | .flatTap(_.setStringProperty("custom_prop1", "custom_value").liftTo[IO]) 543 | .map((_, inputQueueName)) 544 | } 545 | _ <- logger.info(s"Pushed ${bodies.size} messages.") 546 | consumerToProducerFiber <- consumer.handle { (message, mf) => 547 | for { 548 | textMessage <- message.asJmsTextMessageF[IO] 549 | action <- textMessage.getBooleanProperty("visited") match { 550 | case Some(true) => 551 | sendToDownstream(mf, textMessage, outputQueueName1) 552 | case _ => 553 | cloneAndRetry(mf, textMessage, inputQueueName) 554 | } 555 | } yield action 556 | }.start 557 | _ <- logger.info(s"Consumer to Producer started. Collecting messages from output queues...") //todo remove 558 | receivedMessages <- receiveUntil(downstreamConsumer, nMessages = 1) 559 | .timeout(timeout) 560 | .guarantee(consumerToProducerFiber.cancel) 561 | } yield receivedMessages.head 562 | }.asserting(received => assert(received.getText.contains_("MSG"))) 563 | 564 | } 565 | 566 | s"publish $nMessages messages to a temporary queue and then consume them with local transactions" in { 567 | val res = for { 568 | jmsClient <- jmsClientRes 569 | context = jmsClient.context 570 | temporaryDestination <- Resource.eval(jmsClient.createTemporaryQueue) 571 | concurrencyLevel = 1 // IBMMQ defaults create non shareable temp queues 572 | consumer <- jmsClient.createTransactedConsumer(temporaryDestination, concurrencyLevel, pollingInterval) 573 | sendContext <- context.createContext(SessionType.AutoAcknowledge) 574 | messages <- Resource.eval(bodies.traverse(i => context.createTextMessage(i))) 575 | } yield (temporaryDestination, consumer, sendContext, bodies.toSet, messages) 576 | 577 | res.use { 578 | case (temporaryDestination, consumer, context, bodies, messages) => 579 | for { 580 | _ <- messages.traverse_(msg => context.send(temporaryDestination, msg)) 581 | _ <- logger.info(s"Pushed ${messages.size} messages.") 582 | received <- Ref.of[IO, Set[String]](Set()) 583 | consumerFiber <- consumer.handle { (message, _) => 584 | for { 585 | body <- message.asTextF[IO] 586 | _ <- received.update(_ + body) 587 | } yield TransactionAction.commit 588 | }.start 589 | _ <- logger.info(s"Consumer started. Collecting messages from the queue...") 590 | receivedMessages <- (received.get.iterateUntil(_.eqv(bodies)) >> received.get) 591 | .timeout(timeout) 592 | .guarantee(consumerFiber.cancel) 593 | } yield assert(receivedMessages == bodies) 594 | } 595 | } 596 | 597 | s"sendN $nMessages request(with a replyTo) in a Queue, consume them and respond in a temporary Queue" in { 598 | val res = for { 599 | jmsClient <- jmsClientRes 600 | responseQ <- Resource.eval(jmsClient.createTemporaryQueue) 601 | producer <- jmsClient.createProducer(poolSize) 602 | replier <- jmsClient.createAcknowledgerConsumer(inputQueueName, poolSize, pollingInterval) 603 | responseConsumer <- jmsClient.createAutoAcknowledgerConsumer(responseQ, 1, pollingInterval) 604 | } yield (producer, replier, responseQ, responseConsumer) 605 | 606 | res.use { 607 | case (producer, consumer, responseQ, responseConsumer) => 608 | for { 609 | _ <- bodies.parTraverse { msg => 610 | producer.send(mf => 611 | for { 612 | msg <- mf.makeTextMessage(msg) 613 | _ <- msg.setJMSReplyTo(responseQ.destination).liftTo[IO] 614 | } yield (msg, inputQueueName) 615 | ) 616 | } 617 | _ <- logger.info(s"Pushed ${bodies.size} messages.") 618 | consumer <- consumer.handle { 619 | case (request, mf) => 620 | for { 621 | responseDest <- request.getJMSReplyToNameF[IO] 622 | responseMsg <- mf.makeTextMessage("response") 623 | } yield AckAction.send[IO](responseMsg, responseDest) 624 | }.start 625 | 626 | received <- receiveUntil(responseConsumer, bodies.size.toLong) 627 | .timeout(timeout) 628 | .guarantee(consumer.cancel) 629 | texts <- received.traverse(_.asTextF[IO]) 630 | } yield assert( 631 | texts.size == bodies.size && 632 | texts.forall(_.startsWith("response")) 633 | ) 634 | } 635 | } 636 | } 637 | --------------------------------------------------------------------------------