diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Language.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Language.java index 1753969dee..d6c91bc37e 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Language.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/Language.java @@ -7,8 +7,12 @@ package akka.http.javadsl.model.headers; import akka.http.impl.util.Util; import akka.http.scaladsl.model.headers.Language$; -public abstract class Language implements LanguageRange { +public abstract class Language { public static Language create(String primaryTag, String... subTags) { return Language$.MODULE$.apply(primaryTag, Util.convertArray(subTags)); } + + public abstract String primaryTag(); + public abstract Iterable getSubTags(); + public abstract LanguageRange withQValue(float qValue); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/LanguageRange.java b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/LanguageRange.java index 9b7c4f29f4..68fea5fbb7 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/headers/LanguageRange.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/headers/LanguageRange.java @@ -7,10 +7,8 @@ package akka.http.javadsl.model.headers; public interface LanguageRange { public abstract String primaryTag(); public abstract float qValue(); - - public abstract Iterable getSubTags(); public abstract boolean matches(Language language); - + public abstract Iterable getSubTags(); public abstract LanguageRange withQValue(float qValue); public static final LanguageRange ALL = akka.http.scaladsl.model.headers.LanguageRange.$times$.MODULE$; diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptLanguageHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptLanguageHeader.scala index f5e7b463ed..3c99ef7ab4 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptLanguageHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptLanguageHeader.scala @@ -23,5 +23,5 @@ private[parser] trait AcceptLanguageHeader { this: Parser with CommonRules with } } - def `language-range` = rule { ws('*') ~ push(LanguageRange.`*`) | language } + def `language-range` = rule { ws('*') ~ push(LanguageRange.`*`) | language ~> (LanguageRange(_)) } } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala index ef8838599b..8b6262ee1c 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala @@ -193,6 +193,19 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET, range ← encodingRanges } yield range).sortBy(-_.qValue) + /** + * The language-ranges accepted by the client according to the `Accept-Language` request header. + * The returned ranges are sorted by increasing generality (i.e. most specific first). + */ + def acceptedLanguageRanges: immutable.Seq[LanguageRange] = + (for { + `Accept-Language`(languageRanges) ← headers + range ← languageRanges + } yield range).sortBy { + case _: LanguageRange.`*` ⇒ 0 // most general, needs to come last + case x ⇒ -(x.subTags.size + 1) // more subtags -> more specific -> go first + } + /** * All cookies provided by the client in one or more `Cookie` headers. */ @@ -243,6 +256,22 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET, case x ⇒ x collectFirst { case r if r matches encoding ⇒ r.qValue } getOrElse 0f } + /** + * Determines whether the given language is accepted by the client. + */ + def isLanguageAccepted(language: Language, ranges: Seq[LanguageRange] = acceptedLanguageRanges): Boolean = + qValueForLanguage(language, ranges) > 0f + + /** + * Returns the q-value that the client (implicitly or explicitly) attaches to the given language. + * Note: The given ranges must be sorted by increasing generality (i.e. most specific first)! + */ + def qValueForLanguage(language: Language, ranges: Seq[LanguageRange] = acceptedLanguageRanges): Float = + ranges match { + case Nil ⇒ 1.0f // http://tools.ietf.org/html/rfc7231#section-5.3.1 + case x ⇒ x collectFirst { case r if r matches language ⇒ r.qValue } getOrElse 0f + } + /** * Determines whether this request can be safely retried, which is the case only of the request method is idempotent. */ diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/LanguageRange.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/LanguageRange.scala index f9dbe60c03..5826b52158 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/LanguageRange.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/LanguageRange.scala @@ -4,11 +4,13 @@ package akka.http.scaladsl.model.headers +import scala.language.implicitConversions import scala.collection.immutable import akka.http.impl.util._ import akka.http.scaladsl.model.WithQValue import akka.http.javadsl.{ model ⇒ jm } import akka.http.impl.util.JavaMapping.Implicits._ +import akka.japi sealed trait LanguageRange extends jm.headers.LanguageRange with ValueRenderable with WithQValue[LanguageRange] { def qValue: Float @@ -23,7 +25,7 @@ sealed trait LanguageRange extends jm.headers.LanguageRange with ValueRenderable } /** Java API */ - def matches(language: jm.headers.Language): Boolean = matches(language.asScala) + def matches(language: jm.headers.Language) = matches(language.asScala) def getSubTags: java.lang.Iterable[String] = subTags.asJava } object LanguageRange { @@ -31,19 +33,44 @@ object LanguageRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") def primaryTag = "*" def subTags = Nil - def matches(lang: Language): Boolean = true + def matches(lang: Language) = true def withQValue(qValue: Float) = if (qValue == 1.0f) `*` else if (qValue != this.qValue) `*`(qValue.toFloat) else this } object `*` extends `*`(1.0f) + + final case class One(language: Language, qValue: Float) extends LanguageRange { + require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") + def matches(l: Language) = + (language.primaryTag equalsIgnoreCase l.primaryTag) && + language.subTags.size <= l.subTags.size && + (language.subTags zip l.subTags).forall(t ⇒ t._1 equalsIgnoreCase t._2) + def primaryTag = language.primaryTag + def subTags = language.subTags + def withQValue(qValue: Float) = One(language, qValue) + } + + implicit def apply(language: Language): LanguageRange = apply(language, 1.0f) + def apply(language: Language, qValue: Float): LanguageRange = One(language, qValue) } -final case class Language(primaryTag: String, subTags: immutable.Seq[String], qValue: Float = 1.0f) extends jm.headers.Language with LanguageRange { - require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") - def matches(lang: Language): Boolean = lang.primaryTag == this.primaryTag && lang.subTags == this.subTags - def withQValue(qValue: Float): Language = Language(primaryTag, subTags, qValue) - override def withQValue(qValue: Double): Language = withQValue(qValue.toFloat) +final case class Language(primaryTag: String, subTags: immutable.Seq[String]) + extends jm.headers.Language with ValueRenderable with WithQValue[LanguageRange] { + def withQValue(qValue: Float) = LanguageRange(this, qValue.toFloat) + def render[R <: Rendering](r: R): r.type = { + r ~~ primaryTag + if (subTags.nonEmpty) subTags.foreach(r ~~ '-' ~~ _) + r + } + + /** Java API */ + def getSubTags: java.lang.Iterable[String] = subTags.asJava } object Language { - def apply(primaryTag: String, subTags: String*) = new Language(primaryTag, immutable.Seq(subTags: _*)) + implicit def apply(compoundTag: String): Language = + if (compoundTag.indexOf('-') >= 0) { + val tags = compoundTag.split('-') + new Language(tags.head, immutable.Seq(tags.tail: _*)) + } else new Language(compoundTag, immutable.Seq.empty) + def apply(primaryTag: String, subTags: String*): Language = new Language(primaryTag, immutable.Seq(subTags: _*)) } \ No newline at end of file