From a2256beaa0c7b7e28bcd10560ff6d2db84f3f35a Mon Sep 17 00:00:00 2001 From: baovitt Date: Tue, 23 Jul 2024 09:56:02 -0400 Subject: [PATCH 1/4] initial commit --- build.sbt | 5 +++ scala-libraries/README.md | 3 +- .../com/baeldung/elasticmq/DirectClient.scala | 5 +++ .../com/baeldung/elasticmq/RestClient.scala | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala create mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala diff --git a/build.sbt b/build.sbt index 9590494e1..883ccfd47 100644 --- a/build.sbt +++ b/build.sbt @@ -439,6 +439,11 @@ lazy val scala_libraries = (project in file("scala-libraries")) "nl.gn0s1s" %% "elastic4s-core" % elastic4sVersion, logback ), + libraryDependencies ++= Seq( + "org.elasticmq" %% "elasticmq-core" % "1.6.5", + "org.elasticmq" %% "elasticmq-server" % "1.6.5", + "org.elasticmq" %% "elasticmq-rest-sqs" % "1.6.5" + ), Defaults.itSettings ) diff --git a/scala-libraries/README.md b/scala-libraries/README.md index 40012dfc6..d74729b54 100644 --- a/scala-libraries/README.md +++ b/scala-libraries/README.md @@ -7,4 +7,5 @@ - [A Guide to the Scala Retry Library](https://www.baeldung.com/scala/retry-library) - [Introduction to Apache Log4j in Scala](https://www.baeldung.com/scala/apache-log4j) - [Logging in Scala Applications Using Scala-Logging](https://www.baeldung.com/scala/scala-logging) -- [AWScala – AWS SDK for Scala](https://www.baeldung.com/scala/awscala-aws-sdk-for-scala) \ No newline at end of file +- [AWScala – AWS SDK for Scala](https://www.baeldung.com/scala/awscala-aws-sdk-for-scala) +- [Introduction to ElasticMQ] https://baeldung.com/scala/introduction-to-elasticmq \ No newline at end of file diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala new file mode 100644 index 000000000..88eeb19fa --- /dev/null +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala @@ -0,0 +1,5 @@ +package com.baeldung.elasticmq + +// val config = ConfigFactory.load("elasticmq.conf") +// val server = new ElasticMQServer(new ElasticMQServerConfig(config)) +// server.start() \ No newline at end of file diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala new file mode 100644 index 000000000..308879206 --- /dev/null +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala @@ -0,0 +1,31 @@ +package com.baeldung.elasticmq + +import org.elasticmq.rest.sqs.SQSRestServerBuilder +import org.elasticmq.server.ElasticMQServer +import org.elasticmq.server.config.ElasticMQServerConfig + +import com.typesafe.config.ConfigFactory + +import org.apache.pekko.actor.{Actor, ActorRef, ActorSystem} +import org.apache.pekko.event.LoggingAdapter + +implicit val actorSystem: ActorSystem = ActorSystem.create() +implicit val executionContext: concurrent.ExecutionContextExecutor = actorSystem.dispatcher +implicit val m_logger: LoggingAdapter = actorSystem.log + +// class QueueActor extends Actor: +// val messageQueue: mutable.PriorityQueue[InternalMessage]() +// val awaiting: mutable.PriorityQueue[ActorRef]() + +// def receive = case ReceiveMessages => + + +// val result: Future[ActorRef] = flow: +// (queueManager ? Lookup(name)).apply() match +// case Some(queueActor) => queueActor +// case None => (queueManager ? Create(name)).apply() + +lazy val server = SQSRestServerBuilder + .withPort(9325) + .withInterface("localhost") + .start() \ No newline at end of file From 8a4c31dfdcb4d36d34e25fdd9495c7442931f3dd Mon Sep 17 00:00:00 2001 From: baovitt Date: Sun, 28 Jul 2024 09:28:46 -0400 Subject: [PATCH 2/4] more code --- build.sbt | 3 +- .../src/main/resources/elasticmq.conf | 52 +++++ .../baeldung/elasticmq/AsyncSQSClient.scala | 221 ++++++++++++++++++ .../com/baeldung/elasticmq/DirectClient.scala | 2 +- .../baeldung/elasticmq/ElasticMQConfig.scala | 15 ++ .../baeldung/elasticmq/ElasticMQService.scala | 56 +++++ .../com/baeldung/elasticmq/RestClient.scala | 31 --- 7 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 scala-libraries/src/main/resources/elasticmq.conf create mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/AsyncSQSClient.scala create mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQConfig.scala create mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQService.scala delete mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala diff --git a/build.sbt b/build.sbt index 883ccfd47..511434105 100644 --- a/build.sbt +++ b/build.sbt @@ -442,7 +442,8 @@ lazy val scala_libraries = (project in file("scala-libraries")) libraryDependencies ++= Seq( "org.elasticmq" %% "elasticmq-core" % "1.6.5", "org.elasticmq" %% "elasticmq-server" % "1.6.5", - "org.elasticmq" %% "elasticmq-rest-sqs" % "1.6.5" + "org.elasticmq" %% "elasticmq-rest-sqs" % "1.6.5", + "software.amazon.awssdk" % "sqs" % "2.26.24" ), Defaults.itSettings ) diff --git a/scala-libraries/src/main/resources/elasticmq.conf b/scala-libraries/src/main/resources/elasticmq.conf new file mode 100644 index 000000000..99649fe40 --- /dev/null +++ b/scala-libraries/src/main/resources/elasticmq.conf @@ -0,0 +1,52 @@ +# What is the outside visible address of this ElasticMQ node +# Used to create the queue URL (may be different from bind address!) +node-address { + protocol = http + host = localhost + port = 9324 + context-path = "" +} + +rest-sqs { + enabled = true + bind-port = 9324 + bind-hostname = "0.0.0.0" + // Possible values: relaxed, strict + sqs-limits = strict +} + +rest-stats { + enabled = true + bind-port = 9325 + bind-hostname = "0.0.0.0" +} + +// Should the node-address be generated from the bind port/hostname +// Set this to true e.g. when assigning port automatically by using port 0. +generate-node-address = false + +queues { + queue1 { + defaultVisibilityTimeout = 10 seconds + delay = 0 seconds + receiveMessageWait = 0 seconds + deadLettersQueue { + name = "queue1-dead-letters" + maxReceiveCount = 3 // from 1 to 1000 + } + fifo = false + contentBasedDeduplication = false + tags { + tag1 = "tagged1" + tag2 = "tagged2" + } + } + queue1-dead-letters { } +} + +elastic-mq { + region = "elasticMQ" + endPoint = "http://localhost:9325" + access-key-id = "your aws access key id" + secret-access-key = "secret-access-token" +} \ No newline at end of file diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/AsyncSQSClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/AsyncSQSClient.scala new file mode 100644 index 000000000..80b584cd8 --- /dev/null +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/AsyncSQSClient.scala @@ -0,0 +1,221 @@ +package com.baeldung.elasticmq + +import software.amazon.awssdk.auth.credentials.{ + AwsBasicCredentials, + AwsCredentialsProviderChain, + StaticCredentialsProvider +} +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.sqs.model.* +import software.amazon.awssdk.services.sqs.{ + SqsAsyncClient, + SqsAsyncClientBuilder +} + +import java.net.URI +import java.util.UUID + +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.FutureConverters.* +import scala.jdk.CollectionConverters.* + +class SQSAsyncClient( + queueURL: String, + region: String, + endpoint: String +)(implicit executionContext: ExecutionContext): + + private val sqsAsyncClient: SqsAsyncClient = + SqsAsyncClient + .builder() + .region(Region.of(region)) + .credentialsProvider( + AwsCredentialsProviderChain + .builder() + .credentialsProviders( + StaticCredentialsProvider.create( + AwsBasicCredentials.create( + ElasticMQConfig.ELASTIC_MQ_ACCESS_KEY, + ElasticMQConfig.ELASTIC_MQ_SECRET_ACCESS_KEY + ) + ) + ) + .build() + ) + .endpointOverride(URI.create(endpoint)) + .build() + + def createStandardQueue(queueName: String): Future[CreateQueueResponse] = + val request = CreateQueueRequest.builder.queueName(queueName).build + + sqsAsyncClient.createQueue(request).asScala + + final lazy val createFIFOQueueAttributes = Map( + (QueueAttributeName.FIFO_QUEUE, "true") + ).asJava + + def createFIFOQueue(queueName: String): Future[CreateQueueResponse] = + val createQueueRequest = CreateQueueRequest.builder + .queueName(queueName) + .attributes(createFIFOQueueAttributes) + .build + + sqsAsyncClient.createQueue(createQueueRequest).asScala + + def deleteQueue(): Future[DeleteQueueResponse] = + val request = DeleteQueueRequest.builder().queueUrl(queueURL).build() + + sqsAsyncClient.deleteQueue(request).asScala + + def sendMessage(message: String): Future[SendMessageResponse] = + val request = SendMessageRequest + .builder() + .messageBody(message) + .queueUrl(queueURL) + .build() + + sqsAsyncClient.sendMessage(request).asScala + + def sendMessagesInBatch( + messages: List[String] + ): Future[SendMessageBatchResponse] = + val batchRequestEntry = messages + .map( + SendMessageBatchRequestEntry + .builder() + .messageBody(_) + .id(UUID.randomUUID().toString) + .build() + ) + .asJava + val sendMessageBatchRequest = SendMessageBatchRequest + .builder() + .queueUrl(queueURL) + .entries(batchRequestEntry) + .build() + + sqsAsyncClient.sendMessageBatch(sendMessageBatchRequest).asScala + + // maxNumberOfMessages must be less than 10. + def receiveMessages( + maxNumberOfMessages: Int + ): Future[ReceiveMessageResponse] = + val receiveMessageRequest = + ReceiveMessageRequest + .builder() + .maxNumberOfMessages(maxNumberOfMessages) + .queueUrl(queueURL) + .waitTimeSeconds(10) + .build() + + sqsAsyncClient.receiveMessage(receiveMessageRequest).asScala + + def deleteMessage(receiptHandle: String): Future[DeleteMessageResponse] = + val deleteMessageRequest = DeleteMessageRequest + .builder() + .queueUrl(queueURL) + .receiptHandle(receiptHandle) + .build() + + sqsAsyncClient.deleteMessage(deleteMessageRequest).asScala + + def deleteMessageInBatch( + messages: List[Message] + ): Future[DeleteMessageBatchResponse] = + val listDeleteMessageBatchRequestEntry = messages + .map(message => + DeleteMessageBatchRequestEntry + .builder() + .receiptHandle(message.receiptHandle()) + .build() + ) + .asJava + val deleteMessageBatchRequest = DeleteMessageBatchRequest + .builder() + .queueUrl(queueURL) + .entries(listDeleteMessageBatchRequestEntry) + .build() + + sqsAsyncClient.deleteMessageBatch(deleteMessageBatchRequest).asScala + + def getQueueURL(queueName: String): Future[GetQueueUrlResponse] = + val getQueueUrlRequest = + GetQueueUrlRequest.builder().queueName(queueName).build() + + sqsAsyncClient.getQueueUrl(getQueueUrlRequest).asScala + + def listQueues(): Future[ListQueuesResponse] = + sqsAsyncClient.listQueues().asScala + + def listQueuesStartingFromPrefix(prefix: String): Future[ListQueuesResponse] = + val listQueueStartingFromPrefixRequest = + ListQueuesRequest.builder().queueNamePrefix(prefix).build() + + sqsAsyncClient.listQueues(listQueueStartingFromPrefixRequest).asScala + + def changeMessageVisibility( + message: Message + ): Future[ChangeMessageVisibilityResponse] = + val changeMessageVisibilityRequest = ChangeMessageVisibilityRequest + .builder() + .queueUrl(queueURL) + .receiptHandle(message.receiptHandle()) + .visibilityTimeout(30) + .build() + + sqsAsyncClient + .changeMessageVisibility(changeMessageVisibilityRequest) + .asScala + + def changeMessageVisibilityOfBatch( + messages: List[Message] + ): Future[ChangeMessageVisibilityBatchResponse] = + val changeMessageVisibilityBatchRequestEntry = messages + .map(message => + ChangeMessageVisibilityBatchRequestEntry + .builder() + .receiptHandle(message.receiptHandle()) + .visibilityTimeout(30) + .build() + ) + .asJava + val changeMessageVisibilityRequest = ChangeMessageVisibilityBatchRequest + .builder() + .queueUrl(queueURL) + .entries(changeMessageVisibilityBatchRequestEntry) + .build() + + sqsAsyncClient + .changeMessageVisibilityBatch(changeMessageVisibilityRequest) + .asScala + + final lazy val purgeQueueRequest = + PurgeQueueRequest.builder().queueUrl(queueURL).build() + def purgeQueue(): Future[PurgeQueueResponse] = + sqsAsyncClient.purgeQueue(purgeQueueRequest).asScala + + def setQueueAttributes( + attributes: Map[QueueAttributeName, String] + ): Future[SetQueueAttributesResponse] = + val setQueueAttributesRequest = SetQueueAttributesRequest + .builder() + .queueUrl(queueURL) + .attributes(attributes.asJava) + .build() + + sqsAsyncClient.setQueueAttributes(setQueueAttributesRequest).asScala + + def tagQueue(tags: Map[String, String]): Future[TagQueueResponse] = + val tagQueueRequest = + TagQueueRequest.builder().queueUrl(queueURL).tags(tags.asJava).build() + + sqsAsyncClient.tagQueue(tagQueueRequest).asScala + + def untagQueue(listOfTagsToRemove: List[String]): Future[UntagQueueResponse] = + val untagQueueRequest = UntagQueueRequest + .builder() + .queueUrl(queueURL) + .tagKeys(listOfTagsToRemove.asJava) + .build() + + sqsAsyncClient.untagQueue(untagQueueRequest).asScala diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala index 88eeb19fa..226139b9f 100644 --- a/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala @@ -2,4 +2,4 @@ package com.baeldung.elasticmq // val config = ConfigFactory.load("elasticmq.conf") // val server = new ElasticMQServer(new ElasticMQServerConfig(config)) -// server.start() \ No newline at end of file +// server.start() diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQConfig.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQConfig.scala new file mode 100644 index 000000000..58ac2f5e4 --- /dev/null +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQConfig.scala @@ -0,0 +1,15 @@ +package com.baeldung.elasticmq + +import com.typesafe.config.{Config, ConfigFactory} + +object ElasticMQConfig: + + private final val config: Config = ConfigFactory.load("elasticmq.conf") + + final val ELASTIC_MQ_ACCESS_KEY: String = + config.getString("elastic-mq.access-key-id") + final val ELASTIC_MQ_SECRET_ACCESS_KEY: String = + config.getString("elastic-mq.secret-access-key") + + final val ELASTIC_MQ_REGION = config.getString("elastic-mq.region") + final val ELASTIC_MQ_ENDPOINT = config.getString("elastic-mq.endPoint") diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQService.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQService.scala new file mode 100644 index 000000000..64b99420e --- /dev/null +++ b/scala-libraries/src/main/scala/com/baeldung/elasticmq/ElasticMQService.scala @@ -0,0 +1,56 @@ +package com.baeldung.elasticmq + +import org.elasticmq.rest.sqs.SQSRestServerBuilder +import org.elasticmq.server.ElasticMQServer +import org.elasticmq.server.config.ElasticMQServerConfig + +import com.typesafe.config.ConfigFactory + +import org.apache.pekko.actor.{Actor, ActorRef, ActorSystem} +import org.apache.pekko.event.LoggingAdapter + +import scala.util.{Failure, Success} + +object ElasticMQService extends App: + + implicit val actorSystem: ActorSystem = ActorSystem.create() + implicit val executionContext: concurrent.ExecutionContextExecutor = + actorSystem.dispatcher + implicit val m_logger: LoggingAdapter = actorSystem.log + + final val ElasticMQ_URL = s"http://localhost:9324/000000000000/" + + val endpoint = "http://localhost:9325" + val region = "elasticmq" + + val server = SQSRestServerBuilder + .withPort(9325) + .withInterface("localhost") + .start() + + val elasticMQClient = new SQSAsyncClient(ElasticMQ_URL, region, endpoint) + + val uselessWorkflow = + for + _ <- elasticMQClient.createStandardQueue("standardQueueForTest") + testQueueClient = new SQSAsyncClient( + ElasticMQ_URL + "standardQueueForTest", + region, + endpoint + ) + _ <- testQueueClient.createFIFOQueue("fifoQueue.fifo") + _ <- testQueueClient.listQueues() + _ <- testQueueClient.sendMessage("Hi") + _ <- testQueueClient.sendMessagesInBatch( + List("Follow", "Baeldung", "on", "LinkedIn") + ) + _ <- testQueueClient.receiveMessages(5) + _ <- testQueueClient.purgeQueue() + yield () + + uselessWorkflow + .andThen(_ => server.stopAndWait()) + .onComplete: + case Success(_) => m_logger.info("queue created") + case Failure(exception) => + m_logger.error(exception, "exception in uselessWorkflow") diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala deleted file mode 100644 index 308879206..000000000 --- a/scala-libraries/src/main/scala/com/baeldung/elasticmq/RestClient.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.baeldung.elasticmq - -import org.elasticmq.rest.sqs.SQSRestServerBuilder -import org.elasticmq.server.ElasticMQServer -import org.elasticmq.server.config.ElasticMQServerConfig - -import com.typesafe.config.ConfigFactory - -import org.apache.pekko.actor.{Actor, ActorRef, ActorSystem} -import org.apache.pekko.event.LoggingAdapter - -implicit val actorSystem: ActorSystem = ActorSystem.create() -implicit val executionContext: concurrent.ExecutionContextExecutor = actorSystem.dispatcher -implicit val m_logger: LoggingAdapter = actorSystem.log - -// class QueueActor extends Actor: -// val messageQueue: mutable.PriorityQueue[InternalMessage]() -// val awaiting: mutable.PriorityQueue[ActorRef]() - -// def receive = case ReceiveMessages => - - -// val result: Future[ActorRef] = flow: -// (queueManager ? Lookup(name)).apply() match -// case Some(queueActor) => queueActor -// case None => (queueManager ? Create(name)).apply() - -lazy val server = SQSRestServerBuilder - .withPort(9325) - .withInterface("localhost") - .start() \ No newline at end of file From 5bc06e3eeba25ffa364fea0e360dc0d8baf05d5d Mon Sep 17 00:00:00 2001 From: baovitt Date: Mon, 29 Jul 2024 12:31:51 -0400 Subject: [PATCH 3/4] elasticmq --- scala-core-modules/scala-core-9/README.md | 2 +- scala-libraries/README.md | 3 +-- .../src/main/scala/com/baeldung/elasticmq/DirectClient.scala | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala diff --git a/scala-core-modules/scala-core-9/README.md b/scala-core-modules/scala-core-9/README.md index 8b472c65a..9e6cdf898 100644 --- a/scala-core-modules/scala-core-9/README.md +++ b/scala-core-modules/scala-core-9/README.md @@ -6,4 +6,4 @@ - [Find Two’s Complement of a Number in Scala](https://www.baeldung.com/scala/compute-twos-complement) - [Convert Option to Either in Scala](https://www.baeldung.com/scala/option-either-conversion) - [Meaning of _root_ In Scala Import Clause](https://www.baeldung.com/scala/root-import-clause) -- [Check if All Characters in a Scala String Are Either Upper or Lower Case](https://www.baeldung.com/scala/string-check-all-characters-upper-lower-case) +- [Check if All Characters in a Scala String Are Either Upper or Lower Case](https://www.baeldung.com/scala/string-check-all-characters-upper-lower-case diff --git a/scala-libraries/README.md b/scala-libraries/README.md index d74729b54..40012dfc6 100644 --- a/scala-libraries/README.md +++ b/scala-libraries/README.md @@ -7,5 +7,4 @@ - [A Guide to the Scala Retry Library](https://www.baeldung.com/scala/retry-library) - [Introduction to Apache Log4j in Scala](https://www.baeldung.com/scala/apache-log4j) - [Logging in Scala Applications Using Scala-Logging](https://www.baeldung.com/scala/scala-logging) -- [AWScala – AWS SDK for Scala](https://www.baeldung.com/scala/awscala-aws-sdk-for-scala) -- [Introduction to ElasticMQ] https://baeldung.com/scala/introduction-to-elasticmq \ No newline at end of file +- [AWScala – AWS SDK for Scala](https://www.baeldung.com/scala/awscala-aws-sdk-for-scala) \ No newline at end of file diff --git a/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala b/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala deleted file mode 100644 index 226139b9f..000000000 --- a/scala-libraries/src/main/scala/com/baeldung/elasticmq/DirectClient.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.baeldung.elasticmq - -// val config = ConfigFactory.load("elasticmq.conf") -// val server = new ElasticMQServer(new ElasticMQServerConfig(config)) -// server.start() From cce365792cbbd7dea37ad2348ff46daeac927e2b Mon Sep 17 00:00:00 2001 From: baovitt Date: Mon, 29 Jul 2024 17:10:23 -0400 Subject: [PATCH 4/4] README fix --- scala-core-modules/scala-core-9/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scala-core-modules/scala-core-9/README.md b/scala-core-modules/scala-core-9/README.md index 9e6cdf898..8b472c65a 100644 --- a/scala-core-modules/scala-core-9/README.md +++ b/scala-core-modules/scala-core-9/README.md @@ -6,4 +6,4 @@ - [Find Two’s Complement of a Number in Scala](https://www.baeldung.com/scala/compute-twos-complement) - [Convert Option to Either in Scala](https://www.baeldung.com/scala/option-either-conversion) - [Meaning of _root_ In Scala Import Clause](https://www.baeldung.com/scala/root-import-clause) -- [Check if All Characters in a Scala String Are Either Upper or Lower Case](https://www.baeldung.com/scala/string-check-all-characters-upper-lower-case +- [Check if All Characters in a Scala String Are Either Upper or Lower Case](https://www.baeldung.com/scala/string-check-all-characters-upper-lower-case)