Using Java Libraries in Scala

java libraries

Scala does not transpile to Java as TypeScript does to JavaScript.  It is a distinct language that compiles directly to JVM bytecode just as Java does.  Because both compile to the same bytecode, however, Java libraries can be accessed natively from Scala.  This is similar to the sharing of libraries by languages compiled to the same machine code, but is more seamless because the JVM is a higher level abstraction.

For simple cases, Java libraries are imported and used in Scala exactly as they are in Java.  For example, java.util.UUID would be used in Scala as follows.

import java.util.UUID
val id = UUID.randomUUID()

Explicit type declarations as well as the parentheses on zero-argument functions can omitted as appropriate, just as with Scala libraries.  By convention, parentheses are included for functions such as randomUUID which are not pure functions.  If you were to convert a UUID to a string, however, id.toString would normally be used rather than id.toString().

While any Java library can be used in Scala exactly as it would be in Java, for libraries involving futures or collections, we really want to break free of the Java APIs and be able to use Scala futures and collections.  For this, Scala includes standard libraries that provide the necessary conversions.

For concrete examples of these conversions, we will develop a Scala wrapper around the asynchronous Java library for S3.

Converting Java Futures

First we need an S3 client that supports asynchronous operations.  Below shows variations of this for Linode Object Storage and AWS S3.

import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider}
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3AsyncClient
val basicCredentials = AwsBasicCredentials.create(
    "some-access-key",
    "some-secret-key"
)

// Linode
val region = Region.of("us-east")
val endpoint = new URI(s"https://${region.toString}-1.linodeobjects.com")

// AWS
val region = Region.of("us-east-1")
val endpoint  = new URI(s"https://s3.${region.toString}.amazonaws.com")
val client = S3AsyncClient.builder().
    endpointOverride(endpoint).
    credentialsProvider(StaticCredentialsProvider.create(basicCredentials)).
    region(region).
    build()

If we now call client.listBuckets, it will return a value of type java.util.concurrent.CompletableFuture<ListBucketsResponse>.

import java.util.concurrent.CompletableFuture
import software.amazon.awssdk.services.s3.model.ListBucketsResponse
def listBuckets() : CompletableFuture[ListBucketsResponse] = {
    client.listBuckets()
}

This will work as-is in Scala, but will be awkward to use.  To make this work like a native Scala library, we need to convert the CompletableFuture to a Scala Future.  The Scala FutureConverters object provides implicits for converting from Java futures to Scala.  This is part of the standard Scala library and no additional dependency is required.

import scala.concurrent.Future
import scala.jdk.FutureConverters._
import software.amazon.awssdk.services.s3.model.ListBucketsResponse
def listBuckets() : Future[ListBucketsResponse] = {
    client.listBuckets().asScala
}

That is much better, but the list of buckets is wrapped in a function ListBucketsResponse.buckets() that returns a mutable Java collection of type java.util.List<Bucket>.  A more convenient return type would be an immutable Scala collection.

Converting Java Collections

The process for converting collections is very similar.  We start by importing the conversions provided by the Scala CollectionConverters object.  With this imported, we can append asScala to Java collections just as we do with Java futures.  In our listBuckets example, we first have to extract the Java collection from the ListBucketsResponse, in the normal way since the response is already a Scala future, then take the java.util.List returned by the buckets function and call asScala to convert it.  For more flexibility in selecting a target collection, the result of asScala is actually a Scala mutable Buffer.  As an intermediate value, Buffer provides methods for converting to Seq, List, etc.  Here we use toSeq.

import scala.concurrent.Future
import scala.jdk.CollectionConverters._
import scala.jdk.FutureConverters._
import software.amazon.awssdk.services.s3.model.ListBucketsResponse
def listBuckets() : Future[Seq[Bucket]] = {
    val listBucketsResponse: Future[ListBucketsResponse] = client.listBuckets().asScala
    listBucketsResponse.map(_.buckets.asScala.toSeq)
}

We now have a Scala wrapper around the listing of S3 buckets that is completely native.  Bundle this with the S3 authentication from above, and we have the foundation for a Scala wrapper around the S3 Java SDK.

From Scala To Java

In some cases a Java API will require a future or collection as input.  For this, both FutureConverters and CollectionConverters include an asJava method that does the reverse of asScala.

Mocking For Tests

When working with services such as S3, mocking is of course necessary in writing tests.  Given an implementation of a Scala class named ObjectStorageClient, the following ScalaTest test would mock the S3AsyncClient using ScalaMock (line 3), and then construct expected requests and responses in the normal way, except for using Java types.

"An ObjectStorageClient" must {
    "list buckets" in {
        val mockS3         = mock[S3AsyncClient]
        val bucket         = Bucket.builder.name("test-bucket").build()
        val request        = ListBucketsRequest.builder.build()
        val response       = ListBucketsResponse.builder.buckets(bucket).build()
        val expectedResult = response.buckets.asScala.toSeq

        (mockS3.listBuckets(_:ListBucketsRequest)).
            expects(request).
            returning(CompletableFuture.completedFuture(response))

        val losc = new ObjectStorageClient(mockConfiguration) {
            override lazy val client = mockS3
        }

        val futureResult = losc.listBuckets()
        futureResult map { result => result shouldBe expectedResult }
    }
}

Conclusion

A full implementation of an S3 client for Scala built as described above, including listing buckets, object upload, and object download, is available at https://gitlab.com/lfrost/play-cnz/.