!htp #18479 defer parsing of query key-value-pairs

This commit is contained in:
2beaucoup 2015-10-13 08:30:06 +02:00
parent 1378fedad0
commit 8f75c97e47
25 changed files with 398 additions and 275 deletions

View file

@ -4,20 +4,13 @@
package docs.http.javadsl.server; package docs.http.javadsl.server;
import akka.http.javadsl.model.ContentTypes;
import akka.http.javadsl.model.FormData; import akka.http.javadsl.model.FormData;
import akka.http.javadsl.model.HttpRequest; import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.headers.RawHeader;
import akka.http.javadsl.server.Marshallers;
import akka.http.javadsl.server.RequestVal;
import akka.http.javadsl.server.Route; import akka.http.javadsl.server.Route;
import akka.http.javadsl.server.values.FormField; import akka.http.javadsl.server.values.FormField;
import akka.http.javadsl.server.values.FormFields; import akka.http.javadsl.server.values.FormFields;
import akka.http.javadsl.server.values.Headers;
import akka.http.javadsl.testkit.JUnitRouteTest; import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.japi.Pair; import akka.japi.Pair;
import docs.http.scaladsl.server.directives.Person;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
public class FormFieldRequestValsExampleTest extends JUnitRouteTest { public class FormFieldRequestValsExampleTest extends JUnitRouteTest {

View file

@ -6,7 +6,6 @@ package docs.http.javadsl.server;
import akka.actor.ActorSystem; import akka.actor.ActorSystem;
import akka.dispatch.OnFailure; import akka.dispatch.OnFailure;
import akka.http.impl.util.JavaMapping;
import akka.http.impl.util.Util; import akka.http.impl.util.Util;
import akka.http.javadsl.Http; import akka.http.javadsl.Http;
import akka.http.javadsl.IncomingConnection; import akka.http.javadsl.IncomingConnection;
@ -17,9 +16,7 @@ import akka.http.javadsl.model.HttpMethods;
import akka.http.javadsl.model.HttpRequest; import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse; import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.model.Uri; import akka.http.javadsl.model.Uri;
import akka.http.scaladsl.model.*;
import akka.http.scaladsl.model.HttpEntity; import akka.http.scaladsl.model.HttpEntity;
import akka.japi.JavaPartialFunction;
import akka.japi.function.Function; import akka.japi.function.Function;
import akka.japi.function.Procedure; import akka.japi.function.Procedure;
import akka.stream.ActorMaterializer; import akka.stream.ActorMaterializer;
@ -32,7 +29,6 @@ import akka.stream.stage.PushStage;
import akka.stream.stage.SyncDirective; import akka.stream.stage.SyncDirective;
import akka.stream.stage.TerminationDirective; import akka.stream.stage.TerminationDirective;
import akka.util.ByteString; import akka.util.ByteString;
import scala.Function1;
import scala.concurrent.Await; import scala.concurrent.Await;
import scala.concurrent.Future; import scala.concurrent.Future;
import scala.concurrent.duration.FiniteDuration; import scala.concurrent.duration.FiniteDuration;
@ -206,7 +202,7 @@ public class HttpServerExampleDocTest {
.withEntity(ContentTypes.TEXT_HTML, .withEntity(ContentTypes.TEXT_HTML,
"<html><body>Hello world!</body></html>"); "<html><body>Hello world!</body></html>");
else if (uri.path().equals("/hello")) { else if (uri.path().equals("/hello")) {
String name = Util.getOrElse(uri.parameter("name"), "Mister X"); String name = Util.getOrElse(uri.query().get("name"), "Mister X");
return return
HttpResponse.create() HttpResponse.create()

View file

@ -199,7 +199,7 @@ class BasicDirectivesExamplesSpec extends RoutingSpec {
"textract" in { "textract" in {
val pathAndQuery = textract { ctx => val pathAndQuery = textract { ctx =>
val uri = ctx.request.uri val uri = ctx.request.uri
(uri.path, uri.query) (uri.path, uri.query())
} }
val route = val route =
pathAndQuery { (p, query) => pathAndQuery { (p, query) =>

View file

@ -4,34 +4,51 @@
package akka.http.javadsl.model; package akka.http.javadsl.model;
import akka.http.impl.model.parser.CharacterClasses;
import akka.http.impl.util.JavaMapping;
import akka.http.impl.util.StringRendering;
import akka.http.scaladsl.model.Uri.Query;
import akka.http.scaladsl.model.UriRendering;
import akka.japi.Pair; import akka.japi.Pair;
import java.util.Map;
/** /**
* Simple model for `application/x-www-form-urlencoded` form data. * Simple model for `application/x-www-form-urlencoded` form data.
*/ */
public abstract class FormData { public final class FormData {
public abstract Query fields(); private final Query fields;
public FormData(Query fields) {
this.fields = fields;
}
/**
* Converts this FormData to a RequestEntity using UTF8 encoding.
*/
public RequestEntity toEntity() { public RequestEntity toEntity() {
return toEntity(HttpCharsets.UTF_8); return toEntity(HttpCharsets.UTF_8);
} }
/**
* Converts this FormData to a RequestEntity using the given encoding.
*/
public RequestEntity toEntity(HttpCharset charset) { public RequestEntity toEntity(HttpCharset charset) {
// TODO this logic is duplicated in scaladsl.model.FormData, spent hours trying to DRY it but compiler freaked out in a number of ways... -- ktoso return HttpEntities.create(ContentType.create(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED, charset), fields.render(charset));
final akka.http.scaladsl.model.HttpCharset c = (akka.http.scaladsl.model.HttpCharset) charset;
final StringRendering render = (StringRendering) UriRendering.renderQuery(new StringRendering(), this.fields(), c.nioCharset(), CharacterClasses.unreserved());
return HttpEntities.create(ContentType.create(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED, charset), render.get());
} }
/**
* Returns empty FormData.
*/
public static final FormData EMPTY = new FormData(Query.EMPTY);
/**
* Creates the FormData from the given parameters.
*/
@SafeVarargs @SafeVarargs
public static FormData create(Pair<String, String>... fields) { public static FormData create(Pair<String, String>... params) {
return akka.http.scaladsl.model.FormData.create(fields); return new FormData(Query.create(params));
} }
/**
* Creates the FormData from the given parameters.
*/
public static FormData create(Map<String, String> params) {
return new FormData(Query.create(params));
}
} }

View file

@ -47,14 +47,14 @@ public abstract class Host {
/** /**
* Parse the given Host string using the given charset and the default parsing-mode. * Parse the given Host string using the given charset and the default parsing-mode.
*/ */
public static Host create(String string, Charset charset) { public static Host create(String string, Uri.ParsingMode parsingMode) {
return UriJavaAccessor.hostApply(string, charset); return UriJavaAccessor.hostApply(string, parsingMode);
} }
/** /**
* Parse the given Host string using the given charset and parsing-mode. * Parse the given Host string using the given charset and parsing-mode.
*/ */
public static Host create(String string, Charset charset, Uri.ParsingMode parsingMode) { public static Host create(String string, Charset charset, Uri.ParsingMode parsingMode) {
return UriJavaAccessor.hostApply(string, charset, parsingMode); return akka.http.scaladsl.model.Uri.Host$.MODULE$.apply(string, charset, parsingMode);
} }
} }

View file

@ -4,6 +4,8 @@
package akka.http.javadsl.model; package akka.http.javadsl.model;
import java.nio.charset.Charset;
/** /**
* Represents a charset in Http. See {@link HttpCharsets} for a set of predefined charsets and * Represents a charset in Http. See {@link HttpCharsets} for a set of predefined charsets and
* static constructors to create custom charsets. * static constructors to create custom charsets.
@ -37,4 +39,9 @@ public abstract class HttpCharset {
* Returns the predefined alias names for this charset. * Returns the predefined alias names for this charset.
*/ */
public abstract Iterable<String> getAliases(); public abstract Iterable<String> getAliases();
/**
* Returns the Charset for this charset if available or throws an exception otherwise.
*/
public abstract Charset nioCharset();
} }

View file

@ -5,7 +5,6 @@
package akka.http.javadsl.model; package akka.http.javadsl.model;
import akka.japi.Option; import akka.japi.Option;
import akka.stream.javadsl.Source;
import akka.util.ByteString; import akka.util.ByteString;
import java.io.File; import java.io.File;

View file

@ -0,0 +1,108 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.javadsl.model;
import akka.http.impl.model.JavaQuery;
import akka.http.scaladsl.model.*;
import akka.japi.Option;
import akka.japi.Pair;
import akka.parboiled2.CharPredicate;
import akka.parboiled2.ParserInput$;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
public abstract class Query {
/**
* Returns the value of the first parameter with the given key if it exists.
*/
public abstract Option<String> get(String key);
/**
* Returns the value of the first parameter with the given key or the provided default value.
*/
public abstract String getOrElse(String key, String _default);
/**
* Returns the value of all parameters with the given key.
*/
public abstract List<String> getAll(String key);
/**
* Returns a `List` of all parameters of this Query. Use the `toMap()`
* method to filter out entries with duplicated keys.
*/
public abstract List<Pair<String, String>> toList();
/**
* Returns a key/value map of the parameters of this Query. Use
* the `toList()` method to return all parameters if keys may occur
* multiple times.
*/
public abstract Map<String, String> toMap();
/**
* Returns a `Map` of all parameters of this Query. Use the `toMap()`
* method to filter out entries with duplicated keys.
*/
public abstract Map<String, List<String>> toMultiMap();
/**
* Returns a copy of this instance with a query parameter added.
*/
public abstract Query withParam(String key, String value);
/**
* Renders this Query into its string representation using the given charset.
*/
public abstract String render(HttpCharset charset);
/**
* Renders this Query into its string representation using the given charset and char predicate.
*/
public abstract String render(HttpCharset charset, CharPredicate keep);
/**
* Returns an empty Query.
*/
public static final Query EMPTY = new JavaQuery(UriJavaAccessor.emptyQuery());
/**
* Returns a Query created by parsing the given undecoded string representation.
*/
public static Query create(String rawQuery) {
return new JavaQuery(akka.http.scaladsl.model.Uri.Query$.MODULE$.apply(rawQuery));
}
/**
* Returns a Query created by parsing the given undecoded string representation with the provided parsing mode.
*/
public static Query create(String rawQuery, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) {
return new JavaQuery(UriJavaAccessor.queryApply(rawQuery, parsingMode));
}
/**
* Returns a Query created by parsing the given undecoded string representation with the provided charset and parsing mode.
*/
public static Query create(String rawQuery, Charset charset, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) {
return new JavaQuery(akka.http.scaladsl.model.Uri.Query$.MODULE$.apply(ParserInput$.MODULE$.apply(rawQuery), charset, parsingMode));
}
/**
* Returns a Query from the given parameters.
*/
@SafeVarargs
public static Query create(Pair<String, String>... params) {
return new JavaQuery(UriJavaAccessor.queryApply(params));
}
/**
* Returns a Query from the given parameters.
*/
public static Query create(Map<String, String> params) {
return new JavaQuery(UriJavaAccessor.queryApply(params));
}
}

View file

@ -4,12 +4,14 @@
package akka.http.javadsl.model; package akka.http.javadsl.model;
import akka.http.impl.util.JavaAccessors$; import akka.http.impl.model.JavaUri;
import akka.http.scaladsl.model.UriJavaAccessor; import akka.http.scaladsl.model.UriJavaAccessor;
import akka.japi.Option; import akka.japi.Option;
import akka.japi.Pair;
import akka.parboiled2.ParserInput$; import akka.parboiled2.ParserInput$;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -62,32 +64,24 @@ public abstract class Uri {
public abstract Iterable<String> pathSegments(); public abstract Iterable<String> pathSegments();
/** /**
* Returns a String representation of the query of this Uri. * Returns a decoded String representation of the query of this Uri.
*/ */
public abstract String queryString(); public abstract Option<String> queryString(Charset charset);
/** /**
* Looks up a query parameter of this Uri. * Returns an undecoded String representation of the query of this Uri.
*/ */
public abstract Option<String> parameter(String key); public abstract Option<String> rawQueryString();
/** /**
* Returns if the query of this Uri contains a parameter with the given key. * Returns the parsed Query instance of this Uri.
*/ */
public abstract boolean containsParameter(String key); public abstract Query query();
/** /**
* Returns an `Iterable` of all query parameters of this Uri. Use the `parameterMap()` * Returns the parsed Query instance of this Uri using the given charset and parsing mode.
* method to filter out entries with duplicated keys.
*/ */
public abstract Iterable<Map.Entry<String, String>> parameters(); public abstract Query query(Charset charset, akka.http.scaladsl.model.Uri.ParsingMode mode);
/**
* Returns a key/value map of the query parameters of this Uri. Use
* the `parameters()` method to return all parameters if keys may occur
* multiple times.
*/
public abstract Map<String, String> parameterMap();
/** /**
* Returns the fragment part of this Uri. * Returns the fragment part of this Uri.
@ -132,18 +126,18 @@ public abstract class Uri {
/** /**
* Returns a copy of this instance with a new query. * Returns a copy of this instance with a new query.
*/ */
public abstract Uri query(String query); public abstract Uri rawQueryString(String rawQuery);
/**
* Returns a copy of this instance with a new query.
*/
public abstract Uri query(Query query);
/** /**
* Returns a copy of this instance that is relative. * Returns a copy of this instance that is relative.
*/ */
public abstract Uri toRelative(); public abstract Uri toRelative();
/**
* Returns a copy of this instance with a query parameter added.
*/
public abstract Uri addParameter(String key, String value);
/** /**
* Returns a copy of this instance with a new fragment. * Returns a copy of this instance with a new fragment.
*/ */
@ -156,34 +150,31 @@ public abstract class Uri {
public static final akka.http.scaladsl.model.Uri.ParsingMode STRICT = UriJavaAccessor.pmStrict(); public static final akka.http.scaladsl.model.Uri.ParsingMode STRICT = UriJavaAccessor.pmStrict();
public static final akka.http.scaladsl.model.Uri.ParsingMode RELAXED = UriJavaAccessor.pmRelaxed(); public static final akka.http.scaladsl.model.Uri.ParsingMode RELAXED = UriJavaAccessor.pmRelaxed();
public static final akka.http.scaladsl.model.Uri.ParsingMode RELAXED_WITH_RAW_QUERY = UriJavaAccessor.pmRelaxedWithRawQuery();
/** /**
* Creates a default Uri to be modified using the modification methods. * Creates a default Uri to be modified using the modification methods.
*/ */
public static Uri create() { public static final Uri EMPTY = new JavaUri(akka.http.scaladsl.model.Uri.Empty$.MODULE$);
return JavaAccessors$.MODULE$.Uri(akka.http.scaladsl.model.Uri.Empty$.MODULE$);
}
/** /**
* Returns a Uri created by parsing the given string representation. * Returns a Uri created by parsing the given string representation.
*/ */
public static Uri create(String uri) { public static Uri create(String uri) {
return JavaAccessors$.MODULE$.Uri(akka.http.scaladsl.model.Uri.apply(uri)); return new JavaUri(akka.http.scaladsl.model.Uri.apply(uri));
} }
/** /**
* Returns a Uri created by parsing the given string representation and parsing-mode. * Returns a Uri created by parsing the given string representation with the provided parsing mode.
*/ */
public static Uri create(String uri, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) { public static Uri create(String uri, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) {
return JavaAccessors$.MODULE$.Uri(akka.http.scaladsl.model.Uri.apply(ParserInput$.MODULE$.apply(uri), parsingMode)); return new JavaUri(akka.http.scaladsl.model.Uri.apply(ParserInput$.MODULE$.apply(uri), parsingMode));
} }
/** /**
* Returns a Uri created by parsing the given string representation, charset, and parsing-mode. * Returns a Uri created by parsing the given string representation with the provided charset and parsing mode.
*/ */
public static Uri create(String uri, Charset charset, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) { public static Uri create(String uri, Charset charset, akka.http.scaladsl.model.Uri.ParsingMode parsingMode) {
return JavaAccessors$.MODULE$.Uri(akka.http.scaladsl.model.Uri.apply(ParserInput$.MODULE$.apply(uri), charset, parsingMode)); return new JavaUri(akka.http.scaladsl.model.Uri.apply(ParserInput$.MODULE$.apply(uri), charset, parsingMode));
} }
} }

View file

@ -277,10 +277,6 @@ akka.http {
# #
# `relaxed`: all visible 7-Bit ASCII chars are allowed # `relaxed`: all visible 7-Bit ASCII chars are allowed
# #
# `relaxed-with-raw-query`: like `relaxed` but additionally
# the URI query is not parsed, but delivered as one raw string
# as the `key` value of a single Query structure element.
#
uri-parsing-mode = strict uri-parsing-mode = strict
# Enables/disables the logging of warning messages in case an incoming # Enables/disables the logging of warning messages in case an incoming

View file

@ -0,0 +1,33 @@
/**
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.impl.model
import java.{ util ju }
import akka.http.impl.model.parser.CharacterClasses
import akka.http.impl.util.StringRendering
import akka.http.javadsl.model.HttpCharset
import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.model.UriRendering
import akka.http.scaladsl.{ model sm }
import akka.japi.{ Pair, Option }
import akka.parboiled2.CharPredicate
import scala.collection.JavaConverters._
import akka.http.impl.util.JavaMapping.Implicits._
/** INTERNAL API */
case class JavaQuery(query: sm.Uri.Query) extends jm.Query {
override def get(key: String): Option[String] = query.get(key)
override def toMap: ju.Map[String, String] = query.toMap.asJava
override def toList: ju.List[Pair[String, String]] = query.map(_.asJava).asJava
override def getOrElse(key: String, _default: String): String = query.getOrElse(key, _default)
override def toMultiMap: ju.Map[String, ju.List[String]] = query.toMultiMap.map { case (k, v) (k, v.asJava) }.asJava
override def getAll(key: String): ju.List[String] = query.getAll(key).asJava
override def toString = query.toString
override def withParam(key: String, value: String): jm.Query = jm.Query.create(query.map(_.asJava) :+ Pair(key, value): _*)
override def render(charset: HttpCharset): String =
UriRendering.renderQuery(new StringRendering, query, charset.nioCharset, CharacterClasses.unreserved).get
override def render(charset: HttpCharset, keep: CharPredicate): String = render(charset, keep)
}

View file

@ -4,7 +4,9 @@
package akka.http.impl.model package akka.http.impl.model
import java.{ util ju, lang jl } import java.nio.charset.Charset
import java.{ lang jl }
import akka.http.scaladsl.model.Uri.ParsingMode
import akka.japi.Option import akka.japi.Option
import akka.http.javadsl.{ model jm } import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.{ model sm } import akka.http.scaladsl.{ model sm }
@ -23,27 +25,21 @@ case class JavaUri(uri: sm.Uri) extends jm.Uri {
def path(): String = uri.path.toString def path(): String = uri.path.toString
import collection.JavaConverters._
def pathSegments(): jl.Iterable[String] = { def pathSegments(): jl.Iterable[String] = {
import sm.Uri.Path import sm.Uri.Path._
import Path._ def gatherSegments(path: sm.Uri.Path): List[String] = path match {
def gatherSegments(path: Path): List[String] = path match {
case Empty Nil case Empty Nil
case Segment(head, tail) head :: gatherSegments(tail) case Segment(head, tail) head :: gatherSegments(tail)
case Slash(tail) gatherSegments(tail) case Slash(tail) gatherSegments(tail)
} }
import collection.JavaConverters._
gatherSegments(uri.path).asJava gatherSegments(uri.path).asJava
} }
def queryString(): String = uri.query.toString def rawQueryString: Option[String] = uri.rawQueryString
def queryString(charset: Charset): Option[String] = uri.queryString(charset)
def parameterMap(): ju.Map[String, String] = uri.query.toMap.asJava def query: jm.Query = uri.query().asJava
def parameters(): jl.Iterable[Param] = def query(charset: Charset, mode: ParsingMode): jm.Query = uri.query(charset, mode).asJava
uri.query.map { case (k, v) new ju.AbstractMap.SimpleImmutableEntry(k, v): Param }.toIterable.asJava
def containsParameter(key: String): Boolean = uri.query.get(key).isDefined
def parameter(key: String): Option[String] = uri.query.get(key)
type Param = ju.Map.Entry[String, String]
def fragment: Option[String] = uri.fragment def fragment: Option[String] = uri.fragment
@ -62,16 +58,13 @@ case class JavaUri(uri: sm.Uri) extends jm.Uri {
def toRelative: jm.Uri = t(_.toRelative) def toRelative: jm.Uri = t(_.toRelative)
def query(query: String): jm.Uri = t(_.withQuery(query)) def rawQueryString(rawQuery: String): jm.Uri = t(_.withRawQueryString(rawQuery))
def addParameter(key: String, value: String): jm.Uri = t { u def query(query: jm.Query): jm.Uri = t(_.withQuery(query.asScala))
u.withQuery(((key -> value) +: u.query.reverse).reverse)
}
def addPathSegment(segment: String): jm.Uri = t { u def addPathSegment(segment: String): jm.Uri = t { u
import sm.Uri.Path
val newPath = val newPath =
if (u.path.endsWithSlash) u.path ++ Path(segment) if (u.path.endsWithSlash) u.path ++ sm.Uri.Path(segment)
else u.path ++ Path./(segment) else u.path ++ sm.Uri.Path./(segment)
u.withPath(newPath) u.withPath(newPath)
} }

View file

@ -23,7 +23,7 @@ private[http] class UriParser(val input: ParserInput,
def parseAbsoluteUri(): Uri = def parseAbsoluteUri(): Uri =
rule(`absolute-URI` ~ EOI).run() match { rule(`absolute-URI` ~ EOI).run() match {
case Right(_) => create(_scheme, _userinfo, _host, _port, collapseDotSegments(_path), _query, _fragment) case Right(_) => create(_scheme, _userinfo, _host, _port, collapseDotSegments(_path), _rawQueryString, _fragment)
case Left(error) => fail(error, "absolute URI") case Left(error) => fail(error, "absolute URI")
} }
@ -35,7 +35,7 @@ private[http] class UriParser(val input: ParserInput,
def parseAndResolveUriReference(base: Uri): Uri = def parseAndResolveUriReference(base: Uri): Uri =
rule(`URI-reference` ~ EOI).run() match { rule(`URI-reference` ~ EOI).run() match {
case Right(_) => resolve(_scheme, _userinfo, _host, _port, _path, _query, _fragment, base) case Right(_) => resolve(_scheme, _userinfo, _host, _port, _path, _rawQueryString, _fragment, base)
case Left(error) => fail(error, "URI reference") case Left(error) => fail(error, "URI reference")
} }
@ -53,7 +53,7 @@ private[http] class UriParser(val input: ParserInput,
def parseQuery(): Query = def parseQuery(): Query =
rule(query ~ EOI).run() match { rule(query ~ EOI).run() match {
case Right(_) => _query case Right(query) => query
case Left(error) => fail(error, "query") case Left(error) => fail(error, "query")
} }
@ -69,7 +69,6 @@ private[http] class UriParser(val input: ParserInput,
private[this] val `query-char` = uriParsingMode match { private[this] val `query-char` = uriParsingMode match {
case Uri.ParsingMode.Strict `strict-query-char` case Uri.ParsingMode.Strict `strict-query-char`
case Uri.ParsingMode.Relaxed `relaxed-query-char` case Uri.ParsingMode.Relaxed `relaxed-query-char`
case Uri.ParsingMode.RelaxedWithRawQuery `raw-query-char`
} }
private[this] val `fragment-char` = uriParsingMode match { private[this] val `fragment-char` = uriParsingMode match {
case Uri.ParsingMode.Strict `query-fragment-char` case Uri.ParsingMode.Strict `query-fragment-char`
@ -81,12 +80,12 @@ private[http] class UriParser(val input: ParserInput,
var _host: Host = Host.Empty var _host: Host = Host.Empty
var _port: Int = 0 var _port: Int = 0
var _path: Path = Path.Empty var _path: Path = Path.Empty
var _query: Query = Query.Empty var _rawQueryString: Option[String] = None
var _fragment: Option[String] = None var _fragment: Option[String] = None
// http://tools.ietf.org/html/rfc3986#appendix-A // http://tools.ietf.org/html/rfc3986#appendix-A
def URI = rule { scheme ~ ':' ~ `hier-part` ~ optional('?' ~ query) ~ optional('#' ~ fragment) } def URI = rule { scheme ~ ':' ~ `hier-part` ~ optional('?' ~ rawQueryString) ~ optional('#' ~ fragment) }
def origin = rule { scheme ~ ':' ~ '/' ~ '/' ~ hostAndPort } def origin = rule { scheme ~ ':' ~ '/' ~ '/' ~ hostAndPort }
@ -100,9 +99,9 @@ private[http] class UriParser(val input: ParserInput,
def `URI-reference-pushed`: Rule1[Uri] = rule { `URI-reference` ~ push(createUriReference()) } def `URI-reference-pushed`: Rule1[Uri] = rule { `URI-reference` ~ push(createUriReference()) }
def `absolute-URI` = rule { scheme ~ ':' ~ `hier-part` ~ optional('?' ~ query) } def `absolute-URI` = rule { scheme ~ ':' ~ `hier-part` ~ optional('?' ~ rawQueryString) }
def `relative-ref` = rule { `relative-part` ~ optional('?' ~ query) ~ optional('#' ~ fragment) } def `relative-ref` = rule { `relative-part` ~ optional('?' ~ rawQueryString) ~ optional('#' ~ fragment) }
def `relative-part` = rule( def `relative-part` = rule(
'/' ~ '/' ~ authority ~ `path-abempty` '/' ~ '/' ~ authority ~ `path-abempty`
@ -161,7 +160,12 @@ private[http] class UriParser(val input: ParserInput,
def pchar = rule { `path-segment-char` ~ appendSB() | `pct-encoded` } def pchar = rule { `path-segment-char` ~ appendSB() | `pct-encoded` }
def query = { def rawQueryString = rule {
clearSB() ~ oneOrMore(`raw-query-char` ~ appendSB()) ~ run(_rawQueryString = Some(sb.toString)) | run(_rawQueryString = Some(""))
}
// http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
def query: Rule1[Query] = {
def part = rule( def part = rule(
clearSBForDecoding() ~ oneOrMore('+' ~ appendSB(' ') | `query-char` ~ appendSB() | `pct-encoded`) ~ push(getDecodedString()) clearSBForDecoding() ~ oneOrMore('+' ~ appendSB(' ') | `query-char` ~ appendSB() | `pct-encoded`) ~ push(getDecodedString())
| push("")) | push(""))
@ -176,9 +180,7 @@ private[http] class UriParser(val input: ParserInput,
} }
} }
if (uriParsingMode == Uri.ParsingMode.RelaxedWithRawQuery) rule { rule { keyValuePairs }
clearSB() ~ oneOrMore(`query-char` ~ appendSB()) ~ run(_query = Query.Raw(sb.toString)) | run(_query = Query.Empty)
} else rule { keyValuePairs ~> (_query = _) }
} }
def fragment = rule( def fragment = rule(
@ -201,7 +203,7 @@ private[http] class UriParser(val input: ParserInput,
// http://tools.ietf.org/html/rfc7230#section-5.3 // http://tools.ietf.org/html/rfc7230#section-5.3
def `request-target` = rule( def `request-target` = rule(
`absolute-path` ~ optional('?' ~ query) // origin-form `absolute-path` ~ optional('?' ~ rawQueryString) // origin-form
| `absolute-URI` // absolute-form | `absolute-URI` // absolute-form
| authority) // authority-form or asterisk-form | authority) // authority-form or asterisk-form
@ -209,7 +211,7 @@ private[http] class UriParser(val input: ParserInput,
rule(`request-target` ~ EOI).run() match { rule(`request-target` ~ EOI).run() match {
case Right(_) => case Right(_) =>
val path = if (_scheme.isEmpty) _path else collapseDotSegments(_path) val path = if (_scheme.isEmpty) _path else collapseDotSegments(_path)
create(_scheme, _userinfo, _host, _port, path, _query, _fragment) create(_scheme, _userinfo, _host, _port, path, _rawQueryString, _fragment)
case Left(error) => fail(error, "request-target") case Left(error) => fail(error, "request-target")
} }
@ -231,7 +233,7 @@ private[http] class UriParser(val input: ParserInput,
private def createUriReference(): Uri = { private def createUriReference(): Uri = {
val path = if (_scheme.isEmpty) _path else collapseDotSegments(_path) val path = if (_scheme.isEmpty) _path else collapseDotSegments(_path)
create(_scheme, _userinfo, _host, _port, path, _query, _fragment) create(_scheme, _userinfo, _host, _port, path, _rawQueryString, _fragment)
} }
} }

View file

@ -7,7 +7,6 @@ package akka.http.impl.util
import java.io.File import java.io.File
import JavaMapping.Implicits._ import JavaMapping.Implicits._
import akka.http.impl.model.JavaUri
import akka.http.javadsl.model._ import akka.http.javadsl.model._
import akka.http.scaladsl.model import akka.http.scaladsl.model
@ -26,9 +25,6 @@ object JavaAccessors {
/** INTERNAL API */ /** INTERNAL API */
def HttpResponse(): HttpResponse = model.HttpResponse() def HttpResponse(): HttpResponse = model.HttpResponse()
/** INTERNAL API */
def Uri(uri: model.Uri): Uri = JavaUri(uri)
/** INTERNAL API */ /** INTERNAL API */
def HttpEntity(contentType: ContentType, file: File): UniversalEntity = def HttpEntity(contentType: ContentType, file: File): UniversalEntity =
model.HttpEntity(contentType.asScala, file) model.HttpEntity(contentType.asScala, file)

View file

@ -6,7 +6,6 @@ package akka.http.impl.util
import java.net.InetAddress import java.net.InetAddress
import java.{ util ju, lang jl } import java.{ util ju, lang jl }
import akka.http.scaladsl.model.ws.Message
import akka.japi.Pair import akka.japi.Pair
import akka.stream.javadsl import akka.stream.javadsl
import akka.stream.scaladsl import akka.stream.scaladsl
@ -14,7 +13,7 @@ import akka.stream.scaladsl
import scala.collection.immutable import scala.collection.immutable
import scala.reflect.ClassTag import scala.reflect.ClassTag
import akka.japi import akka.japi
import akka.http.impl.model.JavaUri import akka.http.impl.model.{ JavaQuery, JavaUri }
import akka.http.javadsl.{ model jm } import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.{ model sm } import akka.http.scaladsl.{ model sm }
@ -30,7 +29,7 @@ private[http] trait J2SMapping[J] {
private[http] object J2SMapping { private[http] object J2SMapping {
implicit def fromJavaMapping[J](implicit mapping: JavaMapping[J, _]): J2SMapping[J] { type S = mapping.S } = mapping implicit def fromJavaMapping[J](implicit mapping: JavaMapping[J, _]): J2SMapping[J] { type S = mapping.S } = mapping
implicit def seqMapping[J](implicit mapping: J2SMapping[J]): J2SMapping[Seq[J]] { type S = immutable.Seq[mapping.S] } = implicit def fromJavaSeqMapping[J](implicit mapping: J2SMapping[J]): J2SMapping[Seq[J]] { type S = immutable.Seq[mapping.S] } =
new J2SMapping[Seq[J]] { new J2SMapping[Seq[J]] {
type S = immutable.Seq[mapping.S] type S = immutable.Seq[mapping.S]
def toScala(javaObject: Seq[J]): S = javaObject.map(mapping.toScala(_)).toList def toScala(javaObject: Seq[J]): S = javaObject.map(mapping.toScala(_)).toList
@ -45,7 +44,7 @@ private[http] trait S2JMapping[S] {
/** INTERNAL API */ /** INTERNAL API */
private[http] object S2JMapping { private[http] object S2JMapping {
implicit def fromJavaMapping[S](implicit mapping: JavaMapping[_, S]): S2JMapping[S] { type J = mapping.J } = mapping implicit def fromScalaMapping[S](implicit mapping: JavaMapping[_, S]): S2JMapping[S] { type J = mapping.J } = mapping
} }
/** INTERNAL API */ /** INTERNAL API */
@ -92,7 +91,7 @@ private[http] object JavaMapping {
new JavaMapping[jl.Iterable[_J], immutable.Seq[_S]] { new JavaMapping[jl.Iterable[_J], immutable.Seq[_S]] {
import collection.JavaConverters._ import collection.JavaConverters._
def toJava(scalaObject: immutable.Seq[_S]): jl.Iterable[_J] = scalaObject.map(mapping.toJava(_)).asJavaCollection def toJava(scalaObject: immutable.Seq[_S]): jl.Iterable[_J] = scalaObject.map(mapping.toJava).asJavaCollection
def toScala(javaObject: jl.Iterable[_J]): immutable.Seq[_S] = def toScala(javaObject: jl.Iterable[_J]): immutable.Seq[_S] =
Implicits.convertSeqToScala(iterableAsScalaIterableConverter(javaObject).asScala.toSeq) Implicits.convertSeqToScala(iterableAsScalaIterableConverter(javaObject).asScala.toSeq)
} }
@ -104,24 +103,24 @@ private[http] object JavaMapping {
} }
implicit def option[_J, _S](implicit mapping: JavaMapping[_J, _S]): JavaMapping[akka.japi.Option[_J], Option[_S]] = implicit def option[_J, _S](implicit mapping: JavaMapping[_J, _S]): JavaMapping[akka.japi.Option[_J], Option[_S]] =
new JavaMapping[akka.japi.Option[_J], Option[_S]] { new JavaMapping[akka.japi.Option[_J], Option[_S]] {
def toScala(javaObject: japi.Option[_J]): Option[_S] = javaObject.asScala.map(mapping.toScala(_)) def toScala(javaObject: japi.Option[_J]): Option[_S] = javaObject.asScala.map(mapping.toScala)
def toJava(scalaObject: Option[_S]): japi.Option[_J] = japi.Option.fromScalaOption(scalaObject.map(mapping.toJava(_))) def toJava(scalaObject: Option[_S]): japi.Option[_J] = japi.Option.fromScalaOption(scalaObject.map(mapping.toJava))
} }
implicit def flowMapping[JIn, SIn, JOut, SOut, M](implicit inMapping: JavaMapping[JIn, SIn], outMapping: JavaMapping[JOut, SOut]): JavaMapping[javadsl.Flow[JIn, JOut, M], scaladsl.Flow[SIn, SOut, M]] = implicit def flowMapping[JIn, SIn, JOut, SOut, M](implicit inMapping: JavaMapping[JIn, SIn], outMapping: JavaMapping[JOut, SOut]): JavaMapping[javadsl.Flow[JIn, JOut, M], scaladsl.Flow[SIn, SOut, M]] =
new JavaMapping[javadsl.Flow[JIn, JOut, M], scaladsl.Flow[SIn, SOut, M]] { new JavaMapping[javadsl.Flow[JIn, JOut, M], scaladsl.Flow[SIn, SOut, M]] {
def toScala(javaObject: javadsl.Flow[JIn, JOut, M]): S = def toScala(javaObject: javadsl.Flow[JIn, JOut, M]): S =
scaladsl.Flow[SIn].map(inMapping.toJava(_)).viaMat(javaObject)(scaladsl.Keep.right).map(outMapping.toScala(_)) scaladsl.Flow[SIn].map(inMapping.toJava).viaMat(javaObject)(scaladsl.Keep.right).map(outMapping.toScala)
def toJava(scalaObject: scaladsl.Flow[SIn, SOut, M]): J = def toJava(scalaObject: scaladsl.Flow[SIn, SOut, M]): J =
javadsl.Flow.fromGraph { javadsl.Flow.fromGraph {
scaladsl.Flow[JIn].map(inMapping.toScala(_)).viaMat(scalaObject)(scaladsl.Keep.right).map(outMapping.toJava(_)) scaladsl.Flow[JIn].map(inMapping.toScala).viaMat(scalaObject)(scaladsl.Keep.right).map(outMapping.toJava)
} }
} }
def scalaToJavaAdapterFlow[J, S](implicit mapping: JavaMapping[J, S]): scaladsl.Flow[S, J, Unit] = def scalaToJavaAdapterFlow[J, S](implicit mapping: JavaMapping[J, S]): scaladsl.Flow[S, J, Unit] =
scaladsl.Flow[S].map(mapping.toJava(_)) scaladsl.Flow[S].map(mapping.toJava)
def javaToScalaAdapterFlow[J, S](implicit mapping: JavaMapping[J, S]): scaladsl.Flow[J, S, Unit] = def javaToScalaAdapterFlow[J, S](implicit mapping: JavaMapping[J, S]): scaladsl.Flow[J, S, Unit] =
scaladsl.Flow[J].map(mapping.toScala(_)) scaladsl.Flow[J].map(mapping.toScala)
def adapterBidiFlow[JIn, SIn, SOut, JOut](implicit inMapping: JavaMapping[JIn, SIn], outMapping: JavaMapping[JOut, SOut]): scaladsl.BidiFlow[JIn, SIn, SOut, JOut, Unit] = def adapterBidiFlow[JIn, SIn, SOut, JOut](implicit inMapping: JavaMapping[JIn, SIn], outMapping: JavaMapping[JOut, SOut]): scaladsl.BidiFlow[JIn, SIn, SOut, JOut, Unit] =
scaladsl.BidiFlow.fromFlowsMat(javaToScalaAdapterFlow(inMapping), scalaToJavaAdapterFlow(outMapping))(scaladsl.Keep.none) scaladsl.BidiFlow.fromFlowsMat(javaToScalaAdapterFlow(inMapping), scalaToJavaAdapterFlow(outMapping))(scaladsl.Keep.none)
@ -132,8 +131,8 @@ private[http] object JavaMapping {
} }
implicit def tryMapping[_J, _S](implicit mapping: JavaMapping[_J, _S]): JavaMapping[Try[_J], Try[_S]] = implicit def tryMapping[_J, _S](implicit mapping: JavaMapping[_J, _S]): JavaMapping[Try[_J], Try[_S]] =
new JavaMapping[Try[_J], Try[_S]] { new JavaMapping[Try[_J], Try[_S]] {
def toScala(javaObject: Try[_J]): S = javaObject.map(mapping.toScala(_)) def toScala(javaObject: Try[_J]): S = javaObject.map(mapping.toScala)
def toJava(scalaObject: Try[_S]): J = scalaObject.map(mapping.toJava(_)) def toJava(scalaObject: Try[_S]): J = scalaObject.map(mapping.toJava)
} }
implicit object StringIdentity extends Identity[String] implicit object StringIdentity extends Identity[String]
@ -198,12 +197,17 @@ private[http] object JavaMapping {
implicit object WsMessage extends JavaMapping[jm.ws.Message, sm.ws.Message] { implicit object WsMessage extends JavaMapping[jm.ws.Message, sm.ws.Message] {
def toScala(javaObject: J): WsMessage.S = javaObject.asScala def toScala(javaObject: J): WsMessage.S = javaObject.asScala
def toJava(scalaObject: Message): WsMessage.J = jm.ws.Message.adapt(scalaObject) def toJava(scalaObject: S): WsMessage.J = jm.ws.Message.adapt(scalaObject)
} }
implicit object Uri extends JavaMapping[jm.Uri, sm.Uri] { implicit object Uri extends JavaMapping[jm.Uri, sm.Uri] {
def toScala(javaObject: jm.Uri): Uri.S = cast[JavaUri](javaObject).uri def toScala(javaObject: J): Uri.S = cast[JavaUri](javaObject).uri
def toJava(scalaObject: sm.Uri): Uri.J = JavaAccessors.Uri(scalaObject) def toJava(scalaObject: S): Uri.J = JavaUri(scalaObject)
}
implicit object Query extends JavaMapping[jm.Query, sm.Uri.Query] {
def toScala(javaObject: J): Query.S = cast[JavaQuery](javaObject).query
def toJava(scalaObject: S): Query.J = JavaQuery(scalaObject)
} }
private def cast[T](obj: AnyRef)(implicit classTag: ClassTag[T]): T = private def cast[T](obj: AnyRef)(implicit classTag: ClassTag[T]): T =

View file

@ -13,12 +13,11 @@ import akka.http.scaladsl.model.MediaTypes._
/** /**
* Simple model for `application/x-www-form-urlencoded` form data. * Simple model for `application/x-www-form-urlencoded` form data.
*/ */
final case class FormData(fields: Uri.Query) extends jm.FormData { final case class FormData(fields: Uri.Query) {
override def toEntity: akka.http.scaladsl.model.RequestEntity = def toEntity: akka.http.scaladsl.model.RequestEntity =
toEntity(HttpCharsets.`UTF-8`) toEntity(HttpCharsets.`UTF-8`)
def toEntity(charset: HttpCharset): akka.http.scaladsl.model.RequestEntity = { def toEntity(charset: HttpCharset): akka.http.scaladsl.model.RequestEntity = {
// TODO this logic is duplicated in javadsl.model.FormData, spent hours trying to DRY it but compiler freaked out in a number of ways... -- ktoso
val render: StringRendering = UriRendering.renderQuery(new StringRendering, this.fields, charset.nioCharset, CharacterClasses.unreserved) val render: StringRendering = UriRendering.renderQuery(new StringRendering, this.fields, charset.nioCharset, CharacterClasses.unreserved)
HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), render.get) HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), render.get)
} }
@ -32,7 +31,4 @@ object FormData {
def apply(fields: (String, String)*): FormData = def apply(fields: (String, String)*): FormData =
if (fields.isEmpty) Empty else FormData(Uri.Query(fields: _*)) if (fields.isEmpty) Empty else FormData(Uri.Query(fields: _*))
def create(fields: Array[akka.japi.Pair[String, String]]): FormData =
if (fields.isEmpty) Empty else FormData(Uri.Query(fields.map(_.toScala): _*))
} }

View file

@ -5,7 +5,6 @@
package akka.http.scaladsl.model package akka.http.scaladsl.model
import java.lang.{ Iterable JIterable } import java.lang.{ Iterable JIterable }
import akka.http.impl.util
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ Future, ExecutionContext } import scala.concurrent.{ Future, ExecutionContext }
@ -14,7 +13,6 @@ import scala.reflect.{ classTag, ClassTag }
import akka.parboiled2.CharUtils import akka.parboiled2.CharUtils
import akka.stream.Materializer import akka.stream.Materializer
import akka.util.ByteString import akka.util.ByteString
import akka.http.impl.model.JavaUri
import akka.http.impl.util._ import akka.http.impl.util._
import akka.http.javadsl.{ model jm } import akka.http.javadsl.{ model jm }
import akka.http.scaladsl.util.FastFuture._ import akka.http.scaladsl.util.FastFuture._
@ -267,10 +265,11 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET,
override def withUri(path: String): HttpRequest = withUri(Uri(path)) override def withUri(path: String): HttpRequest = withUri(Uri(path))
def withUri(uri: Uri): HttpRequest = copy(uri = uri) def withUri(uri: Uri): HttpRequest = copy(uri = uri)
import JavaMapping.Implicits._
/** Java API */ /** Java API */
override def getUri: jm.Uri = util.JavaAccessors.Uri(uri) override def getUri: jm.Uri = uri.asJava
/** Java API */ /** Java API */
override def withUri(relativeUri: akka.http.javadsl.model.Uri): HttpRequest = copy(uri = relativeUri.asInstanceOf[JavaUri].uri) override def withUri(uri: jm.Uri): HttpRequest = copy(uri = uri.asScala)
} }
object HttpRequest { object HttpRequest {

View file

@ -22,13 +22,26 @@ import Uri._
* An immutable model of an internet URI as defined by http://tools.ietf.org/html/rfc3986. * An immutable model of an internet URI as defined by http://tools.ietf.org/html/rfc3986.
* All members of this class represent the *decoded* URI elements (i.e. without percent-encoding). * All members of this class represent the *decoded* URI elements (i.e. without percent-encoding).
*/ */
sealed abstract case class Uri(scheme: String, authority: Authority, path: Path, query: Query, sealed abstract case class Uri(scheme: String, authority: Authority, path: Path, rawQueryString: Option[String],
fragment: Option[String]) { fragment: Option[String]) {
def isAbsolute: Boolean = !isRelative def isAbsolute: Boolean = !isRelative
def isRelative: Boolean = scheme.isEmpty def isRelative: Boolean = scheme.isEmpty
def isEmpty: Boolean def isEmpty: Boolean
/**
* Parses the rawQueryString member into a Query instance.
*/
def query(charset: Charset = UTF8, mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Query = rawQueryString match {
case Some(q) new UriParser(q, charset, mode).parseQuery()
case None Query.Empty
}
/**
* Returns the query part of the Uri in its decoded form.
*/
def queryString(charset: Charset = UTF8): Option[String] = rawQueryString.map(s decode(s, charset))
/** /**
* The effective port of this Uri given the currently set authority and scheme values. * The effective port of this Uri given the currently set authority and scheme values.
* If the authority has an explicitly set port (i.e. a non-zero port value) then this port * If the authority has an explicitly set port (i.e. a non-zero port value) then this port
@ -40,8 +53,8 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
* Returns a copy of this Uri with the given components. * Returns a copy of this Uri with the given components.
*/ */
def copy(scheme: String = scheme, authority: Authority = authority, path: Path = path, def copy(scheme: String = scheme, authority: Authority = authority, path: Path = path,
query: Query = query, fragment: Option[String] = fragment): Uri = rawQueryString: Option[String] = rawQueryString, fragment: Option[String] = fragment): Uri =
Uri(scheme, authority, path, query, fragment) Uri(scheme, authority, path, rawQueryString, fragment)
/** /**
* Returns a copy of this Uri with the given scheme. The `scheme` change of the Uri has the following * Returns a copy of this Uri with the given scheme. The `scheme` change of the Uri has the following
@ -95,22 +108,12 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
/** /**
* Returns a copy of this Uri with the given query. * Returns a copy of this Uri with the given query.
*/ */
def withQuery(query: Query): Uri = copy(query = query) def withQuery(query: Query): Uri = copy(rawQueryString = if (query.isEmpty) None else Some(query.toString))
/** /**
* Returns a copy of this Uri with a Query created using the given query string. * Returns a copy of this Uri with a Query created using the given query string.
*/ */
def withQuery(query: String): Uri = copy(query = Query(query)) def withRawQueryString(rawQuery: String): Uri = copy(rawQueryString = Some(rawQuery))
/**
* Returns a copy of this Uri with a Query created using the given (key, value) tuples.
*/
def withQuery(kvp: (String, String)*): Uri = copy(query = Query(kvp: _*))
/**
* Returns a copy of this Uri with a Query created using the given map.
*/
def withQuery(map: Map[String, String]): Uri = copy(query = Query(map))
/** /**
* Returns a copy of this Uri with the given fragment. * Returns a copy of this Uri with the given fragment.
@ -123,7 +126,7 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
* The given base Uri must be absolute. * The given base Uri must be absolute.
*/ */
def resolvedAgainst(base: Uri): Uri = def resolvedAgainst(base: Uri): Uri =
resolve(scheme, authority.userinfo, authority.host, authority.port, path, query, fragment, base) resolve(scheme, authority.userinfo, authority.host, authority.port, path, rawQueryString, fragment, base)
/** /**
* Converts this URI to an "effective HTTP request URI" as defined by * Converts this URI to an "effective HTTP request URI" as defined by
@ -131,14 +134,14 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
*/ */
def toEffectiveHttpRequestUri(hostHeaderHost: Host, hostHeaderPort: Int, securedConnection: Boolean = false, def toEffectiveHttpRequestUri(hostHeaderHost: Host, hostHeaderPort: Int, securedConnection: Boolean = false,
defaultAuthority: Authority = Authority.Empty): Uri = defaultAuthority: Authority = Authority.Empty): Uri =
effectiveHttpRequestUri(scheme, authority.host, authority.port, path, query, fragment, securedConnection, effectiveHttpRequestUri(scheme, authority.host, authority.port, path, rawQueryString, fragment, securedConnection,
hostHeaderHost, hostHeaderPort, defaultAuthority) hostHeaderHost, hostHeaderPort, defaultAuthority)
/** /**
* Converts this URI into a relative URI by keeping the path, query and fragment, but dropping the scheme and authority. * Converts this URI into a relative URI by keeping the path, query and fragment, but dropping the scheme and authority.
*/ */
def toRelative = def toRelative =
Uri(path = if (path.isEmpty) Uri.Path./ else path, query = query, fragment = fragment) Uri(path = if (path.isEmpty) Uri.Path./ else path, queryString = rawQueryString, fragment = fragment)
/** /**
* Converts this URI into an HTTP request target "origin-form" as defined by * Converts this URI into an HTTP request target "origin-form" as defined by
@ -148,7 +151,7 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
* be a "relative" URI with a part component starting with a double slash.) * be a "relative" URI with a part component starting with a double slash.)
*/ */
def toHttpRequestTargetOriginForm = def toHttpRequestTargetOriginForm =
create("", Authority.Empty, if (path.isEmpty) Uri.Path./ else path, query, None) create("", Authority.Empty, if (path.isEmpty) Uri.Path./ else path, rawQueryString, None)
/** /**
* Drops the fragment from this URI * Drops the fragment from this URI
@ -160,7 +163,7 @@ sealed abstract case class Uri(scheme: String, authority: Authority, path: Path,
} }
object Uri { object Uri {
object Empty extends Uri("", Authority.Empty, Path.Empty, Query.Empty, None) { object Empty extends Uri("", Authority.Empty, Path.Empty, None, None) {
def isEmpty = true def isEmpty = true
} }
@ -210,13 +213,13 @@ object Uri {
* http://tools.ietf.org/html/rfc3986 the method throws an `IllegalUriException`. * http://tools.ietf.org/html/rfc3986 the method throws an `IllegalUriException`.
*/ */
def apply(scheme: String = "", authority: Authority = Authority.Empty, path: Path = Path.Empty, def apply(scheme: String = "", authority: Authority = Authority.Empty, path: Path = Path.Empty,
query: Query = Query.Empty, fragment: Option[String] = None): Uri = { queryString: Option[String] = None, fragment: Option[String] = None): Uri = {
val p = verifyPath(path, scheme, authority.host) val p = verifyPath(path, scheme, authority.host)
create( create(
scheme = normalizeScheme(scheme), scheme = normalizeScheme(scheme),
authority = authority.normalizedFor(scheme), authority = authority.normalizedFor(scheme),
path = if (scheme.isEmpty) p else collapseDotSegments(p), path = if (scheme.isEmpty) p else collapseDotSegments(p),
query = query, queryString = queryString,
fragment = fragment) fragment = fragment)
} }
@ -227,9 +230,9 @@ object Uri {
* http://tools.ietf.org/html/rfc3986 the method throws an `IllegalUriException`. * http://tools.ietf.org/html/rfc3986 the method throws an `IllegalUriException`.
*/ */
def from(scheme: String = "", userinfo: String = "", host: String = "", port: Int = 0, path: String = "", def from(scheme: String = "", userinfo: String = "", host: String = "", port: Int = 0, path: String = "",
query: Query = Query.Empty, fragment: Option[String] = None, queryString: Option[String] = None, fragment: Option[String] = None,
mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Uri = mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Uri =
apply(scheme, Authority(Host(host, UTF8, mode), normalizePort(port, scheme), userinfo), Path(path), query, fragment) apply(scheme, Authority(Host(host, UTF8, mode), normalizePort(port, scheme), userinfo), Path(path), queryString, fragment)
/** /**
* Parses a string into a normalized absolute URI as defined by http://tools.ietf.org/html/rfc3986#section-4.3. * Parses a string into a normalized absolute URI as defined by http://tools.ietf.org/html/rfc3986#section-4.3.
@ -279,7 +282,7 @@ object Uri {
* Converts a set of URI components to an "effective HTTP request URI" as defined by * Converts a set of URI components to an "effective HTTP request URI" as defined by
* http://tools.ietf.org/html/rfc7230#section-5.5. * http://tools.ietf.org/html/rfc7230#section-5.5.
*/ */
def effectiveHttpRequestUri(scheme: String, host: Host, port: Int, path: Path, query: Query, fragment: Option[String], def effectiveHttpRequestUri(scheme: String, host: Host, port: Int, path: Path, query: Option[String], fragment: Option[String],
securedConnection: Boolean, hostHeaderHost: Host, hostHeaderPort: Int, securedConnection: Boolean, hostHeaderHost: Host, hostHeaderPort: Int,
defaultAuthority: Authority = Authority.Empty): Uri = { defaultAuthority: Authority = Authority.Empty): Uri = {
var _scheme = scheme var _scheme = scheme
@ -324,7 +327,7 @@ object Uri {
def inetAddresses: immutable.Seq[InetAddress] def inetAddresses: immutable.Seq[InetAddress]
def equalsIgnoreCase(other: Host): Boolean def equalsIgnoreCase(other: Host): Boolean
override def toString() = UriRendering.HostRenderer.render(new StringRendering, this).get override def toString = UriRendering.HostRenderer.render(new StringRendering, this).get
// default implementations // default implementations
def isNamedHost: Boolean = false def isNamedHost: Boolean = false
@ -520,7 +523,6 @@ object Uri {
sealed abstract class Query extends LinearSeq[(String, String)] with LinearSeqOptimized[(String, String), Query] { sealed abstract class Query extends LinearSeq[(String, String)] with LinearSeqOptimized[(String, String), Query] {
def key: String def key: String
def value: String def value: String
def isRaw: Boolean
def +:(kvp: (String, String)) = Query.Cons(kvp._1, kvp._2, this) def +:(kvp: (String, String)) = Query.Cons(kvp._1, kvp._2, this)
def get(key: String): Option[String] = { def get(key: String): Option[String] = {
@tailrec def g(q: Query): Option[String] = if (q.isEmpty) None else if (q.key == key) Some(q.value) else g(q.tail) @tailrec def g(q: Query): Option[String] = if (q.isEmpty) None else if (q.key == key) Some(q.value) else g(q.tail)
@ -556,18 +558,19 @@ object Uri {
* Parses the given String into a Query instance. * Parses the given String into a Query instance.
* Note that this method will never return Query.Empty, even for the empty String. * Note that this method will never return Query.Empty, even for the empty String.
* Empty strings will be parsed to `("", "") +: Query.Empty` * Empty strings will be parsed to `("", "") +: Query.Empty`
* If you want to allow for Query.Empty creation use the apply overload taking an `Option[String`. * If you want to allow for Query.Empty creation use the apply overload taking an `Option[String]`.
*/ */
def apply(string: String, charset: Charset = UTF8, mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Query = def apply(string: String): Query = apply(string: ParserInput, UTF8, Uri.ParsingMode.Relaxed)
new UriParser(string, charset, mode).parseQuery() def apply(input: ParserInput, charset: Charset = UTF8, mode: Uri.ParsingMode = Uri.ParsingMode.Relaxed): Query =
def apply(input: Option[String]): Query = apply(input, Uri.ParsingMode.Relaxed) new UriParser(input, charset, mode).parseQuery()
def apply(input: Option[String], mode: Uri.ParsingMode): Query = input match { def apply(input: Option[String]): Query = apply(input, UTF8, Uri.ParsingMode.Relaxed)
def apply(input: Option[String], charset: Charset, mode: Uri.ParsingMode): Query = input match {
case None Query.Empty case None Query.Empty
case Some(string) apply(string, mode = mode) case Some(string) apply(string, charset, mode)
} }
def apply(kvp: (String, String)*): Query = def apply(params: (String, String)*): Query =
kvp.foldRight(Query.Empty: Query) { case ((key, value), acc) Cons(key, value, acc) } params.foldRight(Query.Empty: Query) { case ((key, value), acc) Cons(key, value, acc) }
def apply(map: Map[String, String]): Query = apply(map.toSeq: _*) def apply(params: Map[String, String]): Query = apply(params.toSeq: _*)
def newBuilder: mutable.Builder[(String, String), Query] = new mutable.Builder[(String, String), Query] { def newBuilder: mutable.Builder[(String, String), Query] = new mutable.Builder[(String, String), Query] {
val b = mutable.ArrayBuffer.newBuilder[(String, String)] val b = mutable.ArrayBuffer.newBuilder[(String, String)]
@ -579,23 +582,14 @@ object Uri {
case object Empty extends Query { case object Empty extends Query {
def key = throw new NoSuchElementException("key of empty path") def key = throw new NoSuchElementException("key of empty path")
def value = throw new NoSuchElementException("value of empty path") def value = throw new NoSuchElementException("value of empty path")
def isRaw = true
override def isEmpty = true override def isEmpty = true
override def head = throw new NoSuchElementException("head of empty list") override def head = throw new NoSuchElementException("head of empty list")
override def tail = throw new UnsupportedOperationException("tail of empty query") override def tail = throw new UnsupportedOperationException("tail of empty query")
} }
final case class Cons(key: String, value: String, override val tail: Query) extends Query { final case class Cons(key: String, value: String, override val tail: Query) extends Query {
def isRaw = false
override def isEmpty = false override def isEmpty = false
override def head = (key, value) override def head = (key, value)
} }
final case class Raw(value: String) extends Query {
def key = ""
def isRaw = true
override def isEmpty = false
override def head = ("", value)
override def tail = Empty
}
} }
val defaultPorts: Map[String, Int] = val defaultPorts: Map[String, Int] =
@ -608,25 +602,23 @@ object Uri {
object ParsingMode { object ParsingMode {
case object Strict extends ParsingMode case object Strict extends ParsingMode
case object Relaxed extends ParsingMode case object Relaxed extends ParsingMode
case object RelaxedWithRawQuery extends ParsingMode
def apply(string: String): ParsingMode = def apply(string: String): ParsingMode =
string match { string match {
case "strict" Strict case "strict" Strict
case "relaxed" Relaxed case "relaxed" Relaxed
case "relaxed-with-raw-query" RelaxedWithRawQuery case x throw new IllegalArgumentException(x + " is not a legal UriParsingMode")
case x throw new IllegalArgumentException(x + " is not a legal UriParsingMode")
} }
} }
// http://tools.ietf.org/html/rfc3986#section-5.2.2 // http://tools.ietf.org/html/rfc3986#section-5.2.2
private[http] def resolve(scheme: String, userinfo: String, host: Host, port: Int, path: Path, query: Query, private[http] def resolve(scheme: String, userinfo: String, host: Host, port: Int, path: Path, query: Option[String],
fragment: Option[String], base: Uri): Uri = { fragment: Option[String], base: Uri): Uri = {
require(base.isAbsolute, "Resolution base Uri must be absolute") require(base.isAbsolute, "Resolution base Uri must be absolute")
if (scheme.isEmpty) if (scheme.isEmpty)
if (host.isEmpty) if (host.isEmpty)
if (path.isEmpty) { if (path.isEmpty) {
val q = if (query.isEmpty) base.query else query val q = if (query.isEmpty) base.rawQueryString else query
create(base.scheme, base.authority, base.path, q, fragment) create(base.scheme, base.authority, base.path, q, fragment)
} else { } else {
// http://tools.ietf.org/html/rfc3986#section-5.2.3 // http://tools.ietf.org/html/rfc3986#section-5.2.3
@ -747,14 +739,14 @@ object Uri {
private[http] def fail(summary: String, detail: String = "") = throw IllegalUriException(summary, detail) private[http] def fail(summary: String, detail: String = "") = throw IllegalUriException(summary, detail)
private[http] def create(scheme: String, userinfo: String, host: Host, port: Int, path: Path, query: Query, private[http] def create(scheme: String, userinfo: String, host: Host, port: Int, path: Path, queryString: Option[String],
fragment: Option[String]): Uri = fragment: Option[String]): Uri =
create(scheme, Authority(host, normalizePort(port, scheme), userinfo), path, query, fragment) create(scheme, Authority(host, normalizePort(port, scheme), userinfo), path, queryString, fragment)
private[http] def create(scheme: String, authority: Authority, path: Path, query: Query, private[http] def create(scheme: String, authority: Authority, path: Path, queryString: Option[String],
fragment: Option[String]): Uri = fragment: Option[String]): Uri =
if (path.isEmpty && scheme.isEmpty && authority.isEmpty && query.isEmpty && fragment.isEmpty) Empty if (path.isEmpty && scheme.isEmpty && authority.isEmpty && queryString.isEmpty && fragment.isEmpty) Empty
else new Uri(scheme, authority, path, query, fragment) { def isEmpty = false } else new Uri(scheme, authority, path, queryString, fragment) { def isEmpty = false }
} }
object UriRendering { object UriRendering {
@ -803,7 +795,8 @@ object UriRendering {
if (isAbsolute) r ~~ scheme ~~ ':' if (isAbsolute) r ~~ scheme ~~ ':'
renderAuthority(r, authority, path, scheme, charset) renderAuthority(r, authority, path, scheme, charset)
renderPath(r, path, charset, encodeFirstSegmentColons = isRelative) renderPath(r, path, charset, encodeFirstSegmentColons = isRelative)
if (query.nonEmpty) renderQuery(r ~~ '?', query, charset) else r rawQueryString.foreach(r ~~ '?' ~~ _)
r
} }
def renderAuthority[R <: Rendering](r: R, authority: Authority, scheme: String, charset: Charset): r.type = def renderAuthority[R <: Rendering](r: R, authority: Authority, scheme: String, charset: Charset): r.type =
@ -842,7 +835,6 @@ object UriRendering {
if (value ne Query.EmptyValue) r ~~ '=' if (value ne Query.EmptyValue) r ~~ '='
enc(value) enc(value)
append(tail) append(tail)
case Query.Raw(value) r ~~ value
} }
append(query) append(query)
} }
@ -879,11 +871,19 @@ abstract class UriJavaAccessor
* INTERNAL API. * INTERNAL API.
*/ */
object UriJavaAccessor { object UriJavaAccessor {
import collection.JavaConverters._
def hostApply(string: String): Host = Uri.Host(string) def hostApply(string: String): Host = Uri.Host(string)
def hostApply(string: String, charset: Charset): Host = Uri.Host(string, charset) def hostApply(string: String, mode: Uri.ParsingMode): Host = Uri.Host(string, mode = mode)
def hostApply(string: String, charset: Charset, pm: Uri.ParsingMode): Host = Uri.Host(string, charset, pm) def hostApply(string: String, charset: Charset): Host = Uri.Host(string, charset = charset)
def emptyHost: Uri.Host = Uri.Host.Empty def emptyHost: Uri.Host = Uri.Host.Empty
def queryApply(params: Array[akka.japi.Pair[String, String]]): Uri.Query = Uri.Query(params.map(_.toScala): _*)
def queryApply(params: java.util.Map[String, String]): Uri.Query = Uri.Query(params.asScala.toSeq: _*)
def queryApply(string: String, mode: Uri.ParsingMode): Uri.Query = Uri.Query(string, mode = mode)
def queryApply(string: String, charset: Charset): Uri.Query = Uri.Query(string, charset = charset)
def emptyQuery: Uri.Query = Uri.Query.Empty
def pmStrict: Uri.ParsingMode = Uri.ParsingMode.Strict def pmStrict: Uri.ParsingMode = Uri.ParsingMode.Strict
def pmRelaxed: Uri.ParsingMode = Uri.ParsingMode.Relaxed def pmRelaxed: Uri.ParsingMode = Uri.ParsingMode.Relaxed
def pmRelaxedWithRawQuery: Uri.ParsingMode = Uri.ParsingMode.RelaxedWithRawQuery
} }

View file

@ -7,6 +7,7 @@ package akka.http.javadsl.model;
import akka.http.impl.util.Util; import akka.http.impl.util.Util;
import akka.http.javadsl.model.headers.Authorization; import akka.http.javadsl.model.headers.Authorization;
import akka.http.javadsl.model.headers.HttpCredentials; import akka.http.javadsl.model.headers.HttpCredentials;
import akka.japi.Pair;
public class JavaApiTestCases { public class JavaApiTestCases {
/** Builds a request for use on the client side */ /** Builds a request for use on the client side */
@ -22,7 +23,7 @@ public class JavaApiTestCases {
if (request.method() == HttpMethods.GET) { if (request.method() == HttpMethods.GET) {
Uri uri = request.getUri(); Uri uri = request.getUri();
if (uri.path().equals("/hello")) { if (uri.path().equals("/hello")) {
String name = Util.getOrElse(uri.parameter("name"), "Mister X"); String name = Util.getOrElse(uri.query().get("name"), "Mister X");
return return
HttpResponse.create() HttpResponse.create()
@ -56,14 +57,14 @@ public class JavaApiTestCases {
/** Build a uri to send a form */ /** Build a uri to send a form */
public static Uri createUriForOrder(String orderId, String price, String amount) { public static Uri createUriForOrder(String orderId, String price, String amount) {
return Uri.create() return Uri.create("/order").query(
.path("/order") Query.create(
.addParameter("orderId", orderId) Pair.create("orderId", orderId),
.addParameter("price", price) Pair.create("price", price),
.addParameter("amount", amount); Pair.create("amount", amount)));
} }
public static Uri addSessionId(Uri uri) { public static Query addSessionId(Query query) {
return uri.addParameter("session", "abcdefghijkl"); return query.withParam("session", "abcdefghijkl");
} }
} }

View file

@ -25,7 +25,7 @@ import com.typesafe.config.{ Config, ConfigFactory }
import org.scalatest.matchers.Matcher import org.scalatest.matchers.Matcher
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers } import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
import scala.concurrent.{ Await, Future } import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
@ -271,7 +271,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|""" should parseTo( |""" should parseTo(
HttpRequest( HttpRequest(
GET, GET,
"/foobar?q=baz", "/foobar?q=b%61z",
List( List(
`Raw-Request-URI`("/f%6f%6fbar?q=b%61z"), `Raw-Request-URI`("/f%6f%6fbar?q=b%61z"),
Host("ping")), Host("ping")),

View file

@ -576,8 +576,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
} }
"parse with custom uri parsing mode" in { "parse with custom uri parsing mode" in {
val targetUri = Uri("http://example.org/?abc=def=ghi", Uri.ParsingMode.RelaxedWithRawQuery) val targetUri = Uri("http://example.org/?abc=def=ghi", Uri.ParsingMode.Relaxed)
HeaderParser.parseFull("location", "http://example.org/?abc=def=ghi", HeaderParser.Settings(uriParsingMode = Uri.ParsingMode.RelaxedWithRawQuery)) shouldEqual HeaderParser.parseFull("location", "http://example.org/?abc=def=ghi", HeaderParser.Settings(uriParsingMode = Uri.ParsingMode.Relaxed)) shouldEqual
Right(Location(targetUri)) Right(Location(targetUri))
} }
} }

View file

@ -4,6 +4,8 @@
package akka.http.javadsl.model package akka.http.javadsl.model
import akka.japi.Pair
import org.scalatest.{ FreeSpec, MustMatchers } import org.scalatest.{ FreeSpec, MustMatchers }
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
@ -11,9 +13,9 @@ import scala.collection.JavaConverters._
class JavaApiSpec extends FreeSpec with MustMatchers { class JavaApiSpec extends FreeSpec with MustMatchers {
"The Java API should work for" - { "The Java API should work for" - {
"work with Uris" - { "work with Uris" - {
"addParameter" in { "query" in {
Uri.create("/abc") Uri.create("/abc")
.addParameter("name", "paul") must be(Uri.create("/abc?name=paul")) .query(Query.create(Pair.create("name", "paul"))) must be(Uri.create("/abc?name=paul"))
} }
"addSegment" in { "addSegment" in {
Uri.create("/abc") Uri.create("/abc")
@ -38,33 +40,23 @@ class JavaApiSpec extends FreeSpec with MustMatchers {
} }
"access parameterMap" in { "access parameterMap" in {
Uri.create("/abc?name=blub&age=28") Uri.create("/abc?name=blub&age=28")
.parameterMap().asScala must contain allOf ("name" -> "blub", "age" -> "28") .query().toMap.asScala must contain allOf ("name" -> "blub", "age" -> "28")
} }
"access parameters" in { "access parameters" in {
val Seq(param1, param2, param3) = val Seq(param1, param2, param3) =
Uri.create("/abc?name=blub&age=28&name=blub2") Uri.create("/abc?name=blub&age=28&name=blub2")
.parameters.asScala.toSeq .query().toList.asScala.map(_.toScala)
param1.getKey must be("name") param1 must be("name" -> "blub")
param1.getValue must be("blub") param2 must be("age" -> "28")
param3 must be("name" -> "blub2")
param2.getKey must be("age")
param2.getValue must be("28")
param3.getKey must be("name")
param3.getValue must be("blub2")
}
"containsParameter" in {
val uri = Uri.create("/abc?name=blub")
uri.containsParameter("name") must be(true)
uri.containsParameter("age") must be(false)
} }
"access single parameter" in { "access single parameter" in {
val uri = Uri.create("/abc?name=blub") val query = Uri.create("/abc?name=blub").query()
uri.parameter("name") must be(akka.japi.Option.some("blub")) query.get("name") must be(akka.japi.Option.some("blub"))
uri.parameter("age") must be(akka.japi.Option.none) query.get("age") must be(akka.japi.Option.none)
Uri.create("/abc?name=blub&name=blib").parameter("name") must be(akka.japi.Option.some("blub")) Uri.create("/abc?name=blub&name=blib").query.get("name") must be(akka.japi.Option.some("blub"))
} }
} }
} }

View file

@ -55,8 +55,8 @@ class JavaApiTestCaseSpecs extends FreeSpec with MustMatchers {
Uri.create("/order?orderId=123&price=149&amount=42")) Uri.create("/order?orderId=123&price=149&amount=42"))
} }
"addSessionId" in { "addSessionId" in {
val origin = Uri.create("/order?orderId=123") val orderId = Query.create("orderId=123")
JavaApiTestCases.addSessionId(origin) must be(Uri.create("/order?orderId=123&session=abcdefghijkl")) Uri.create("/order").query(JavaApiTestCases.addSessionId(orderId)) must be(Uri.create("/order?orderId=123&session=abcdefghijkl"))
} }
"create HttpsContext" in { "create HttpsContext" in {
import akka.japi.{ Option JOption } import akka.japi.{ Option JOption }

View file

@ -199,9 +199,9 @@ class UriSpec extends WordSpec with Matchers {
def roundTripTo(p: Path, cs: Charset = UTF8) = def roundTripTo(p: Path, cs: Charset = UTF8) =
Matcher[String] { s Matcher[String] { s
val rendering = UriRendering.renderPath(new StringRendering, p, cs).get val rendering = UriRendering.renderPath(new StringRendering, p, cs).get
if (rendering != s) MatchResult(false, s"The path rendered to '$rendering' rather than '$s'", "<?>") if (rendering != s) MatchResult(matches = false, s"The path rendered to '$rendering' rather than '$s'", "<?>")
else if (Path(s, cs) != p) MatchResult(false, s"The string parsed to '${Path(s, cs)}' rather than '$p'", "<?>") else if (Path(s, cs) != p) MatchResult(matches = false, s"The string parsed to '${Path(s, cs)}' rather than '$p'", "<?>")
else MatchResult(true, "<?>", "<?>") else MatchResult(matches = true, "<?>", "<?>")
} }
"" should roundTripTo(Empty) "" should roundTripTo(Empty)
@ -273,28 +273,29 @@ class UriSpec extends WordSpec with Matchers {
"Uri.Query instances" should { "Uri.Query instances" should {
def parser(mode: Uri.ParsingMode): String Query = Query(_, mode = mode) def parser(mode: Uri.ParsingMode): String Query = Query(_, mode = mode)
"be parsed and rendered correctly in strict mode" in { "be parsed correctly in strict mode" in {
val test = parser(Uri.ParsingMode.Strict) val strict = parser(Uri.ParsingMode.Strict)
test("") shouldEqual ("", "") +: Query.Empty strict("") shouldEqual ("", "") +: Query.Empty
test("a") shouldEqual ("a", "") +: Query.Empty strict("a") shouldEqual ("a", "") +: Query.Empty
test("a=") shouldEqual ("a", "") +: Query.Empty strict("a=") shouldEqual ("a", "") +: Query.Empty
test("=a") shouldEqual ("", "a") +: Query.Empty strict("a=+") shouldEqual ("a", " ") +: Query.Empty
test("a&") shouldEqual ("a", "") +: ("", "") +: Query.Empty strict("a=%2B") shouldEqual ("a", "+") +: Query.Empty
a[IllegalUriException] should be thrownBy test("a^=b") strict("=a") shouldEqual ("", "a") +: Query.Empty
strict("a&") shouldEqual ("a", "") +: ("", "") +: Query.Empty
strict("a=%62") shouldEqual ("a", "b") +: Query.Empty
a[IllegalUriException] should be thrownBy strict("a^=b")
} }
"be parsed and rendered correctly in relaxed mode" in { "be parsed correctly in relaxed mode" in {
val test = parser(Uri.ParsingMode.Relaxed) val relaxed = parser(Uri.ParsingMode.Relaxed)
test("") shouldEqual ("", "") +: Query.Empty relaxed("") shouldEqual ("", "") +: Query.Empty
test("a") shouldEqual ("a", "") +: Query.Empty relaxed("a") shouldEqual ("a", "") +: Query.Empty
test("a=") shouldEqual ("a", "") +: Query.Empty relaxed("a=") shouldEqual ("a", "") +: Query.Empty
test("=a") shouldEqual ("", "a") +: Query.Empty relaxed("a=+") shouldEqual ("a", " ") +: Query.Empty
test("a&") shouldEqual ("a", "") +: ("", "") +: Query.Empty relaxed("a=%2B") shouldEqual ("a", "+") +: Query.Empty
test("a^=b") shouldEqual ("a^", "b") +: Query.Empty relaxed("=a") shouldEqual ("", "a") +: Query.Empty
} relaxed("a&") shouldEqual ("a", "") +: ("", "") +: Query.Empty
"be parsed and rendered correctly in relaxed-with-raw-query mode" in { relaxed("a=%62") shouldEqual ("a", "b") +: Query.Empty
val test = parser(Uri.ParsingMode.RelaxedWithRawQuery) relaxed("a^=b") shouldEqual ("a^", "b") +: Query.Empty
test("a^=b&c").toString shouldEqual "a^=b&c"
test("a%2Fb") shouldEqual Uri.Query.Raw("a%2Fb")
} }
"properly support the retrieval interface" in { "properly support the retrieval interface" in {
val query = Query("a=1&b=2&c=3&b=4&b") val query = Query("a=1&b=2&c=3&b=4&b")
@ -341,7 +342,7 @@ class UriSpec extends WordSpec with Matchers {
Uri.from(scheme = "http", host = "www.ietf.org", path = "/rfc/rfc2396.txt") Uri.from(scheme = "http", host = "www.ietf.org", path = "/rfc/rfc2396.txt")
Uri("ldap://[2001:db8::7]/c=GB?objectClass?one") shouldEqual Uri("ldap://[2001:db8::7]/c=GB?objectClass?one") shouldEqual
Uri.from(scheme = "ldap", host = "[2001:db8::7]", path = "/c=GB", query = Query("objectClass?one")) Uri.from(scheme = "ldap", host = "[2001:db8::7]", path = "/c=GB", queryString = Some("objectClass?one"))
Uri("mailto:John.Doe@example.com") shouldEqual Uri("mailto:John.Doe@example.com") shouldEqual
Uri.from(scheme = "mailto", path = "John.Doe@example.com") Uri.from(scheme = "mailto", path = "John.Doe@example.com")
@ -360,15 +361,16 @@ class UriSpec extends WordSpec with Matchers {
// more examples // more examples
Uri("http://") shouldEqual Uri(scheme = "http", authority = Authority(Host.Empty)) Uri("http://") shouldEqual Uri(scheme = "http", authority = Authority(Host.Empty))
Uri("http:?") shouldEqual Uri.from(scheme = "http", query = Query("")) Uri("http:?") shouldEqual Uri.from(scheme = "http", queryString = Some(""))
Uri("?a+b=c%2Bd") shouldEqual Uri.from(query = ("a b", "c+d") +: Query.Empty) Uri("http:") shouldEqual Uri.from(scheme = "http", queryString = None)
Uri("?a+b=c%2Bd").query() shouldEqual ("a b", "c+d") +: Query.Empty
// illegal paths // illegal paths
Uri("foo/another@url/[]and{}") shouldEqual Uri.from(path = "foo/another@url/%5B%5Dand%7B%7D") Uri("foo/another@url/[]and{}") shouldEqual Uri.from(path = "foo/another@url/%5B%5Dand%7B%7D")
a[IllegalUriException] should be thrownBy Uri("foo/another@url/[]and{}", mode = Uri.ParsingMode.Strict) a[IllegalUriException] should be thrownBy Uri("foo/another@url/[]and{}", mode = Uri.ParsingMode.Strict)
// handle query parameters with more than percent-encoded character // handle query parameters with more than percent-encoded character
Uri("?%7Ba%7D=$%7B%7D", UTF8, Uri.ParsingMode.Strict) shouldEqual Uri(query = Query.Cons("{a}", "${}", Query.Empty)) Uri("?%7Ba%7D=$%7B%7D", UTF8, Uri.ParsingMode.Strict).query() shouldEqual Query.Cons("{a}", "${}", Query.Empty)
// don't double decode // don't double decode
Uri("%2520").path.head shouldEqual "%20" Uri("%2520").path.head shouldEqual "%20"
@ -430,13 +432,12 @@ class UriSpec extends WordSpec with Matchers {
normalize("eXAMPLE://a/./b/../b/%63/{foo}/[bar]") shouldEqual "example://a/b/c/%7Bfoo%7D/%5Bbar%5D" normalize("eXAMPLE://a/./b/../b/%63/{foo}/[bar]") shouldEqual "example://a/b/c/%7Bfoo%7D/%5Bbar%5D"
a[IllegalUriException] should be thrownBy normalize("eXAMPLE://a/./b/../b/%63/{foo}/[bar]", mode = Uri.ParsingMode.Strict) a[IllegalUriException] should be thrownBy normalize("eXAMPLE://a/./b/../b/%63/{foo}/[bar]", mode = Uri.ParsingMode.Strict)
// queries // queries and fragments
normalize("?") shouldEqual "?" normalize("?") shouldEqual "?"
normalize("?key") shouldEqual "?key" normalize("?key") shouldEqual "?key"
normalize("?key=") shouldEqual "?key=" normalize("?key=") shouldEqual "?key="
normalize("?key=&a=b") shouldEqual "?key=&a=b" normalize("?key=&a=b") shouldEqual "?key=&a=b"
normalize("?key={}&a=[]") shouldEqual "?key=%7B%7D&a=%5B%5D" normalize("?key={}&a=[]") shouldEqual "?key={}&a=[]"
a[IllegalUriException] should be thrownBy normalize("?key={}&a=[]", mode = Uri.ParsingMode.Strict)
normalize("?=value") shouldEqual "?=value" normalize("?=value") shouldEqual "?=value"
normalize("?key=value") shouldEqual "?key=value" normalize("?key=value") shouldEqual "?key=value"
normalize("?a+b") shouldEqual "?a+b" normalize("?a+b") shouldEqual "?a+b"
@ -456,9 +457,9 @@ class UriSpec extends WordSpec with Matchers {
"support tunneling a URI through a query param" in { "support tunneling a URI through a query param" in {
val uri = Uri("http://aHost/aPath?aParam=aValue#aFragment") val uri = Uri("http://aHost/aPath?aParam=aValue#aFragment")
val q = Query("uri" -> uri.toString) val q = Query("uri" -> uri.toString)
val uri2 = Uri(path = Path./, query = q, fragment = Some("aFragment")).toString val uri2 = Uri(path = Path./, fragment = Some("aFragment")).withQuery(q).toString
uri2 shouldEqual "/?uri=http://ahost/aPath?aParam%3DaValue%23aFragment#aFragment" uri2 shouldEqual "/?uri=http://ahost/aPath?aParam%3DaValue%23aFragment#aFragment"
Uri(uri2).query shouldEqual q Uri(uri2).query() shouldEqual q
Uri(q.getOrElse("uri", "<nope>")) shouldEqual uri Uri(q.getOrElse("uri", "<nope>")) shouldEqual uri
} }
@ -499,10 +500,10 @@ class UriSpec extends WordSpec with Matchers {
} }
// illegal query // illegal query
the[IllegalUriException] thrownBy Uri("?a=b=c") shouldBe { the[IllegalUriException] thrownBy Uri("?a=b=c").query() shouldBe {
IllegalUriException("Illegal URI reference: Invalid input '=', expected '+', query-char, 'EOI', '#', '&' or pct-encoded (line 1, column 5)", IllegalUriException("Illegal query: Invalid input '=', expected '+', query-char, 'EOI', '&' or pct-encoded (line 1, column 4)",
"?a=b=c\n" + "a=b=c\n" +
" ^") " ^")
} }
} }
@ -588,9 +589,8 @@ class UriSpec extends WordSpec with Matchers {
uri.withUserInfo("someInfo") shouldEqual Uri("http://someInfo@host:80/path?query#fragment") uri.withUserInfo("someInfo") shouldEqual Uri("http://someInfo@host:80/path?query#fragment")
uri.withQuery(Query("param1" -> "value1")) shouldEqual Uri("http://host:80/path?param1=value1#fragment") uri.withQuery(Query("param1" -> "value1")) shouldEqual Uri("http://host:80/path?param1=value1#fragment")
uri.withQuery("param1=value1") shouldEqual Uri("http://host:80/path?param1=value1#fragment") uri.withQuery(Query(Map("param1" -> "value1"))) shouldEqual Uri("http://host:80/path?param1=value1#fragment")
uri.withQuery(("param1", "value1")) shouldEqual Uri("http://host:80/path?param1=value1#fragment") uri.withRawQueryString("param1=value1") shouldEqual Uri("http://host:80/path?param1=value1#fragment")
uri.withQuery(Map("param1" -> "value1")) shouldEqual Uri("http://host:80/path?param1=value1#fragment")
uri.withFragment("otherFragment") shouldEqual Uri("http://host:80/path?query#otherFragment") uri.withFragment("otherFragment") shouldEqual Uri("http://host:80/path?query#otherFragment")
} }

View file

@ -57,13 +57,13 @@ object ParameterDirectives extends ParameterDirectives {
import BasicDirectives._ import BasicDirectives._
private val _parameterMap: Directive1[Map[String, String]] = private val _parameterMap: Directive1[Map[String, String]] =
extract(_.request.uri.query.toMap) extract(_.request.uri.query().toMap)
private val _parameterMultiMap: Directive1[Map[String, List[String]]] = private val _parameterMultiMap: Directive1[Map[String, List[String]]] =
extract(_.request.uri.query.toMultiMap) extract(_.request.uri.query().toMultiMap)
private val _parameterSeq: Directive1[immutable.Seq[(String, String)]] = private val _parameterSeq: Directive1[immutable.Seq[(String, String)]] =
extract(_.request.uri.query.toSeq) extract(_.request.uri.query().toSeq)
sealed trait ParamMagnet { sealed trait ParamMagnet {
type Out type Out
@ -109,7 +109,7 @@ object ParameterDirectives extends ParameterDirectives {
extractRequestContext flatMap { ctx extractRequestContext flatMap { ctx
import ctx.executionContext import ctx.executionContext
import ctx.materializer import ctx.materializer
handleParamResult(paramName, fsou(ctx.request.uri.query get paramName)) handleParamResult(paramName, fsou(ctx.request.uri.query().get(paramName)))
} }
implicit def forString(implicit fsu: FSU[String]): ParamDefAux[String, Directive1[String]] = implicit def forString(implicit fsu: FSU[String]): ParamDefAux[String, Directive1[String]] =
extractParameter[String, String] { string filter(string, fsu) } extractParameter[String, String] { string filter(string, fsu) }
@ -134,7 +134,7 @@ object ParameterDirectives extends ParameterDirectives {
extractRequestContext flatMap { ctx extractRequestContext flatMap { ctx
import ctx.executionContext import ctx.executionContext
import ctx.materializer import ctx.materializer
onComplete(fsou(ctx.request.uri.query get paramName)) flatMap { onComplete(fsou(ctx.request.uri.query().get(paramName))) flatMap {
case Success(value) if value == requiredValue pass case Success(value) if value == requiredValue pass
case _ reject case _ reject
} }
@ -150,7 +150,7 @@ object ParameterDirectives extends ParameterDirectives {
extractRequestContext flatMap { ctx extractRequestContext flatMap { ctx
import ctx.executionContext import ctx.executionContext
import ctx.materializer import ctx.materializer
handleParamResult(paramName, Future.sequence(ctx.request.uri.query.getAll(paramName).map(fsu.apply))) handleParamResult(paramName, Future.sequence(ctx.request.uri.query().getAll(paramName).map(fsu.apply)))
} }
implicit def forRepVR[T](implicit fsu: FSU[T]): ParamDefAux[RepeatedValueReceptacle[T], Directive1[Iterable[T]]] = implicit def forRepVR[T](implicit fsu: FSU[T]): ParamDefAux[RepeatedValueReceptacle[T], Directive1[Iterable[T]]] =
extractParameter[RepeatedValueReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, fsu) } extractParameter[RepeatedValueReceptacle[T], Iterable[T]] { rvr repeatedFilter(rvr.name, fsu) }