diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala index 8a8909cdff..8e8b75b29e 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala @@ -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 */ diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala b/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala index 6521f4a42b..641469e01b 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpMessage.scala @@ -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 */ diff --git a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala index 0978474cbe..364d4c15ce 100644 --- a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala @@ -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: _*)) { diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala b/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala index c7dbaba2d8..291a014417 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala @@ -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)) diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala index d85c969400..279ba35841 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/headers.scala @@ -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 } diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/AcceptEncodingHeader.scala b/akka-http-core/src/main/scala/akka/http/model/parser/AcceptEncodingHeader.scala index 075e4f5ddc..e375518ea1 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/AcceptEncodingHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/AcceptEncodingHeader.scala @@ -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)) } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/AcceptHeader.scala b/akka-http-core/src/main/scala/akka/http/model/parser/AcceptHeader.scala index 0e58662d5f..00c58e8823 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/AcceptHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/AcceptHeader.scala @@ -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) diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala index 5d9053d490..d4cbd6d128 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala @@ -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)) } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala b/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala index 217cd78e79..fe0522d369 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/SimpleHeaders.scala @@ -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`(_)) } diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/UriParser.scala b/akka-http-core/src/main/scala/akka/http/model/parser/UriParser.scala index 49d3d44813..cc1fb6911e 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/UriParser.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/UriParser.scala @@ -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) diff --git a/akka-http-core/src/main/scala/akka/http/parsing/HttpHeaderParser.scala b/akka-http-core/src/main/scala/akka/http/parsing/HttpHeaderParser.scala index 641518e08a..d5618732e3 100644 --- a/akka-http-core/src/main/scala/akka/http/parsing/HttpHeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/parsing/HttpHeaderParser.scala @@ -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 ⇒ diff --git a/akka-http-core/src/main/scala/akka/http/util/EnhancedString.scala b/akka-http-core/src/main/scala/akka/http/util/EnhancedString.scala index 690fdec892..7cf6baecbf 100644 --- a/akka-http-core/src/main/scala/akka/http/util/EnhancedString.scala +++ b/akka-http-core/src/main/scala/akka/http/util/EnhancedString.scala @@ -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) } diff --git a/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala b/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala index 01280aabcd..97463ecf7e 100644 --- a/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala +++ b/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala @@ -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)) } diff --git a/akka-http-core/src/test/scala/akka/http/model/TurkishISpec.scala b/akka-http-core/src/test/scala/akka/http/model/TurkishISpec.scala new file mode 100644 index 0000000000..83355ba135 --- /dev/null +++ b/akka-http-core/src/test/scala/akka/http/model/TurkishISpec.scala @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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) + } + } + } +}