=hco #15682 replace default toLowerCase calls with one using Locale.ROOT

This commit is contained in:
Johannes Rudolph 2014-09-08 17:56:16 +02:00
parent 10868966d3
commit 0bc2f37fc4
14 changed files with 76 additions and 21 deletions

View file

@ -81,8 +81,8 @@ object HttpCharset {
// see http://www.iana.org/assignments/character-sets
object HttpCharsets extends ObjectRegistry[String, HttpCharset] {
def register(charset: HttpCharset): HttpCharset = {
charset.aliases.foreach(alias register(alias.toLowerCase, charset))
register(charset.value.toLowerCase, charset)
charset.aliases.foreach(alias register(alias.toRootLowerCase, charset))
register(charset.value.toRootLowerCase, charset)
}
/** Register standard charset that is required to be supported on all platforms */

View file

@ -13,6 +13,7 @@ import scala.concurrent.duration.FiniteDuration
import akka.stream.FlowMaterializer
import scala.concurrent.{ ExecutionContext, Future }
import akka.util.ByteString
import akka.http.util._
/**
* Common base class of HttpRequest and HttpResponse.
@ -81,8 +82,10 @@ sealed trait HttpMessage extends japi.HttpMessage {
def connectionCloseExpected: Boolean = HttpMessage.connectionCloseExpected(protocol, header[Connection])
def addHeader(header: japi.HttpHeader): Self = mapHeaders(_ :+ header.asInstanceOf[HttpHeader])
/** Removes the header with the given name (case-insensitive) */
def removeHeader(headerName: String): Self = {
val lowerHeaderName = headerName.toLowerCase()
val lowerHeaderName = headerName.toRootLowerCase
mapHeaders(_.filterNot(_.is(lowerHeaderName)))
}
@ -101,7 +104,7 @@ sealed trait HttpMessage extends japi.HttpMessage {
def getHeader[T <: japi.HttpHeader](headerClass: Class[T]): akka.japi.Option[T] = header(ClassTag(headerClass))
/** Java API */
def getHeader(headerName: String): akka.japi.Option[japi.HttpHeader] = {
val lowerCased = headerName.toLowerCase
val lowerCased = headerName.toRootLowerCase
headers.find(_.is(lowerCased))
}
/** Java API */

View file

@ -76,7 +76,7 @@ object MediaRange {
def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = {
val (ps, q) = splitOffQValue(params, qValue)
Custom(mainType.toLowerCase, ps, q)
Custom(mainType.toRootLowerCase, ps, q)
}
final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable {
@ -228,15 +228,15 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
def register(mediaType: MediaType): MediaType = synchronized {
def registerFileExtension(ext: String): Unit = {
val lcExt = ext.toLowerCase
val lcExt = ext.toRootLowerCase
require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'")
extensionMap = extensionMap.updated(lcExt, mediaType)
}
mediaType.fileExtensions.foreach(registerFileExtension)
register(mediaType.mainType.toLowerCase -> mediaType.subType.toLowerCase, mediaType)
register(mediaType.mainType.toRootLowerCase -> mediaType.subType.toRootLowerCase, mediaType)
}
def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toLowerCase)
def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase)
private def app(subType: String, compressible: Boolean, binary: Boolean, fileExtensions: String*) = register {
new NonMultipartMediaType("application", subType, compressible, binary, immutable.Seq(fileExtensions: _*)) {

View file

@ -49,7 +49,7 @@ object HttpEncoding {
// see http://www.iana.org/assignments/http-parameters/http-parameters.xml
object HttpEncodings extends ObjectRegistry[String, HttpEncoding] {
def register(encoding: HttpEncoding): HttpEncoding =
register(encoding.value.toLowerCase, encoding)
register(encoding.value.toRootLowerCase, encoding)
private def register(value: String): HttpEncoding = register(HttpEncoding(value))

View file

@ -21,7 +21,7 @@ import ProtectedHeaderCreation.enable
sealed abstract class ModeledCompanion extends Renderable {
val name = getClass.getSimpleName.replace("$minus", "-").dropRight(1) // trailing $
val lowercaseName = name.toLowerCase
val lowercaseName = name.toRootLowerCase
private[this] val nameBytes = name.asciiBytes
def render[R <: Rendering](r: R): r.type = r ~~ nameBytes ~~ ':' ~~ ' '
}
@ -109,7 +109,7 @@ final case class `If-Range`(entityTagOrDateTime: Either[EntityTag, DateTime]) ex
// FIXME: resurrect SSL-Session-Info header once akka.io.SslTlsSupport supports it
final case class RawHeader(name: String, value: String) extends japi.headers.RawHeader {
val lowercaseName = name.toLowerCase
val lowercaseName = name.toRootLowerCase
def render[R <: Rendering](r: R): r.type = r ~~ name ~~ ':' ~~ ' ' ~~ value
}

View file

@ -27,5 +27,5 @@ private[parser] trait AcceptEncodingHeader { this: Parser with CommonRules with
def codings = rule { ws('*') ~ push(HttpEncodingRange.`*`) | token ~> getEncoding }
private val getEncoding: String HttpEncodingRange =
name HttpEncodingRange(HttpEncodings.getForKey(name.toLowerCase) getOrElse HttpEncoding.custom(name))
name HttpEncodingRange(HttpEncodings.getForKeyCaseInsensitive(name) getOrElse HttpEncoding.custom(name))
}

View file

@ -5,6 +5,8 @@
package akka.http.model
package parser
import akka.http.util._
import akka.parboiled2.Parser
import headers._
@ -19,7 +21,7 @@ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonAc
def `media-range-decl` = rule {
`media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params)
if (sub == "*") {
val mainLower = main.toLowerCase
val mainLower = main.toRootLowerCase
MediaRanges.getForKey(mainLower) match {
case Some(registered) if (params.isEmpty) registered else registered.withParams(params.toMap)
case None MediaRange.custom(mainLower, params.toMap)

View file

@ -5,6 +5,8 @@
package akka.http.model
package parser
import akka.http.util._
import MediaTypes._
private[parser] trait CommonActions {
@ -12,8 +14,8 @@ private[parser] trait CommonActions {
type StringMapBuilder = scala.collection.mutable.Builder[(String, String), Map[String, String]]
def getMediaType(mainType: String, subType: String, params: Map[String, String]): MediaType = {
mainType.toLowerCase match {
case "multipart" subType.toLowerCase match {
mainType.toRootLowerCase match {
case "multipart" subType.toRootLowerCase match {
case "mixed" multipart.mixed(params)
case "alternative" multipart.alternative(params)
case "related" multipart.related(params)
@ -23,7 +25,7 @@ private[parser] trait CommonActions {
case custom multipart(custom, params)
}
case mainLower
MediaTypes.getForKey((mainLower, subType.toLowerCase)) match {
MediaTypes.getForKey((mainLower, subType.toRootLowerCase)) match {
case Some(registered) if (params.isEmpty) registered else registered.withParams(params)
case None MediaType.custom(mainType, subType, params = params, allowArbitrarySubtypes = true)
}
@ -32,7 +34,7 @@ private[parser] trait CommonActions {
def getCharset(name: String): HttpCharset =
HttpCharsets
.getForKey(name.toLowerCase)
.getForKeyCaseInsensitive(name)
.orElse(HttpCharset.custom(name))
.getOrElse(throw new ParsingException("Unsupported charset", name))
}

View file

@ -77,7 +77,7 @@ private[parser] trait SimpleHeaders { this: Parser with CommonRules with CommonA
// http://tools.ietf.org/html/rfc7231#section-3.1.2.2
// http://tools.ietf.org/html/rfc7231#appendix-D
def `content-encoding` = rule {
oneOrMore(token ~> (x HttpEncodings.getForKey(x.toLowerCase) getOrElse HttpEncoding.custom(x)))
oneOrMore(token ~> (x HttpEncodings.getForKeyCaseInsensitive(x) getOrElse HttpEncoding.custom(x)))
.separatedBy(listSep) ~ EOI ~> (`Content-Encoding`(_))
}

View file

@ -6,6 +6,7 @@ package akka.http.model.parser
import java.nio.charset.Charset
import akka.parboiled2._
import akka.http.util.enhanceString_
import akka.http.model.Uri
import akka.http.model.headers.HttpOrigin
import Uri._
@ -220,7 +221,7 @@ private[http] class UriParser(val input: ParserInput,
if (firstPercentIx >= 0) decode(sb.toString, charset, firstPercentIx)() else sb.toString
private def getDecodedStringAndLowerIfEncoded(charset: Charset = uriParsingCharset) =
if (firstPercentIx >= 0) decode(sb.toString, charset, firstPercentIx)().toLowerCase else sb.toString
if (firstPercentIx >= 0) decode(sb.toString, charset, firstPercentIx)().toRootLowerCase else sb.toString
private def createUriReference(): Uri = {
val path = if (_scheme.isEmpty) _path else collapseDotSegments(_path)

View file

@ -9,7 +9,7 @@ import java.lang.{ StringBuilder ⇒ JStringBuilder }
import scala.annotation.tailrec
import akka.parboiled2.CharUtils
import akka.util.ByteString
import akka.http.util.{ SingletonException, Rendering }
import akka.http.util._
import akka.http.model.{ IllegalHeaderException, StatusCodes, HttpHeader, ErrorInfo }
import akka.http.model.headers.RawHeader
import akka.http.model.parser.HeaderParser
@ -420,7 +420,7 @@ private[http] object HttpHeaderParser {
val pivot = (startIx + endIx) / 2
items(pivot) match {
case valueParser: HeaderValueParser
val insertName = valueParser.headerName.toLowerCase + ':'
val insertName = valueParser.headerName.toRootLowerCase + ':'
if (parser.isEmpty) parser.insertRemainingCharsAsNewNodes(ByteString(insertName), valueParser)()
else parser.insert(ByteString(insertName), valueParser)()
case header: String

View file

@ -4,6 +4,8 @@
package akka.http.util
import java.util.Locale
import scala.annotation.tailrec
import scala.collection.immutable
@ -111,4 +113,10 @@ private[http] class EnhancedString(val underlying: String) extends AnyVal {
/** Strips margin and fixes the newline sequence to the given one preventing dependencies on the build platform */
def stripMarginWithNewline(newline: String) = underlying.stripMargin.replace("\r\n", "\n").replace("\n", newline)
/**
* Provides a default toLowerCase that doesn't suffer from the dreaded turkish-i problem.
* See http://bugs.java.com/view_bug.do?bug_id=6208680
*/
def toRootLowerCase: String = underlying.toLowerCase(Locale.ROOT)
}

View file

@ -22,4 +22,7 @@ private[http] trait ObjectRegistry[K, V <: AnyRef] {
protected def registry: Map[K, V] = _registry
def getForKey(key: K): Option[V] = registry.get(key)
def getForKeyCaseInsensitive(key: String)(implicit conv: String <:< K): Option[V] =
getForKey(conv(key.toRootLowerCase))
}

View file

@ -0,0 +1,36 @@
/*
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model
import java.util.Locale
import akka.http.util._
import org.scalatest.{ Matchers, WordSpec }
class TurkishISpec extends WordSpec with Matchers {
"Model" should {
"not suffer from turkish-i problem" in {
val charsetCons = Class.forName("akka.http.model.HttpCharsets$").getDeclaredConstructor()
charsetCons.setAccessible(true)
val previousLocale = Locale.getDefault
try {
// recreate HttpCharsets in turkish locale
Locale.setDefault(new Locale("tr", "TR"))
val testString = "ISO-8859-1"
// demonstrate difference between toRootLowerCase and toLowerCase(turkishLocale)
testString.toLowerCase should not equal (testString.toRootLowerCase)
val newCharsets = charsetCons.newInstance().asInstanceOf[HttpCharsets.type]
newCharsets.getForKey("iso-8859-1") shouldEqual Some(newCharsets.`ISO-8859-1`)
} finally {
Locale.setDefault(previousLocale)
}
}
}
}