From 46fd9fa470f864209223f65dff535b441d8c702a Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 18:35:57 +0200 Subject: [PATCH 01/18] Introduce conditional request parameters leveraging If-(None)-Match and If-(Not)-Modified HTTP headers support in Riak --- .../riak/internal/RiakHttpClientHelper.scala | 2 +- ...UriSupport.scala => RiakHttpSupport.scala} | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) rename src/main/scala/com/scalapenos/riak/internal/{RiakUriSupport.scala => RiakHttpSupport.scala} (70%) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index 7e36514..56df5c8 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -33,7 +33,7 @@ private[riak] object RiakHttpClientHelper { } } -private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakUriSupport with RiakIndexSupport with DateTimeSupport { +private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSupport with RiakIndexSupport with DateTimeSupport { import scala.concurrent.Future import scala.concurrent.Future._ diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakUriSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala similarity index 70% rename from src/main/scala/com/scalapenos/riak/internal/RiakUriSupport.scala rename to src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index b02de1c..3e7559a 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakUriSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -17,9 +17,9 @@ package com.scalapenos.riak package internal -private[riak] trait RiakUriSupport { - import spray.http.Uri - import spray.http.Uri._ +private[riak] trait RiakHttpSupport { + import spray.http.{ Uri, HttpHeader, HttpHeaders, EntityTag }, HttpHeaders._, Uri._ + import DateTimeSupport._ // ========================================================================== // Query Parameters @@ -37,6 +37,30 @@ private[riak] trait RiakUriSupport { def query = ("returnbody", s"$returnBody") +: Query.Empty } + // ========================================================================== + // Conditional Request Parameters + // ========================================================================== + + sealed trait ConditionalRequestParam { + def asHeader: HttpHeader + } + + case class IfNoneMatch(eTag: String) extends ConditionalRequestParam { + def asHeader: HttpHeader = `If-None-Match`(EntityTag(eTag)) + } + + case class IfMatch(eTag: String) extends ConditionalRequestParam { + def asHeader: HttpHeader = `If-Match`(EntityTag(eTag)) + } + + case class IfModified(timestamp: DateTime) extends ConditionalRequestParam { + def asHeader: HttpHeader = `If-Modified-Since`(toSprayDateTime(timestamp)) + } + + case class IfNotModified(timestamp: DateTime) extends ConditionalRequestParam { + def asHeader: HttpHeader = `If-Unmodified-Since`(toSprayDateTime(timestamp)) + } + // ========================================================================== // URL building and Query Parameters // ========================================================================== From 4bbc75b7278698954e75381a2179360c82d94719 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 18:36:11 +0200 Subject: [PATCH 02/18] Remove an unused import --- .../com/scalapenos/riak/internal/RiakHttpClientHelper.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index 56df5c8..734e1cc 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -44,8 +44,6 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu import spray.httpx.SprayJsonSupport._ import spray.json.DefaultJsonProtocol._ - import org.slf4j.LoggerFactory - import SprayClientExtras._ import RiakHttpHeaders._ import RiakHttpClientHelper._ From 47b43d456d3667314c2b483087922de13f7e249c Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 18:40:15 +0200 Subject: [PATCH 03/18] Add conditional request params to internal helper API for fetching single values from Riak --- .../com/scalapenos/riak/internal/RiakHttpClientHelper.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index 734e1cc..27d5c6e 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -66,14 +66,14 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu } } - def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver): Future[Option[RiakValue]] = { - httpRequest(Get(KeyUri(server, bucket, key))).flatMap { response ⇒ + def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalRequestParams: List[ConditionalRequestParam] = Nil): Future[Option[RiakValue]] = { + httpRequest(Get(KeyUri(server, bucket, key)).withHeaders(conditionalRequestParams.map(_.asHeader))).flatMap { response ⇒ response.status match { case OK ⇒ successful(toRiakValue(response)) case NotFound ⇒ successful(None) + case NotModified ⇒ successful(None) // This means that client is not able to distinguish cases when value is not in Riak or a supplied condition is not met. case MultipleChoices ⇒ resolveConflict(server, bucket, key, response, resolver).map(Some(_)) case other ⇒ throw new BucketOperationFailed(s"Fetch for key '$key' in bucket '$bucket' produced an unexpected response code '$other'.") - // TODO: case NotModified => successful(None) } } } From 88d1bcaee9621f27c2b26f0b4e506e03dee55f01 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 19:13:18 +0200 Subject: [PATCH 04/18] Introduce conditional request params to public RiakBucket single object fetch API --- .../scala/com/scalapenos/riak/RiakBucket.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 0ef4a62..3834206 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -19,6 +19,7 @@ package com.scalapenos.riak trait RiakBucket { import scala.concurrent.{ ExecutionContext, Future } import internal._ + import RiakBucket._ val name: String @@ -28,7 +29,7 @@ trait RiakBucket { */ def resolver: RiakConflictsResolver - def fetch(key: String): Future[Option[RiakValue]] + def fetch(key: String, conditionalParams: ConditionalRequestParam*): Future[Option[RiakValue]] def fetch(index: String, value: String): Future[List[RiakValue]] = fetch(RiakIndex(index, value)) def fetch(index: String, value: Int): Future[List[RiakValue]] = fetch(RiakIndex(index, value)) @@ -72,3 +73,16 @@ trait RiakBucket { def unsafe: UnsafeBucketOperations } + +object RiakBucket { + + sealed trait ConditionalRequestParam + + case class IfNoneMatch(eTag: String) extends ConditionalRequestParam + + case class IfMatch(eTag: String) extends ConditionalRequestParam + + case class IfModified(timestamp: DateTime) extends ConditionalRequestParam + + case class IfNotModified(timestamp: DateTime) extends ConditionalRequestParam +} From f7eddda9f578b57f905eb3317615359538da057e Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 19:14:57 +0200 Subject: [PATCH 05/18] Adapt Riak HTTP helper to support conditional request param fetch API --- .../riak/internal/RiakHttpBucket.scala | 6 +++- .../riak/internal/RiakHttpClientHelper.scala | 5 +-- .../riak/internal/RiakHttpSupport.scala | 31 +++++++------------ 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpBucket.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpBucket.scala index a2feab7..d35d354 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpBucket.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpBucket.scala @@ -17,8 +17,12 @@ package com.scalapenos.riak package internal +import RiakBucket._ + private[riak] sealed class RiakHttpBucket(helper: RiakHttpClientHelper, server: RiakServerInfo, val name: String, val resolver: RiakConflictsResolver) extends RiakBucket { - def fetch(key: String) = helper.fetch(server, name, key, resolver) + + def fetch(key: String, conditionalParams: ConditionalRequestParam*) = helper.fetch(server, name, key, resolver, conditionalParams) + def fetch(index: RiakIndex) = helper.fetch(server, name, index, resolver) def fetch(indexRange: RiakIndexRange) = helper.fetch(server, name, indexRange, resolver) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index 27d5c6e..bbfa4c9 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -47,6 +47,7 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu import SprayClientExtras._ import RiakHttpHeaders._ import RiakHttpClientHelper._ + import RiakBucket._ import system.dispatcher @@ -66,8 +67,8 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu } } - def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalRequestParams: List[ConditionalRequestParam] = Nil): Future[Option[RiakValue]] = { - httpRequest(Get(KeyUri(server, bucket, key)).withHeaders(conditionalRequestParams.map(_.asHeader))).flatMap { response ⇒ + def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalParams: ConditionalRequestParam*): Future[Option[RiakValue]] = { + httpRequest(Get(KeyUri(server, bucket, key)).withHeaders(conditionalParams.map(_.asHttpHeader): _*)).flatMap { response ⇒ response.status match { case OK ⇒ successful(toRiakValue(response)) case NotFound ⇒ successful(None) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index 3e7559a..7345261 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -20,6 +20,7 @@ package internal private[riak] trait RiakHttpSupport { import spray.http.{ Uri, HttpHeader, HttpHeaders, EntityTag }, HttpHeaders._, Uri._ import DateTimeSupport._ + import RiakBucket._ // ========================================================================== // Query Parameters @@ -38,27 +39,19 @@ private[riak] trait RiakHttpSupport { } // ========================================================================== - // Conditional Request Parameters + // Conditional Request Parameters Support // ========================================================================== - sealed trait ConditionalRequestParam { - def asHeader: HttpHeader - } - - case class IfNoneMatch(eTag: String) extends ConditionalRequestParam { - def asHeader: HttpHeader = `If-None-Match`(EntityTag(eTag)) - } - - case class IfMatch(eTag: String) extends ConditionalRequestParam { - def asHeader: HttpHeader = `If-Match`(EntityTag(eTag)) - } - - case class IfModified(timestamp: DateTime) extends ConditionalRequestParam { - def asHeader: HttpHeader = `If-Modified-Since`(toSprayDateTime(timestamp)) - } - - case class IfNotModified(timestamp: DateTime) extends ConditionalRequestParam { - def asHeader: HttpHeader = `If-Unmodified-Since`(toSprayDateTime(timestamp)) + implicit class ConditionalHttpRequestParam(conditionalParam: ConditionalRequestParam) { + def asHttpHeader: HttpHeader = { + conditionalParam match { + case IfModified(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) + case IfNotModified(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) + case IfMatch(eTag) ⇒ `If-Match`(EntityTag(eTag)) + case IfNoneMatch(eTag) ⇒ `If-None-Match`(EntityTag(eTag)) + case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") + } + } } // ========================================================================== From aeeebdcf5eeec473cfe5db29a30fd65522187725 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Tue, 5 Jul 2016 19:16:27 +0200 Subject: [PATCH 06/18] Fix compile error in internal API helper --- .../com/scalapenos/riak/internal/RiakHttpClientHelper.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index bbfa4c9..f399d72 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -67,7 +67,7 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu } } - def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalParams: ConditionalRequestParam*): Future[Option[RiakValue]] = { + def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalParams: Seq[ConditionalRequestParam] = Seq()): Future[Option[RiakValue]] = { httpRequest(Get(KeyUri(server, bucket, key)).withHeaders(conditionalParams.map(_.asHttpHeader): _*)).flatMap { response ⇒ response.status match { case OK ⇒ successful(toRiakValue(response)) From 2d60100c5e76e245fd8957da56653b0d2f68d7fc Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:13:39 +0200 Subject: [PATCH 07/18] Refine class constructor arguments --- src/main/scala/com/scalapenos/riak/RiakBucket.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 3834206..0754403 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -78,9 +78,9 @@ object RiakBucket { sealed trait ConditionalRequestParam - case class IfNoneMatch(eTag: String) extends ConditionalRequestParam + case class IfNoneMatch(eTag: ETag) extends ConditionalRequestParam - case class IfMatch(eTag: String) extends ConditionalRequestParam + case class IfMatch(eTag: ETag) extends ConditionalRequestParam case class IfModified(timestamp: DateTime) extends ConditionalRequestParam From 2625e736cc893f33409fc9327e750f39be51e4d0 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:14:46 +0200 Subject: [PATCH 08/18] Add handling for 412 PreconditionFailed http status code when fetching with If-Match condition that doesn't hold --- .../riak/internal/RiakHttpClientHelper.scala | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala index f399d72..8c8e3d6 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpClientHelper.scala @@ -70,11 +70,12 @@ private[riak] class RiakHttpClientHelper(system: ActorSystem) extends RiakHttpSu def fetch(server: RiakServerInfo, bucket: String, key: String, resolver: RiakConflictsResolver, conditionalParams: Seq[ConditionalRequestParam] = Seq()): Future[Option[RiakValue]] = { httpRequest(Get(KeyUri(server, bucket, key)).withHeaders(conditionalParams.map(_.asHttpHeader): _*)).flatMap { response ⇒ response.status match { - case OK ⇒ successful(toRiakValue(response)) - case NotFound ⇒ successful(None) - case NotModified ⇒ successful(None) // This means that client is not able to distinguish cases when value is not in Riak or a supplied condition is not met. - case MultipleChoices ⇒ resolveConflict(server, bucket, key, response, resolver).map(Some(_)) - case other ⇒ throw new BucketOperationFailed(s"Fetch for key '$key' in bucket '$bucket' produced an unexpected response code '$other'.") + case OK ⇒ successful(toRiakValue(response)) + case NotFound ⇒ successful(None) + case NotModified ⇒ successful(None) // This means that client is not able to distinguish cases when value is not in Riak or a supplied condition is not met. + case MultipleChoices ⇒ resolveConflict(server, bucket, key, response, resolver).map(Some(_)) + case PreconditionFailed ⇒ successful(None) // Fetch with If-Match header returns that if ETag value doesn't match. + case other ⇒ throw new BucketOperationFailed(s"Fetch for key '$key' in bucket '$bucket' produced an unexpected response code '$other'.") } } } From 8ebfe8d8ad16ab6e3163500bc12b6c9cfd04a9a4 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:29:17 +0200 Subject: [PATCH 09/18] Rename a class --- src/main/scala/com/scalapenos/riak/RiakBucket.scala | 2 +- .../scala/com/scalapenos/riak/internal/RiakHttpSupport.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 0754403..9d0d4a9 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -84,5 +84,5 @@ object RiakBucket { case class IfModified(timestamp: DateTime) extends ConditionalRequestParam - case class IfNotModified(timestamp: DateTime) extends ConditionalRequestParam + case class IfUnmodified(timestamp: DateTime) extends ConditionalRequestParam } diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index 7345261..e518551 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -46,9 +46,9 @@ private[riak] trait RiakHttpSupport { def asHttpHeader: HttpHeader = { conditionalParam match { case IfModified(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) - case IfNotModified(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) case IfMatch(eTag) ⇒ `If-Match`(EntityTag(eTag)) case IfNoneMatch(eTag) ⇒ `If-None-Match`(EntityTag(eTag)) + case IfUnmodified(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") } } From fb6fe919105c5f2cd616f1682dd1fb7f0a6ee4c5 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:30:25 +0200 Subject: [PATCH 10/18] Align class API naming with http header names --- src/main/scala/com/scalapenos/riak/RiakBucket.scala | 4 ++-- .../scala/com/scalapenos/riak/internal/RiakHttpSupport.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 9d0d4a9..630d89e 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -82,7 +82,7 @@ object RiakBucket { case class IfMatch(eTag: ETag) extends ConditionalRequestParam - case class IfModified(timestamp: DateTime) extends ConditionalRequestParam + case class IfModifiedSince(timestamp: DateTime) extends ConditionalRequestParam - case class IfUnmodified(timestamp: DateTime) extends ConditionalRequestParam + case class IfUnmodifiedSince(timestamp: DateTime) extends ConditionalRequestParam } diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index e518551..494b43f 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -45,10 +45,10 @@ private[riak] trait RiakHttpSupport { implicit class ConditionalHttpRequestParam(conditionalParam: ConditionalRequestParam) { def asHttpHeader: HttpHeader = { conditionalParam match { - case IfModified(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) case IfMatch(eTag) ⇒ `If-Match`(EntityTag(eTag)) case IfNoneMatch(eTag) ⇒ `If-None-Match`(EntityTag(eTag)) - case IfUnmodified(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) + case IfModifiedSince(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) + case IfUnmodifiedSince(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") } } From d6e814cfeef5919c454032ce2cd3b5499a4107e5 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:34:35 +0200 Subject: [PATCH 11/18] Add tests for conditional fetch object requests --- .../com/scalapenos/riak/RiakBucketSpec.scala | 84 ++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala index 1f95d11..0456bed 100644 --- a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala +++ b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala @@ -16,6 +16,9 @@ package com.scalapenos.riak +import com.scalapenos.riak.RiakBucket.{IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince} +import org.joda.time.DateTime + class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with RandomBucketSupport { "A RiakBucket" should { @@ -53,6 +56,85 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with fetched should beNone } - } + // ============================================================================ + // Conditional requests tests + // ============================================================================ + + "not return back a stored value if 'If-None-Match' condition does not hold for a requested data" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + val eTag = storedValue.etag + + bucket.fetch(key, IfNoneMatch(eTag)).await must beNone + } + + "return back a stored value if 'If-None-Match' condition holds for requested data" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfNoneMatch(randomKey)).await must beSome(storedValue) + } + + "not return back a stored value if 'If-Match' condition does not hold for a requested data" in { + val bucket = randomBucket + val key = randomKey + + bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfMatch(randomKey)).await must beNone + } + + "return back a stored value if 'If-Match' condition holds for requested data" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + val eTag = storedValue.etag + + bucket.fetch(key, IfMatch(eTag)).await must beSome(storedValue) + } + + "not return back a stored value if 'If-Modified-Since' condition does not hold for a requested data" in { + val bucket = randomBucket + val key = randomKey + + bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfModifiedSince(DateTime.now.plusMinutes(5))).await must beNone + } + + "return back a stored value if 'If-Modified-Since' condition holds for requested data" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfModifiedSince(DateTime.now.minusMinutes(5))).await must beSome(storedValue) + } + + "not return back a stored value if 'If-Unmodified-Since' condition does not hold for a requested data" in { + val bucket = randomBucket + val key = randomKey + + bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfUnmodifiedSince(DateTime.now.minusMinutes(5))).await must beNone + } + + "return back a stored value if 'If-Unmodified-Since' condition holds for requested data" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + bucket.fetch(key, IfUnmodifiedSince(DateTime.now.plusMinutes(5))).await must beSome(storedValue) + } + } } From e051adebf12d406667e60db1a695be752f10aff6 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:35:12 +0200 Subject: [PATCH 12/18] Add a workaround for ETag-based http headers rendering --- .../scala/com/scalapenos/riak/internal/RiakHttpSupport.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index 494b43f..e499a06 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -45,10 +45,10 @@ private[riak] trait RiakHttpSupport { implicit class ConditionalHttpRequestParam(conditionalParam: ConditionalRequestParam) { def asHttpHeader: HttpHeader = { conditionalParam match { - case IfMatch(eTag) ⇒ `If-Match`(EntityTag(eTag)) - case IfNoneMatch(eTag) ⇒ `If-None-Match`(EntityTag(eTag)) case IfModifiedSince(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) case IfUnmodifiedSince(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) + case IfMatch(eTag) ⇒ RawHeader("If-Match", eTag.value) // TODO this `If-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value + case IfNoneMatch(eTag) ⇒ RawHeader("If-None-Match", eTag.value) // TODO this `If-None-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") } } From c5f51697066b36979cef192d65ce4492860d5876 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 01:36:40 +0200 Subject: [PATCH 13/18] Fix formatting --- .../com/scalapenos/riak/internal/RiakHttpSupport.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index e499a06..53f2a79 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -45,11 +45,11 @@ private[riak] trait RiakHttpSupport { implicit class ConditionalHttpRequestParam(conditionalParam: ConditionalRequestParam) { def asHttpHeader: HttpHeader = { conditionalParam match { - case IfModifiedSince(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) + case IfModifiedSince(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) case IfUnmodifiedSince(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) - case IfMatch(eTag) ⇒ RawHeader("If-Match", eTag.value) // TODO this `If-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value - case IfNoneMatch(eTag) ⇒ RawHeader("If-None-Match", eTag.value) // TODO this `If-None-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value - case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") + case IfMatch(eTag) ⇒ RawHeader("If-Match", eTag.value) // TODO this `If-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value + case IfNoneMatch(eTag) ⇒ RawHeader("If-None-Match", eTag.value) // TODO this `If-None-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value + case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") } } } From fed1fc20f42cf60bc3480bcf4b60c10c11eabda6 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 18:32:53 +0200 Subject: [PATCH 14/18] Fix a failing test: Riak treats If-Modified-Since condition as true for any time in future --- src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala index 0456bed..8f8e2f6 100644 --- a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala +++ b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala @@ -107,7 +107,7 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfModifiedSince(DateTime.now.plusMinutes(5))).await must beNone + bucket.fetch(key, IfModifiedSince(DateTime.now)).await must beNone } "return back a stored value if 'If-Modified-Since' condition holds for requested data" in { From 7733e08f3a2a376796cd1ad89c08eb5504447cb7 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Wed, 6 Jul 2016 18:46:46 +0200 Subject: [PATCH 15/18] Add scaladoc for new conditional request parameters --- .../com/scalapenos/riak/RiakBucket.scala | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 630d89e..2bffe2c 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -76,13 +76,41 @@ trait RiakBucket { object RiakBucket { + /** + * Parameter for conditional request semantics. + * Can be used for Fetch Value and Store Value operations. + */ sealed trait ConditionalRequestParam + /** + * Perform a request on a RiakValue only if value's ETag does not match the given one. + * + * @param eTag the target ETag value. + */ case class IfNoneMatch(eTag: ETag) extends ConditionalRequestParam + /** + * Perform a request on a RiakValue only if value's ETag matches the given one. + * + * @param eTag the target ETag value. + */ case class IfMatch(eTag: ETag) extends ConditionalRequestParam + /** + * Perform a request on a RiakValue only if value's Last-Modified time is after the given timestamp. + * + * @param timestamp + * + * *Note*: if target time is in the future then Riak always treats this condition as `true`. + */ case class IfModifiedSince(timestamp: DateTime) extends ConditionalRequestParam + /** + * Perform a request on a RiakValue only if value's Last-Modified time is before the given timestamp. + * + * @param timestamp + * + * *Note*: if target time is in the future then Riak always treats this condition as `true`. + */ case class IfUnmodifiedSince(timestamp: DateTime) extends ConditionalRequestParam } From 9a6bd024d570b00177c5fa138268d180833b6c7a Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 7 Jul 2016 11:28:06 +0200 Subject: [PATCH 16/18] [changed] Improve conditional request parameter naming As IfNoneChange accepts only one ETag value it's probably better to adjust it's name accordingly --- src/main/scala/com/scalapenos/riak/RiakBucket.scala | 2 +- .../com/scalapenos/riak/internal/RiakHttpSupport.scala | 2 +- src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/com/scalapenos/riak/RiakBucket.scala b/src/main/scala/com/scalapenos/riak/RiakBucket.scala index 2bffe2c..ba23252 100644 --- a/src/main/scala/com/scalapenos/riak/RiakBucket.scala +++ b/src/main/scala/com/scalapenos/riak/RiakBucket.scala @@ -87,7 +87,7 @@ object RiakBucket { * * @param eTag the target ETag value. */ - case class IfNoneMatch(eTag: ETag) extends ConditionalRequestParam + case class IfNotMatch(eTag: ETag) extends ConditionalRequestParam /** * Perform a request on a RiakValue only if value's ETag matches the given one. diff --git a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala index 53f2a79..504ea97 100644 --- a/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala +++ b/src/main/scala/com/scalapenos/riak/internal/RiakHttpSupport.scala @@ -48,7 +48,7 @@ private[riak] trait RiakHttpSupport { case IfModifiedSince(date) ⇒ `If-Modified-Since`(toSprayDateTime(date)) case IfUnmodifiedSince(date) ⇒ `If-Unmodified-Since`(toSprayDateTime(date)) case IfMatch(eTag) ⇒ RawHeader("If-Match", eTag.value) // TODO this `If-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value - case IfNoneMatch(eTag) ⇒ RawHeader("If-None-Match", eTag.value) // TODO this `If-None-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value + case IfNotMatch(eTag) ⇒ RawHeader("If-None-Match", eTag.value) // TODO this `If-None-Match`(EntityTag(eTag)) doesn't work as spray escapes double quotes in ETag value case _ ⇒ throw new IllegalArgumentException("Unknown conditional request param: cannot convert to HTTP header.") } } diff --git a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala index 8f8e2f6..9ccb817 100644 --- a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala +++ b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala @@ -16,7 +16,7 @@ package com.scalapenos.riak -import com.scalapenos.riak.RiakBucket.{IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince} +import com.scalapenos.riak.RiakBucket.{ IfMatch, IfModifiedSince, IfNotMatch, IfUnmodifiedSince } import org.joda.time.DateTime class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with RandomBucketSupport { @@ -69,7 +69,7 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with val eTag = storedValue.etag - bucket.fetch(key, IfNoneMatch(eTag)).await must beNone + bucket.fetch(key, IfNotMatch(eTag)).await must beNone } "return back a stored value if 'If-None-Match' condition holds for requested data" in { @@ -78,7 +78,7 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with val storedValue = bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfNoneMatch(randomKey)).await must beSome(storedValue) + bucket.fetch(key, IfNotMatch(randomKey)).await must beSome(storedValue) } "not return back a stored value if 'If-Match' condition does not hold for a requested data" in { From 07a62f57fdd69ea42ca95af83d3a1d90a1f31c32 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 7 Jul 2016 12:01:24 +0200 Subject: [PATCH 17/18] = Avoid using current time in integration tests with Riak (as it may cause test flickiness) --- .../com/scalapenos/riak/RiakBucketSpec.scala | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala index 9ccb817..4ef3922 100644 --- a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala +++ b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala @@ -105,9 +105,10 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with val bucket = randomBucket val key = randomKey - bucket.storeAndFetch(key, "value").await + val storedValue = bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfModifiedSince(DateTime.now)).await must beNone + // Fetch if the value has been modified after store operation + bucket.fetch(key, IfModifiedSince(storedValue.lastModified.plusMillis(1))).await must beNone } "return back a stored value if 'If-Modified-Since' condition holds for requested data" in { @@ -116,16 +117,18 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with val storedValue = bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfModifiedSince(DateTime.now.minusMinutes(5))).await must beSome(storedValue) + // Fetch if the value has been modified since before the store operation + bucket.fetch(key, IfModifiedSince(storedValue.lastModified.minusMinutes(5))).await must beSome(storedValue) } "not return back a stored value if 'If-Unmodified-Since' condition does not hold for a requested data" in { val bucket = randomBucket val key = randomKey - bucket.storeAndFetch(key, "value").await + val storedValue = bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfUnmodifiedSince(DateTime.now.minusMinutes(5))).await must beNone + // Fetch if the value has not been modified since before the store operation + bucket.fetch(key, IfUnmodifiedSince(storedValue.lastModified.minusMinutes(5))).await must beNone } "return back a stored value if 'If-Unmodified-Since' condition holds for requested data" in { @@ -134,7 +137,8 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with val storedValue = bucket.storeAndFetch(key, "value").await - bucket.fetch(key, IfUnmodifiedSince(DateTime.now.plusMinutes(5))).await must beSome(storedValue) + // Fetch if the value has not been modified since after the store operation + bucket.fetch(key, IfUnmodifiedSince(storedValue.lastModified.plusMinutes(5))).await must beSome(storedValue) } } } From 3fc479529b89b71ac1d6265b16562875dbe8ecc4 Mon Sep 17 00:00:00 2001 From: Dmitry Ivanov Date: Thu, 7 Jul 2016 12:04:07 +0200 Subject: [PATCH 18/18] = Add an integration tests for combining multiple conditional request parameters together --- .../com/scalapenos/riak/RiakBucketSpec.scala | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala index 4ef3922..f9e2aee 100644 --- a/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala +++ b/src/test/scala/com/scalapenos/riak/RiakBucketSpec.scala @@ -140,5 +140,45 @@ class RiakBucketSpec extends RiakClientSpecification with RandomKeySupport with // Fetch if the value has not been modified since after the store operation bucket.fetch(key, IfUnmodifiedSince(storedValue.lastModified.plusMinutes(5))).await must beSome(storedValue) } + + // Combining multiple request conditions + + "support multiple conditional request parameters" in { + val bucket = randomBucket + val key = randomKey + + val storedValue = bucket.storeAndFetch(key, "value").await + + // Fetch a value that hasn't been modified since after the store operation (this condition holds) + // only if it has a different tag (this condition doesn't hold) + bucket.fetch(key, + IfUnmodifiedSince(storedValue.lastModified.plusMillis(1)), + IfNotMatch(storedValue.etag) + ).await must beNone + + // Fetch a value if it has the same ETag (this condition holds) + // has been modified since before the store operation (this condition also holds) + bucket.fetch(key, + IfMatch(storedValue.etag), + IfModifiedSince(storedValue.lastModified.minusMillis(1)) + ).await must beSome(storedValue) + + // Fetch a value if it has the same ETag (this condition holds) + // and has been modified since after the store operation (this condition doesn't hold) + bucket.fetch(key, + IfMatch(storedValue.etag), + IfModifiedSince(storedValue.lastModified.plusMillis(1)) + ).await must beNone + + bucket.fetch(key, + // Repeating the same conditional parameter doesn't change the behaviour + IfNotMatch(storedValue.etag), + IfNotMatch(storedValue.etag)).await must beNone + + bucket.fetch(key, + // Repeating the same conditional parameter doesn't change the behaviour + IfModifiedSince(storedValue.lastModified.minusMinutes(5)), + IfModifiedSince(storedValue.lastModified.minusMinutes(5))).await must beSome(storedValue) + } } }