├── 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/.*/' -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 | [](https://travis-ci.com/fp-in-bo/jms4s)
3 | [](https://maven-badges.herokuapp.com/maven-central/dev.fpinbo/jms4s_2.12)
4 | 
5 | [![Mergify Status][mergify-status]][mergify]
6 | [](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 |
--------------------------------------------------------------------------------