diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ae82c9..4fce245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Set up JDK 8 with SBT cache + - name: Set up JDK 11 with SBT cache uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '8' + java-version: '11' cache: 'sbt' - name: Install SBT and jq diff --git a/cloudinary-core/build.sbt b/cloudinary-core/build.sbt index 02d7bc6..f3e2e14 100644 --- a/cloudinary-core/build.sbt +++ b/cloudinary-core/build.sbt @@ -35,7 +35,7 @@ pomExtra := { } libraryDependencies ++= Seq( - "com.ning" % "async-http-client" % "1.9.40", + "org.asynchttpclient" % "async-http-client" % "3.0.3", "org.json4s" %% "json4s-native" % "3.6.10", "org.json4s" %% "json4s-ext" % "3.6.10", "org.scalatest" %% "scalatest" % "3.2.2" % "test", @@ -48,4 +48,3 @@ libraryDependencies += "org.scalamock" %% "scalamock-scalatest-support" % "3.4.1 resolvers ++= Seq("sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", "sonatype releases" at "https://oss.sonatype.org/content/repositories/releases") scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") - diff --git a/cloudinary-core/src/main/scala/com/cloudinary/Api.scala b/cloudinary-core/src/main/scala/com/cloudinary/Api.scala index a8e7ec2..45611fa 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/Api.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/Api.scala @@ -3,8 +3,8 @@ package com.cloudinary import java.util.Date import java.util.TimeZone import scala.concurrent.Future -import com.ning.http.client.Realm -import com.ning.http.client.RequestBuilder +import org.asynchttpclient.Realm +import org.asynchttpclient.RequestBuilder import response._ import parameters.UpdateParameters import java.text.SimpleDateFormat @@ -55,9 +55,7 @@ class Api(implicit cloudinary: Cloudinary) { } } - val realm = new Realm.RealmBuilder() - .setPrincipal(cloudinary.apiKey()) - .setPassword(cloudinary.apiSecret()) + val realm = new Realm.Builder(cloudinary.apiKey(), cloudinary.apiSecret()) .setUsePreemptiveAuth(true) .setScheme(Realm.AuthScheme.BASIC) .build() diff --git a/cloudinary-core/src/main/scala/com/cloudinary/AsyncCloudinaryHandler.scala b/cloudinary-core/src/main/scala/com/cloudinary/AsyncCloudinaryHandler.scala index 8807b6a..d2fd115 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/AsyncCloudinaryHandler.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/AsyncCloudinaryHandler.scala @@ -2,7 +2,7 @@ package com.cloudinary import scala.concurrent.Promise -import _root_.com.ning.http.client.{ +import _root_.org.asynchttpclient.{ AsyncHandler, AsyncCompletionHandler, HttpResponseStatus, @@ -68,4 +68,4 @@ class AsyncCloudinaryHandler[JValue](result: Promise[JsonAST.JValue]) extends As case Left(e) => result.failure(e) } } -} \ No newline at end of file +} diff --git a/cloudinary-core/src/main/scala/com/cloudinary/Cloudinary.scala b/cloudinary-core/src/main/scala/com/cloudinary/Cloudinary.scala index 2d16e79..c9803cd 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/Cloudinary.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/Cloudinary.scala @@ -6,7 +6,7 @@ import java.security.NoSuchAlgorithmException import java.net.URI import java.net.URLDecoder import java.io.UnsupportedEncodingException -import _root_.com.ning.http.client.RequestBuilder +import _root_.org.asynchttpclient.RequestBuilder object Cloudinary { final val CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; @@ -60,7 +60,7 @@ object Cloudinary { val digest = sign(params.mkString("&"), apiSecret) bytes2Hex(digest).toLowerCase() } - + def sign(toSign:String, apiSecret: String) = { var md: MessageDigest = null try { diff --git a/cloudinary-core/src/main/scala/com/cloudinary/HttpClient.scala b/cloudinary-core/src/main/scala/com/cloudinary/HttpClient.scala index 9a88ee3..7e9c1cd 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/HttpClient.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/HttpClient.scala @@ -1,14 +1,14 @@ package com.cloudinary import java.util.concurrent.atomic.AtomicReference +import java.time.Duration -import com.ning.http.client.AsyncHttpClientConfig -import com.ning.http.client.AsyncHttpClient +import org.asynchttpclient.{AsyncHttpClient, DefaultAsyncHttpClient, DefaultAsyncHttpClientConfig} import org.json4s._ import org.json4s.native.JsonMethods._ import scala.concurrent.{Future, Promise} -import com.ning.http.client.Request +import org.asynchttpclient.Request import concurrent.ExecutionContext.Implicits.global import com.cloudinary.response.RawResponse @@ -53,10 +53,11 @@ object HttpClient { private[cloudinary] def newClient(): AsyncHttpClient = { - val asyncHttpConfig = new AsyncHttpClientConfig.Builder() - asyncHttpConfig.setUserAgent(Cloudinary.USER_AGENT) - asyncHttpConfig.setReadTimeout(-1) - new AsyncHttpClient(asyncHttpConfig.build()) + val asyncHttpConfig = new DefaultAsyncHttpClientConfig.Builder() + .setUserAgent(Cloudinary.USER_AGENT) + .setReadTimeout(Duration.ofMillis(-1)) + .build() + new DefaultAsyncHttpClient(asyncHttpConfig) } /** diff --git a/cloudinary-core/src/main/scala/com/cloudinary/Uploader.scala b/cloudinary-core/src/main/scala/com/cloudinary/Uploader.scala index 7bc8060..1ecda41 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/Uploader.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/Uploader.scala @@ -2,11 +2,12 @@ package com.cloudinary import java.io._ import java.nio.charset.StandardCharsets +import java.time.Duration import com.cloudinary.parameters._ import com.cloudinary.response._ -import com.ning.http.client.RequestBuilder -import com.ning.http.client.multipart.{ByteArrayPart, FilePart, StringPart} +import org.asynchttpclient.RequestBuilder +import org.asynchttpclient.request.body.multipart.{ByteArrayPart, FilePart, StringPart} import org.json4s.JsonDSL._ import org.json4s.native.JsonMethods._ @@ -31,7 +32,7 @@ class Uploader(implicit val cloudinary: Cloudinary) { val apiUrlBuilder = new RequestBuilder("POST") apiUrlBuilder.setUrl(apiUrl) - requestTimeout.map(timeout => apiUrlBuilder.setRequestTimeout(timeout)) //in milliseconds + requestTimeout.map(timeout => apiUrlBuilder.setRequestTimeout(Duration.ofMillis(timeout))) //in milliseconds processedParams foreach { param => @@ -40,10 +41,10 @@ class Uploader(implicit val cloudinary: Cloudinary) { case list: Iterable[_] => list.foreach { v => apiUrlBuilder.addBodyPart( - new StringPart(k + "[]", v.toString, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8)) + new StringPart(k + "[]", v.toString)) } case null => - case _ => apiUrlBuilder.addBodyPart(new StringPart(k, v.toString, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8)) + case _ => apiUrlBuilder.addBodyPart(new StringPart(k, v.toString)) } } @@ -52,7 +53,7 @@ class Uploader(implicit val cloudinary: Cloudinary) { case fp: FilePart => apiUrlBuilder.addBodyPart(fp) case f: File => apiUrlBuilder.addBodyPart(new FilePart("file", f)) case fn: String if !fn.matches(illegalFileName) => apiUrlBuilder.addBodyPart(new FilePart("file", new File(fn))) - case body: String => apiUrlBuilder.addBodyPart(new StringPart("file", body, StringPart.DEFAULT_CONTENT_TYPE, StandardCharsets.UTF_8)) + case body: String => apiUrlBuilder.addBodyPart(new StringPart("file", body)) case body: Array[Byte] => apiUrlBuilder.addBodyPart(new ByteArrayPart("file", body)) case null => case _ => throw new IOException("Unrecognized file parameter " + file); diff --git a/cloudinary-core/src/main/scala/com/cloudinary/Url.scala b/cloudinary-core/src/main/scala/com/cloudinary/Url.scala index 152a057..900ee62 100644 --- a/cloudinary-core/src/main/scala/com/cloudinary/Url.scala +++ b/cloudinary-core/src/main/scala/com/cloudinary/Url.scala @@ -1,7 +1,7 @@ package com.cloudinary import java.net.URLDecoder -import com.ning.http.util.Base64 +import java.util.Base64 case class Url( cloudName: String, @@ -154,7 +154,7 @@ case class Url( val signature = if (signUrl) { val toSign = List(transformationStr, Some(signableSource)).flatten.mkString("/") Some("s--" + - Base64.encode(Cloudinary.sign(toSign, apiSecret.getOrElse(throw new Exception("Must supply api secret to sign URLs")))). + Base64.getEncoder.encodeToString(Cloudinary.sign(toSign, apiSecret.getOrElse(throw new Exception("Must supply api secret to sign URLs")))). take(8). replace('+', '-').replace('/', '_') + "--") } else None diff --git a/cloudinary-core/src/test/scala/com/cloudinary/ApiSpec.scala b/cloudinary-core/src/test/scala/com/cloudinary/ApiSpec.scala index ef5701e..12f7e54 100644 --- a/cloudinary-core/src/test/scala/com/cloudinary/ApiSpec.scala +++ b/cloudinary-core/src/test/scala/com/cloudinary/ApiSpec.scala @@ -8,7 +8,7 @@ import java.util.{Date, TimeZone} import com.cloudinary.Implicits._ import com.cloudinary.parameters._ import com.cloudinary.response._ -import com.ning.http.client._ +import org.asynchttpclient._ import org.scalamock.scalatest.MockFactory import org.scalatest._ import matchers.should._ @@ -85,11 +85,10 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi it should "allow listing resources with cursor" in { val cursor = "OJNASGONQG0230JGV0JV3Q0IDVO" val (provider, api) = mockApi() - (provider.execute _) expects where { (request: Request, _) => { + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => val params = getQuery(request) params.contains(("next_cursor", cursor)) } - } api.resources(maxResults = 1, nextCursor = cursor) } @@ -139,12 +138,11 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi df.setTimeZone(TimeZone.getTimeZone("UTC")) val (provider, api) = mockApi() val startAt = df.parse("22 Aug 2016 07:57:34 UTC") - (provider.execute _) expects where { (request: Request, *) => { + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => val params = getQuery(request) params.contains(("start_at", "22 Aug 2016 07:57:34 UTC GMT")) && params.contains(("direction", "asc")) } - } api.resources(`type` = "upload", startAt = Some(startAt), direction = Some(Api.ASCENDING)) } @@ -244,9 +242,8 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi it should "allow listing transformations with next_cursor" in { val (provider, api) = mockApi() - (provider.execute _) expects where { (request: Request, *) => { - request.getQueryParams.contains(new Param("next_cursor", "1234567")) - } + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => + request.getQueryParams.contains(new Param("next_cursor", "1234567")) } api.transformations(nextCursor = "1234567") } @@ -282,24 +279,21 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi val t = Transformation().c_("scale").w_(100) val (provider, api) = mockApi() inSequence { - (provider.execute _) expects where { - (request: Request, *) => { + (provider.executeRequest _) expects where { + (request: Request, _: AsyncHandler[_]) => request.getQueryParams.contains(new Param("next_cursor", "1234567")) && request.getUrl.matches(".+/" + apiTestTransformation + "?.+") - } } - (provider.execute _) expects where { - (request: Request, *) => { + (provider.executeRequest _) expects where { + (request: Request, _: AsyncHandler[_]) => request.getQueryParams.contains(new Param("next_cursor", "1234567")) && request.getUrl.matches(".+/" + apiTestTransformation + "?.+") - } } - (provider.execute _) expects where { - (request: Request, *) => { + (provider.executeRequest _) expects where { + (request: Request, _: AsyncHandler[_]) => request.getQueryParams.contains(new Param("max_results", "111")) && !request.getQueryParams.asScala.exists(p => p.getName == "next_cursor") && request.getUrl.matches(".+/" + apiTestTransformation + "?.+") - } } } api.transformationByName(apiTestTransformation, "1234567") @@ -309,10 +303,9 @@ class ApiSpec extends MockableFlatSpec with Matchers with OptionValues with Insi it should "allow listing transformation by name with next_cursor" in { val (provider, api) = mockApi() - provider.execute _ expects where { - (request: Request, handler: AsyncHandler[Nothing]) => { + (provider.executeRequest _) expects where { + (request: Request, _: AsyncHandler[_]) => request.getQueryParams.contains(new Param("next_cursor", "1234567")) - } } api.transformationByName(apiTestTransformation, nextCursor = "1234567") } diff --git a/cloudinary-core/src/test/scala/com/cloudinary/MockableFlatSpec.scala b/cloudinary-core/src/test/scala/com/cloudinary/MockableFlatSpec.scala index 856e1d4..7429f60 100644 --- a/cloudinary-core/src/test/scala/com/cloudinary/MockableFlatSpec.scala +++ b/cloudinary-core/src/test/scala/com/cloudinary/MockableFlatSpec.scala @@ -2,8 +2,8 @@ package com.cloudinary import java.net.URLDecoder -import com.ning.http.client.multipart.StringPart -import com.ning.http.client.{AsyncHttpClient, AsyncHttpClientConfig, AsyncHttpProvider, Request} +import org.asynchttpclient.request.body.multipart.StringPart +import org.asynchttpclient.{AsyncHttpClient, DefaultAsyncHttpClientConfig, Request, AsyncHandler, ListenableFuture} import org.scalamock.clazz.Mock import org.scalamock.scalatest.MockFactory import org.scalatest.flatspec.AnyFlatSpec @@ -11,6 +11,10 @@ import org.scalatest.BeforeAndAfterEach import scala.collection.JavaConverters._ +trait MockableAsyncHttpClient extends AsyncHttpClient { + override def executeRequest[T](request: Request, handler: AsyncHandler[T]): ListenableFuture[T] +} + class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterEach{ protected val prefix = "cloudinary_scala" protected val suffix = sys.env.getOrElse("TRAVIS_JOB_ID", (10000 + scala.util.Random.nextInt(89999)).toString) @@ -26,36 +30,34 @@ class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterE } /** - * Mock the AsyncHttpProvider so that calls do not invoke the server side. - * Expectations can be set on the execute method of AsyncHttpProvider. + * Mock the AsyncHttpClient so that calls do not invoke the server side. + * Expectations can be set on the executeRequest method of AsyncHttpClient. * @return the mocked instance */ def mockHttp() = { - val mockProvider: AsyncHttpProvider = mock[AsyncHttpProvider] - val asyncHttpConfig = new AsyncHttpClientConfig.Builder() - asyncHttpConfig.setUserAgent(Cloudinary.USER_AGENT) - (mockProvider, new AsyncHttpClient(mockProvider, asyncHttpConfig.build())) + val mockClient: MockableAsyncHttpClient = mock[MockableAsyncHttpClient] + (mockClient, mockClient) } /** - * Returns an instance of [[com.cloudinary.Api Api]] with a mocked [[com.ning.http.client.AsyncHttpProvider AsyncHttpProvider]] + * Returns an instance of [[com.cloudinary.Api Api]] with a mocked [[org.asynchttpclient.AsyncHttpClient AsyncHttpClient]] */ def mockApi() = { val api = cloudinary.api() - val (mockProvider, client) = mockHttp() + val (mockClient, client) = mockHttp() api.httpclient.client = client - (mockProvider, api) + (mockClient, api) } /** - * Returns an instance of [[com.cloudinary.Uploader Uploader]] with a mocked [[com.ning.http.client.AsyncHttpProvider AsyncHttpProvider]] + * Returns an instance of [[com.cloudinary.Uploader Uploader]] with a mocked [[org.asynchttpclient.AsyncHttpClient AsyncHttpClient]] */ def mockUploader() = { val uploader = cloudinary.uploader() - val (mockProvider, client) = mockHttp() + val (mockClient, client) = mockHttp() uploader.httpclient.client = client - (mockProvider, uploader) + (mockClient, uploader) } @@ -65,7 +67,7 @@ class MockableFlatSpec extends AnyFlatSpec with MockFactory with BeforeAndAfterE * @return an array of tuples in the form of (name, value) */ def getParts(request: Request): scala.collection.mutable.Buffer[(String, String)] = { - request.getParts.asScala.map(p => { + request.getBodyParts.asScala.map(p => { val sp = p.asInstanceOf[StringPart] (sp.getName, sp.getValue) }) diff --git a/cloudinary-core/src/test/scala/com/cloudinary/UploaderSpec.scala b/cloudinary-core/src/test/scala/com/cloudinary/UploaderSpec.scala index 2d40119..7d077b4 100644 --- a/cloudinary-core/src/test/scala/com/cloudinary/UploaderSpec.scala +++ b/cloudinary-core/src/test/scala/com/cloudinary/UploaderSpec.scala @@ -5,8 +5,8 @@ import java.util.concurrent.TimeoutException import com.cloudinary.Implicits._ import com.cloudinary.parameters._ import com.cloudinary.response._ -import com.ning.http.client.Request -import com.ning.http.client.multipart.StringPart +import org.asynchttpclient.{Request, AsyncHandler, ListenableFuture} +import org.asynchttpclient.request.body.multipart.StringPart import org.scalatest.{BeforeAndAfterAll, Inside, OptionValues, Tag, matchers} import matchers.should._ @@ -204,25 +204,20 @@ class UploaderSpec extends MockableFlatSpec with Matchers with OptionValues with it should "support generating sprites" in { val sprite_test_tag: String = "sprite_test_tag" + suffix - val (provider, uploader )= mockUploader() - val tagPart: StringPart = new StringPart("tag", sprite_test_tag, "UTF-8") - (provider.execute _) expects where { (request: Request, *) => { - val map = getParts(request) - map.contains(("tag", sprite_test_tag)) - } + val (provider, uploader) = mockUploader() + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => + getParts(request).contains(("tag", sprite_test_tag)) } - (provider.execute _) expects where { (request: Request, *) => { + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => val map = getParts(request) map.contains(("tag", sprite_test_tag)) && map.contains(("transformation", "w_100")) } - } - (provider.execute _) expects where { (request: Request, *) => { + (provider.executeRequest _) expects where { (request: Request, _: AsyncHandler[_]) => val map = getParts(request) map.contains(("tag", sprite_test_tag)) && map.contains(("transformation", "f_jpg,w_100")) } - } uploader.generateSprite(sprite_test_tag) uploader.generateSprite(sprite_test_tag, transformation = new Transformation().w_(100)) uploader.generateSprite(sprite_test_tag, transformation = new Transformation().w_(100), format = "jpg")