package controllers.caching import play.api.mvc.Results.NotModified import play.api.mvc.{Request, Result} import java.io.{ByteArrayOutputStream, DataOutputStream, OutputStream} import java.security.{DigestOutputStream, MessageDigest} import java.util.Base64 import scala.concurrent.{ExecutionContext, Future} import scala.util.Using trait Etagable[A]: extension (a: A) def etag: String trait DigestEtagable[A] extends Etagable[A]: private val base64 = Base64.getUrlEncoder.withoutPadding protected def writeDigestData(a: A, s: DataOutputStream): Unit extension (a: A) { override def etag: String = { val md = MessageDigest.getInstance("SHA-1") Using(DataOutputStream(DigestOutputStream(OutputStream.nullOutputStream(), md))) { dos => writeDigestData(a, dos) dos.flush() } base64.encodeToString(md.digest()) } } trait CheckFresh { protected def checkFreshAsync[A, E: Etagable](entity: E)(onStale: => Future[Result])(using request: Request[A], ec: ExecutionContext): Future[Result] = { compareEtag(request, entity).fold( entityEtag => onStale.map(_.withHeaders("etag" -> entityEtag)), requestEtag => Future.successful(notModified(requestEtag)) ) } protected def checkFresh[A, E: Etagable](entity: E)(onStale: => Result)(using request: Request[A]): Result = { compareEtag(request, entity).fold( entityEtag => onStale.withHeaders("etag" -> entityEtag), requestEtag => notModified(requestEtag) ) } private def compareEtag[A, E: Etagable](request: Request[A], entity: E) = { val entityEtag = entity.etag request.headers.get("If-None-Match").filter(e => e == entityEtag) match { case Some(etag) => Right(etag) case None => Left(entityEtag) } } private def notModified(etag: String) = NotModified.withHeaders("etag" -> etag) }