diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e05ee6..34d56704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Changelog +### [:link: 2.2.0](https://github.com/KarelCemus/play-redis/tree/2.2.0) + +Support of plain arrays in JavaRedis [#176](https://github.com/KarelCemus/play-redis/pull/176). + ### [:link: 2.1.2](https://github.com/KarelCemus/play-redis/tree/2.1.2) JDK 10 compatibility. Replace deprecated com.sun.misc.BASE64* usages with jdk8 java.util.Base64 [#170](https://github.com/KarelCemus/play-redis/pull/170). diff --git a/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala b/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala index 56411f26..bc4cde2e 100644 --- a/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala +++ b/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala @@ -33,7 +33,7 @@ private[ impl ] class JavaRedis( internal: CacheAsyncApi, environment: Environme // set the value internal.set( key, value, duration ), // and set its type to be able to read it - internal.set( s"classTag::$key", if ( value == null ) "" else value.getClass.getCanonicalName, duration ) + internal.set( s"classTag::$key", classTagOf( value ), duration ) ) ).map { case Seq( done, _ ) => done @@ -57,8 +57,7 @@ private[ impl ] class JavaRedis( internal: CacheAsyncApi, environment: Environme implicit val context = HttpExecutionContext.fromThread( runtime.context ) // get the tag and decode it def getClassTag = internal.get[ String ]( s"classTag::$key" ) - def decodeClassTag( name: String ): ClassTag[ T ] = if ( name == "" ) ClassTag.Null.asInstanceOf[ ClassTag[ T ] ] else ClassTag( environment.classLoader.loadClass( name ) ) - def decodedClassTag( tag: Option[ String ] ) = tag.map( decodeClassTag ) + def decodedClassTag( tag: Option[ String ] ) = tag.map( classTagFrom[ T ] ) // if tag is defined, get Option[ value ] otherwise None def getValue = getClassTag.map( decodedClassTag ).flatMap { case Some( ClassTag.Null ) => Future.successful( Some( null.asInstanceOf[ T ] ) ) @@ -81,6 +80,15 @@ private[ impl ] class JavaRedis( internal: CacheAsyncApi, environment: Environme } def removeAll( ) = internal.invalidate().toJava + + protected def classTagOf( value: Any ): String = { + if ( value == null ) "" else value.getClass.getCanonicalName + } + + protected def classTagFrom[ T ]( tag: String ): ClassTag[ T ] = { + if ( tag == "" ) ClassTag.Null.asInstanceOf[ ClassTag[ T ] ] + else ClassTag( classTagNameToClass( tag, environment ) ) + } } private[ impl ] object JavaRedis { @@ -93,4 +101,19 @@ private[ impl ] object JavaRedis { private[ impl ] implicit class ScalaCompatibility[ T ]( val future: CompletionStage[ T ] ) extends AnyVal { @inline def toScala: Future[ T ] = FutureConverters.toScala( future ) } + + // $COVERAGE-OFF$ + /** java primitives are serialized into their type names instead of classes */ + private[ impl ] def classTagNameToClass( name: String, environment: Environment ): Class[ _ ] = name match { + case "boolean[]" => classOf[ Array[ java.lang.Boolean ] ] + case "byte[]" => classOf[ Array[ java.lang.Byte ] ] + case "char[]" => classOf[ Array[ java.lang.Character ] ] + case "short[]" => classOf[ Array[ java.lang.Short ] ] + case "int[]" => classOf[ Array[ java.lang.Integer ] ] + case "long[]" => classOf[ Array[ java.lang.Long ] ] + case "float[]" => classOf[ Array[ java.lang.Float ] ] + case "double[]" => classOf[ Array[ java.lang.Double ] ] + case clazz => environment.classLoader.loadClass( clazz ) + } + // $COVERAGE-ON$ } diff --git a/src/test/java/play/api/cache/redis/JavaTypes.java b/src/test/java/play/api/cache/redis/JavaTypes.java new file mode 100644 index 00000000..b284fcb4 --- /dev/null +++ b/src/test/java/play/api/cache/redis/JavaTypes.java @@ -0,0 +1,10 @@ +package play.api.cache.redis; + +/** + * @author Karel Cemus + */ +public class JavaTypes { + + public static final byte byteValue = 5; + public static final byte[] bytesValue = new byte[]{1, 2, 3}; +} diff --git a/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala b/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala index 2ce1dd4d..65f9bb1d 100644 --- a/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala +++ b/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala @@ -5,6 +5,7 @@ import java.util.Date import scala.reflect.ClassTag import play.api.inject.guice.GuiceApplicationBuilder +import play.api.cache.redis._ import org.joda.time.{DateTime, DateTimeZone} import org.specs2.mock.Mockito @@ -24,6 +25,11 @@ class SerializerSpecs extends Specification with Mockito { "byte" in { 0xAB.toByte.encoded mustEqual "-85" + JavaTypes.byteValue.encoded mustEqual "5" + } + + "byte[]" in { + JavaTypes.bytesValue.encoded mustEqual "AQID" } "char" in { @@ -93,7 +99,6 @@ class SerializerSpecs extends Specification with Mockito { |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== """.stripMargin.lines.map(_.trim).mkString } - } "AkkaDecoder" should "decode" >> { @@ -102,6 +107,10 @@ class SerializerSpecs extends Specification with Mockito { "-85".decoded[ Byte ] mustEqual 0xAB.toByte } + "byte[]" in { + "YWJj".decoded[ Array[ Byte ] ] mustEqual Array( "a".head.toByte, "b".head.toByte, "c".head.toByte ) + } + "char" in { "a".decoded[ Char ] mustEqual 'a' "b".decoded[ Char ] mustEqual 'b' diff --git a/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala b/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala index 61f27d98..dbf2b9d6 100644 --- a/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala +++ b/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala @@ -1,7 +1,5 @@ package play.api.cache.redis.impl -import java.util.concurrent.Callable - import scala.concurrent.duration.Duration import play.api.cache.redis._ @@ -105,5 +103,35 @@ class JavaRedisSpecs( implicit ee: ExecutionEnv ) extends Specification with Red async.invalidate() returns execDone cache.removeAll().toScala must beDone.await } + + "get and set 'byte'" in new MockedJavaRedis { + val byte = JavaTypes.byteValue + + // set a value + // note: there should be hit on "byte" but the value is wrapped instead + async.set( anyString, beEq( byte ), any[ Duration ] ) returns execDone + async.set( anyString, beEq( "byte" ), any[ Duration ] ) returns execDone + async.set( anyString, beEq( "java.lang.Byte" ), any[ Duration ] ) returns execDone + cache.set( key, byte ).toScala must beDone.await + + // hit on GET + async.get[ Byte ]( beEq( key ) )( anyClassTag ) returns Some( byte ) + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "java.lang.Byte" ) + cache.get[ Byte ]( key ).toScala must beEqualTo( byte ).await + } + + "get and set 'byte[]'" in new MockedJavaRedis { + val bytes = JavaTypes.bytesValue + + // set a value + async.set( anyString, beEq( bytes ), any[ Duration ] ) returns execDone + async.set( anyString, beEq( "byte[]" ), any[ Duration ] ) returns execDone + cache.set( key, bytes ).toScala must beDone.await + + // hit on GET + async.get[ Array[ Byte ] ]( beEq( key ) )( anyClassTag ) returns Some( bytes ) + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "byte[]" ) + cache.get[ Array[ Byte ] ]( key ).toScala must beEqualTo( bytes ).await + } } }