diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/StatusCode.java b/akka-http-core/src/main/java/akka/http/javadsl/model/StatusCode.java index 28dcb26e0b..0ab20c0219 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/StatusCode.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/StatusCode.java @@ -40,4 +40,9 @@ public abstract class StatusCode { * a non-empty entity. */ public abstract boolean allowsEntity(); + + /** + * Returns if the status-code is a redirection status code. + */ + public abstract boolean isRedirection(); } diff --git a/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala b/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala index 3bede026c3..7796f1d019 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala @@ -6,6 +6,10 @@ package akka.http.impl.util import java.net.InetAddress import java.{ util ⇒ ju, lang ⇒ jl } +import akka.http.scaladsl.model.ws.Message +import akka.stream.javadsl +import akka.stream.scaladsl + import scala.collection.immutable import scala.reflect.ClassTag import akka.japi @@ -52,6 +56,9 @@ object JavaMapping { def asJava: J } + def toJava[J, S](s: S)(implicit mapping: JavaMapping[J, S]): J = mapping.toJava(s) + def toScala[J, S](j: J)(implicit mapping: JavaMapping[J, S]): S = mapping.toScala(j) + object Implicits { import scala.language.implicitConversions @@ -87,6 +94,16 @@ object JavaMapping { 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]] = + new JavaMapping[javadsl.Flow[JIn, JOut, M], scaladsl.Flow[SIn, SOut, M]] { + def toScala(javaObject: javadsl.Flow[JIn, JOut, M]): S = + scaladsl.Flow[SIn].map(inMapping.toJava).viaMat(javaObject)(scaladsl.Keep.right).map(outMapping.toScala) + def toJava(scalaObject: scaladsl.Flow[SIn, SOut, M]): J = + javadsl.Flow.wrap { + scaladsl.Flow[JIn].map(inMapping.toScala).viaMat(scalaObject)(scaladsl.Keep.right).map(outMapping.toJava) + } + } + implicit object StringIdentity extends Identity[String] implicit object LongMapping extends JavaMapping[jl.Long, Long] { @@ -144,6 +161,11 @@ object JavaMapping { implicit object ProductVersion extends Inherited[jm.headers.ProductVersion, sm.headers.ProductVersion] implicit object RangeUnit extends Inherited[jm.headers.RangeUnit, sm.headers.RangeUnit] + implicit object WsMessage extends JavaMapping[jm.ws.Message, sm.ws.Message] { + def toScala(javaObject: J): WsMessage.S = javaObject.asScala + def toJava(scalaObject: Message): WsMessage.J = jm.ws.Message.adapt(scalaObject) + } + implicit object Uri extends JavaMapping[jm.Uri, sm.Uri] { def toScala(javaObject: jm.Uri): Uri.S = cast[JavaUri](javaObject).uri def toJava(scalaObject: sm.Uri): Uri.J = JavaAccessors.Uri(scalaObject) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala index 083c99e6a7..e3bf031ff8 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala @@ -16,6 +16,7 @@ sealed abstract class StatusCode extends jm.StatusCode with LazyValueBytesRender def defaultMessage: String def isSuccess: Boolean def isFailure: Boolean + def isRedirection: Boolean def allowsEntity: Boolean } @@ -35,16 +36,25 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] { sealed protected abstract class HttpFailure extends StatusCode { def isSuccess = false def isFailure = true + def isRedirection: Boolean = false + def allowsEntity = true } // format: OFF final case class Informational private[StatusCodes] (intValue: Int)(val reason: String, - val defaultMessage: String) extends HttpSuccess { def allowsEntity = false } + val defaultMessage: String) extends HttpSuccess { + def allowsEntity = false + def isRedirection: Boolean = false + } final case class Success private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String, - val allowsEntity: Boolean = true) extends HttpSuccess + val allowsEntity: Boolean = true) extends HttpSuccess { + def isRedirection: Boolean = false + } final case class Redirection private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String, - val htmlTemplate: String, val allowsEntity: Boolean = true) extends HttpSuccess + val htmlTemplate: String, val allowsEntity: Boolean = true) extends HttpSuccess { + def isRedirection: Boolean = true + } final case class ClientError private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String) extends HttpFailure final case class ServerError private[StatusCodes] (intValue: Int)(val reason: String, val defaultMessage: String) extends HttpFailure @@ -54,6 +64,7 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] { val isSuccess: Boolean, val allowsEntity: Boolean) extends StatusCode { def isFailure: Boolean = !isSuccess + def isRedirection: Boolean = false } private def reg[T <: StatusCode](code: T): T = { diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ws/UpgradeToWebsocket.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ws/UpgradeToWebsocket.scala index 7b2cbfe5ad..62c65ff10a 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ws/UpgradeToWebsocket.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ws/UpgradeToWebsocket.scala @@ -5,6 +5,8 @@ package akka.http.scaladsl.model.ws import java.lang.Iterable +import akka.http.impl.util.JavaMapping + import scala.collection.immutable import akka.stream import akka.stream.javadsl @@ -63,13 +65,13 @@ trait UpgradeToWebsocket extends jm.ws.UpgradeToWebsocket { * Java API */ def handleMessagesWith(handlerFlow: stream.javadsl.Flow[jm.ws.Message, jm.ws.Message, _]): HttpResponse = - handleMessages(adaptJavaFlow(handlerFlow)) + handleMessages(JavaMapping.toScala(handlerFlow)) /** * Java API */ def handleMessagesWith(handlerFlow: stream.javadsl.Flow[jm.ws.Message, jm.ws.Message, _], subprotocol: String): HttpResponse = - handleMessages(adaptJavaFlow(handlerFlow), subprotocol = Some(subprotocol)) + handleMessages(JavaMapping.toScala(handlerFlow), subprotocol = Some(subprotocol)) /** * Java API @@ -85,8 +87,6 @@ trait UpgradeToWebsocket extends jm.ws.UpgradeToWebsocket { subprotocol: String): HttpResponse = handleMessages(createScalaFlow(inSink, outSource), subprotocol = Some(subprotocol)) - private[this] def adaptJavaFlow(handlerFlow: stream.javadsl.Flow[jm.ws.Message, jm.ws.Message, _]): Flow[Message, Message, Any] = - Flow[Message].map(jm.ws.Message.adapt).via(handlerFlow.asScala).map(_.asScala) private[this] def createScalaFlow(inSink: stream.javadsl.Sink[jm.ws.Message, _], outSource: stream.javadsl.Source[jm.ws.Message, _]): Flow[Message, Message, Any] = - adaptJavaFlow(Flow.wrap(inSink.asScala, outSource.asScala)((_, _) ⇒ ()).asJava) + JavaMapping.toScala(Flow.wrap(inSink.asScala, outSource.asScala)((_, _) ⇒ ()).asJava) } diff --git a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/JUnitRouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/JUnitRouteTest.scala index e9e791d1a7..b71ac71959 100644 --- a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/JUnitRouteTest.scala +++ b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/JUnitRouteTest.scala @@ -4,6 +4,8 @@ package akka.http.javadsl.testkit +import akka.http.javadsl.server._ +import Directives._ import org.junit.rules.ExternalResource import org.junit.{ Rule, Assert } import scala.concurrent.duration._ @@ -35,6 +37,11 @@ abstract class JUnitRouteTestBase extends RouteTest { throw new IllegalStateException("Assertion should have failed") } } + + protected def completeWithValueToString[T](value: RequestVal[T]): Route = + handleWith(value, new Handler1[T] { + def handle(ctx: RequestContext, t: T): RouteResult = ctx.complete(t.toString) + }) } abstract class JUnitRouteTest extends JUnitRouteTestBase { private[this] val _systemResource = new ActorSystemResource diff --git a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala index cf30346798..d917ae4999 100644 --- a/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala +++ b/akka-http-testkit/src/main/scala/akka/http/javadsl/testkit/RouteTest.scala @@ -26,8 +26,12 @@ abstract class RouteTest extends AllDirectives { protected def awaitDuration: FiniteDuration = 500.millis - def runRoute(route: Route, request: HttpRequest): TestResponse = { - val scalaRoute = ScalaRoute.seal(RouteImplementation(route)) + def runRoute(route: Route, request: HttpRequest): TestResponse = + runScalaRoute(ScalaRoute.seal(RouteImplementation(route)), request) + def runRouteUnSealed(route: Route, request: HttpRequest): TestResponse = + runScalaRoute(RouteImplementation(route), request) + + private def runScalaRoute(scalaRoute: ScalaRoute, request: HttpRequest): TestResponse = { val result = scalaRoute(new server.RequestContextImpl(request.asScala, NoLogging, RoutingSettings(system))) result.awaitResult(awaitDuration) match { diff --git a/akka-http-tests-java8/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp8.java b/akka-http-tests-java8/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp8.java index ad7183ff10..5b23a230f3 100644 --- a/akka-http-tests-java8/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp8.java +++ b/akka-http-tests-java8/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp8.java @@ -11,8 +11,8 @@ import akka.http.javadsl.server.values.*; import java.io.IOException; public class SimpleServerApp8 extends HttpApp { - static Parameter x = Parameters.integer("x"); - static Parameter y = Parameters.integer("y"); + static Parameter x = Parameters.intValue("x"); + static Parameter y = Parameters.intValue("y"); static PathMatcher xSegment = PathMatchers.integerNumber(); static PathMatcher ySegment = PathMatchers.integerNumber(); diff --git a/akka-http-tests-java8/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java b/akka-http-tests-java8/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java index b89f17091f..5790232992 100644 --- a/akka-http-tests-java8/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java +++ b/akka-http-tests-java8/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java @@ -11,6 +11,11 @@ import akka.http.javadsl.server.values.*; import static akka.http.javadsl.server.Directives.*; public class HandlerBindingTest extends JUnitRouteTest { + Parameter aParam = Parameters.intValue("a"); + Parameter bParam = Parameters.intValue("b"); + Parameter cParam = Parameters.intValue("c"); + Parameter dParam = Parameters.intValue("d"); + @Test public void testHandlerWithoutExtractions() { Route route = handleWith(ctx -> ctx.complete("Ok")); @@ -19,7 +24,7 @@ public class HandlerBindingTest extends JUnitRouteTest { } @Test public void testHandler1() { - Route route = handleWith(Parameters.integer("a"), (ctx, a) -> ctx.complete("Ok " + a)); + Route route = handleWith(aParam, (ctx, a) -> ctx.complete("Ok " + a)); TestResponse response = runRoute(route, HttpRequest.GET("?a=23")); response.assertStatusCode(200); response.assertEntity("Ok 23"); @@ -28,8 +33,8 @@ public class HandlerBindingTest extends JUnitRouteTest { public void testHandler2() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), + aParam, + bParam, (ctx, a, b) -> ctx.complete("Sum: " + (a + b))); TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42")); response.assertStatusCode(200); @@ -39,9 +44,9 @@ public class HandlerBindingTest extends JUnitRouteTest { public void testHandler3() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), - Parameters.integer("c"), + aParam, + bParam, + cParam, (ctx, a, b, c) -> ctx.complete("Sum: " + (a + b + c))); TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30")); response.assertStatusCode(200); @@ -51,10 +56,10 @@ public class HandlerBindingTest extends JUnitRouteTest { public void testHandler4() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), - Parameters.integer("c"), - Parameters.integer("d"), + aParam, + bParam, + cParam, + dParam, (ctx, a, b, c, d) -> ctx.complete("Sum: " + (a + b + c + d))); TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45")); response.assertStatusCode(200); @@ -67,10 +72,10 @@ public class HandlerBindingTest extends JUnitRouteTest { public void testHandler4MethodRef() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), - Parameters.integer("c"), - Parameters.integer("d"), + aParam, + bParam, + cParam, + dParam, this::sum); TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45")); response.assertStatusCode(200); diff --git a/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp.java b/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp.java index 99ab187589..5c5f5ebc99 100644 --- a/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp.java +++ b/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/simple/SimpleServerApp.java @@ -5,17 +5,20 @@ package akka.http.javadsl.server.examples.simple; import akka.actor.ActorSystem; +import akka.dispatch.Futures; import akka.http.javadsl.server.*; import akka.http.javadsl.server.values.Parameter; import akka.http.javadsl.server.values.Parameters; import akka.http.javadsl.server.values.PathMatcher; import akka.http.javadsl.server.values.PathMatchers; +import scala.concurrent.Future; import java.io.IOException; +import java.util.concurrent.Callable; public class SimpleServerApp extends HttpApp { - static Parameter x = Parameters.integer("x"); - static Parameter y = Parameters.integer("y"); + static Parameter x = Parameters.intValue("x"); + static Parameter y = Parameters.intValue("y"); static PathMatcher xSegment = PathMatchers.integerNumber(); static PathMatcher ySegment = PathMatchers.integerNumber(); @@ -24,6 +27,13 @@ public class SimpleServerApp extends HttpApp { int result = x * y; return ctx.complete(String.format("%d * %d = %d", x, y, result)); } + public static Future multiplyAsync(final RequestContext ctx, final int x, final int y) { + return Futures.future(new Callable() { + public RouteResult call() throws Exception { + return multiply(ctx, x, y); + } + }, ctx.executionContext()); + } @Override public Route createRoute() { @@ -59,6 +69,10 @@ public class SimpleServerApp extends HttpApp { path("multiply", xSegment, ySegment).route( // bind handler by reflection handleWith(SimpleServerApp.class, "multiply", xSegment, ySegment) + ), + path("multiplyAsync", xSegment, ySegment).route( + // bind async handler by reflection + handleWith(SimpleServerApp.class, "multiplyAsync", xSegment, ySegment) ) ); } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/CompleteTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/CompleteTest.java index 299613285a..bc2082b679 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/CompleteTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/CompleteTest.java @@ -46,8 +46,8 @@ public class CompleteTest extends JUnitRouteTest { } @Test public void completeWithFuture() { - Parameter x = Parameters.integer("x"); - Parameter y = Parameters.integer("y"); + Parameter x = Parameters.intValue("x"); + Parameter y = Parameters.intValue("y"); Handler2 slowCalc = new Handler2() { @Override diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java index 94bf68d7be..866e41c96c 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/HandlerBindingTest.java @@ -26,8 +26,8 @@ public class HandlerBindingTest extends JUnitRouteTest { } @Test public void testHandlerWithSomeExtractions() { - final Parameter a = Parameters.integer("a"); - final Parameter b = Parameters.integer("b"); + final Parameter a = Parameters.intValue("a"); + final Parameter b = Parameters.intValue("b"); Route route = handleWith( new Handler() { @@ -42,7 +42,7 @@ public class HandlerBindingTest extends JUnitRouteTest { } @Test public void testHandlerIfExtractionFails() { - final Parameter a = Parameters.integer("a"); + final Parameter a = Parameters.intValue("a"); Route route = handleWith( new Handler() { @@ -58,7 +58,7 @@ public class HandlerBindingTest extends JUnitRouteTest { } @Test public void testHandler1() { - final Parameter a = Parameters.integer("a"); + final Parameter a = Parameters.intValue("a"); Route route = handleWith( a, @@ -76,8 +76,8 @@ public class HandlerBindingTest extends JUnitRouteTest { @Test public void testHandler2() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), + Parameters.intValue("a"), + Parameters.intValue("b"), new Handler2() { @Override public RouteResult handle(RequestContext ctx, Integer a, Integer b) { @@ -92,9 +92,9 @@ public class HandlerBindingTest extends JUnitRouteTest { @Test public void testHandler3() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), - Parameters.integer("c"), + Parameters.intValue("a"), + Parameters.intValue("b"), + Parameters.intValue("c"), new Handler3() { @Override public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c) { @@ -109,10 +109,10 @@ public class HandlerBindingTest extends JUnitRouteTest { @Test public void testHandler4() { Route route = handleWith( - Parameters.integer("a"), - Parameters.integer("b"), - Parameters.integer("c"), - Parameters.integer("d"), + Parameters.intValue("a"), + Parameters.intValue("b"), + Parameters.intValue("c"), + Parameters.intValue("d"), new Handler4() { @Override public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c, Integer d) { @@ -131,7 +131,7 @@ public class HandlerBindingTest extends JUnitRouteTest { return ctx.complete("Negated: " + (- a)); } } - Route route = handleWith(new Test(), "negate", Parameters.integer("a")); + Route route = handleWith(new Test(), "negate", Parameters.intValue("a")); runRoute(route, HttpRequest.GET("?a=23")) .assertStatusCode(200) .assertEntity("Negated: -23"); @@ -142,7 +142,7 @@ public class HandlerBindingTest extends JUnitRouteTest { } @Test public void testStaticReflectiveHandler() { - Route route = handleWith(HandlerBindingTest.class, "squared", Parameters.integer("a")); + Route route = handleWith(HandlerBindingTest.class, "squared", Parameters.intValue("a")); runRoute(route, HttpRequest.GET("?a=23")) .assertStatusCode(200) .assertEntity("Squared: 529"); diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/CodingDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/CodingDirectivesTest.java index 4b725a4a20..81063d5358 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/CodingDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/CodingDirectivesTest.java @@ -11,6 +11,7 @@ import akka.http.javadsl.model.headers.ContentEncoding; import akka.http.javadsl.model.headers.HttpEncodings; import akka.http.javadsl.server.Coder; import akka.stream.ActorMaterializer; +import akka.http.javadsl.server.*; import akka.util.ByteString; import org.junit.*; import scala.concurrent.Await; @@ -86,7 +87,53 @@ public class CodingDirectivesTest extends JUnitRouteTest { } @Test - public void testAutomaticDecoding() {} + public void testAutomaticDecoding() { + TestRoute route = + testRoute( + decodeRequest( + completeWithValueToString(RequestVals.entityAs(Unmarshallers.String())) + ) + ); + + HttpRequest deflateRequest = + HttpRequest.POST("/") + .addHeader(ContentEncoding.create(HttpEncodings.DEFLATE)) + .withEntity(Coder.Deflate.encode(ByteString.fromString("abcdef"))); + route.run(deflateRequest) + .assertStatusCode(200) + .assertEntity("abcdef"); + + HttpRequest gzipRequest = + HttpRequest.POST("/") + .addHeader(ContentEncoding.create(HttpEncodings.GZIP)) + .withEntity(Coder.Gzip.encode(ByteString.fromString("hijklmnopq"))); + route.run(gzipRequest) + .assertStatusCode(200) + .assertEntity("hijklmnopq"); + } @Test - public void testGzipDecoding() {} + public void testGzipDecoding() { + TestRoute route = + testRoute( + decodeRequestWith(Coder.Gzip, + completeWithValueToString(RequestVals.entityAs(Unmarshallers.String())) + ) + ); + + HttpRequest gzipRequest = + HttpRequest.POST("/") + .addHeader(ContentEncoding.create(HttpEncodings.GZIP)) + .withEntity(Coder.Gzip.encode(ByteString.fromString("hijklmnopq"))); + route.run(gzipRequest) + .assertStatusCode(200) + .assertEntity("hijklmnopq"); + + HttpRequest deflateRequest = + HttpRequest.POST("/") + .addHeader(ContentEncoding.create(HttpEncodings.DEFLATE)) + .withEntity(Coder.Deflate.encode(ByteString.fromString("abcdef"))); + route.run(deflateRequest) + .assertStatusCode(400) + .assertEntity("The request's Content-Encoding is not supported. Expected:\ngzip"); + } } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java index ad900046b5..39a1ba7fca 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java @@ -14,8 +14,8 @@ import akka.http.javadsl.testkit.*; public class ExecutionDirectivesTest extends JUnitRouteTest { @Test public void testCatchExceptionThrownFromHandler() { - Parameter a = Parameters.integer("a"); - Parameter b = Parameters.integer("b"); + Parameter a = Parameters.intValue("a"); + Parameter b = Parameters.intValue("b"); Handler2 divide = new Handler2() { @Override diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HostDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HostDirectivesTest.java new file mode 100644 index 0000000000..3def552483 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/HostDirectivesTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.Uri; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import akka.japi.function.Function; +import org.junit.Test; + +import java.util.ArrayList; + +public class HostDirectivesTest extends JUnitRouteTest { + @Test + public void testHostFilterBySingleName() { + TestRoute route = testRoute(host("example.org", complete("OK!"))); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("https://other.org"))) + .assertStatusCode(404); + } + @Test + public void testHostFilterByNames() { + ArrayList hosts = new ArrayList(); + hosts.add("example.org"); + hosts.add("example2.org"); + TestRoute route = testRoute(host(hosts, complete("OK!"))); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example2.org"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("https://other.org"))) + .assertStatusCode(404); + } + @Test + public void testHostFilterByPredicate() { + Function predicate = + new Function(){ + @Override + public Boolean apply(String hostName) throws Exception { + return hostName.contains("ample"); + } + }; + + TestRoute route = testRoute(host(predicate, complete("OK!"))); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("https://other.org"))) + .assertStatusCode(404); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/MiscDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/MiscDirectivesTest.java new file mode 100644 index 0000000000..d0ebd5c683 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/MiscDirectivesTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.Uri; +import akka.http.javadsl.server.RequestContext; +import akka.http.javadsl.server.RequestVal; +import akka.http.javadsl.server.RequestVals; +import akka.http.javadsl.server.values.Parameter; +import akka.http.javadsl.server.values.Parameters; +import akka.http.javadsl.server.values.PathMatchers; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import akka.japi.function.Function; +import akka.japi.function.Function2; +import org.junit.Test; + +public class MiscDirectivesTest extends JUnitRouteTest { + static Parameter stringParam = Parameters.stringValue("stringParam"); + + static Function isShort = + new Function() { + @Override + public Boolean apply(String str) throws Exception { + return str.length() < 5; + } + }; + + @Test + public void testValidateRequestContext() { + Function hasShortPath = + new Function() { + @Override + public Boolean apply(RequestContext ctx) throws Exception { + return ctx.request().getUri().path().toString().length() < 5; + } + }; + + TestRoute route = testRoute(validate(hasShortPath, "Path too long!", complete("OK!"))); + + route + .run(HttpRequest.create().withUri(Uri.create("/abc"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("/abcdefghijkl"))) + .assertStatusCode(400) + .assertEntity("Path too long!"); + } + @Test + public void testValidateOneStandaloneRequestVal() { + TestRoute route = testRoute(validate(stringParam, isShort, "stringParam too long!", complete("OK!"))); + + route + .run(HttpRequest.GET("/?stringParam=abcd")) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.GET("/?stringParam=abcdefg")) + .assertStatusCode(400) + .assertEntity("stringParam too long!"); + } + @Test + public void testValidateOnePathMatcherRequestVal() { + RequestVal nameSegment = PathMatchers.segment(); + + TestRoute route = testRoute( + path("people", nameSegment, "address").route( + validate(nameSegment, isShort, "Segment too long!", complete("OK!")) + ) + ); + + route + .run(HttpRequest.GET("/people/john/address")) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.GET("/people/hermanbaker/address")) + .assertStatusCode(400) + .assertEntity("Segment too long!"); + } + @Test + public void testValidateTwoRequestVals() { + Function2 stringParamEqualsHostName = + new Function2() { + @Override + public Boolean apply(String stringParam, String hostName) throws Exception { + return stringParam.equals(hostName); + } + }; + + TestRoute route = + testRoute( + validate(stringParam, RequestVals.host(), stringParamEqualsHostName, "stringParam must equal hostName!", + complete("OK!"))); + + route + .run(HttpRequest.GET("http://example.org/?stringParam=example.org")) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.GET("http://blubber.org/?stringParam=example.org")) + .assertStatusCode(400) + .assertEntity("stringParam must equal hostName!"); + + route + .run(HttpRequest.GET("http://blubber.org/")) + .assertStatusCode(404); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/PathDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/PathDirectivesTest.java index e4c1e8592d..54ffc0a955 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/PathDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/PathDirectivesTest.java @@ -4,6 +4,7 @@ package akka.http.javadsl.server.directives; +import akka.http.javadsl.model.StatusCodes; import akka.http.javadsl.server.values.PathMatcher; import org.junit.Test; @@ -76,24 +77,140 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("hey", name).route(toStringEcho(name)) + path("hey", name).route(completeWithValueToString(name)) ); route.run(HttpRequest.GET("/hey/jude")) .assertEntity("jude"); } + @Test + public void testPathEnd() { + TestRoute route = + testRoute( + pathPrefix("test").route( + pathEnd().route(complete("end")), + path("abc").route(complete("abc")) + ) + ); + + route.run(HttpRequest.GET("/test")) + .assertEntity("end"); + + route.run(HttpRequest.GET("/test/abc")) + .assertEntity("abc"); + + route.run(HttpRequest.GET("/xyz")) + .assertStatusCode(404); + } @Test public void testSingleSlash() { TestRoute route = testRoute( + pathPrefix("test").route( pathSingleSlash().route(complete("Ok")) + ) ); - route.run(HttpRequest.GET("/")) + route.run(HttpRequest.GET("/test/")) .assertEntity("Ok"); + route.run(HttpRequest.GET("/test")) + .assertStatusCode(404); + } + + @Test + public void testPathEndOrSingleSlash() { + TestRoute route = + testRoute( + pathPrefix("test").route( + pathEndOrSingleSlash().route(complete("Ok")) + ) + ); + + route.run(HttpRequest.GET("/test")) + .assertEntity("Ok"); + + route.run(HttpRequest.GET("/test/")) + .assertEntity("Ok"); + route.run(HttpRequest.GET("/abc")) + .assertStatusCode(404); + } + + @Test + public void testRawPathPrefixTest() { + TestRoute route = + testRoute( + rawPathPrefixTest(PathMatchers.SLASH(), "abc").route( + completeWithValueToString(RequestVals.unmatchedPath()) + ) + ); + + route.run(HttpRequest.GET("/abc")) + .assertEntity("/abc"); + + route.run(HttpRequest.GET("/abc/def")) + .assertEntity("/abc/def"); + + route.run(HttpRequest.GET("/abcd/ef")) + .assertEntity("/abcd/ef"); + + route.run(HttpRequest.GET("/xyz/def")) + .assertStatusCode(404); + } + @Test + public void testPathPrefixTest() { + TestRoute route = + testRoute( + pathPrefixTest("abc").route(completeWithValueToString(RequestVals.unmatchedPath())) + ); + + route.run(HttpRequest.GET("/abc")) + .assertEntity("/abc"); + + route.run(HttpRequest.GET("/abc/def")) + .assertEntity("/abc/def"); + + route.run(HttpRequest.GET("/abcd/ef")) + .assertEntity("/abcd/ef"); + + route.run(HttpRequest.GET("/xyz/def")) + .assertStatusCode(404); + } + @Test + public void testPathSuffix() { + TestRoute route = + testRoute( + pathSuffix(PathMatchers.SLASH(), "abc").route(completeWithValueToString(RequestVals.unmatchedPath())) + ); + + route.run(HttpRequest.GET("/test/abc/")) + .assertEntity("/test/"); + + route.run(HttpRequest.GET("/abc/")) + .assertEntity("/"); + + route.run(HttpRequest.GET("/abc/def")) + .assertStatusCode(404); + + route.run(HttpRequest.GET("/abc")) + .assertStatusCode(404); + } + @Test + public void testPathSuffixTest() { + TestRoute route = + testRoute( + pathSuffixTest("abc").route(completeWithValueToString(RequestVals.unmatchedPath())) + ); + + route.run(HttpRequest.GET("/test/abc")) + .assertEntity("/test/abc"); + + route.run(HttpRequest.GET("/abc")) + .assertEntity("/abc"); + + route.run(HttpRequest.GET("/abc/def")) .assertStatusCode(404); } @@ -103,7 +220,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("age", age).route(toStringEcho(age)) + path("age", age).route(completeWithValueToString(age)) ); route.run(HttpRequest.GET("/age/38")) @@ -112,7 +229,6 @@ public class PathDirectivesTest extends JUnitRouteTest { route.run(HttpRequest.GET("/age/abc")) .assertStatusCode(404); } - @Test public void testTwoVals() { // tests that `x` and `y` have different identities which is important for @@ -122,7 +238,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("multiply", x, y).route( + path("multiply", x, "with", y).route( handleWith(x, y, new Handler2() { @Override public RouteResult handle(RequestContext ctx, Integer x, Integer y) { @@ -132,7 +248,7 @@ public class PathDirectivesTest extends JUnitRouteTest { ) ); - route.run(HttpRequest.GET("/multiply/3/6")) + route.run(HttpRequest.GET("/multiply/3/with/6")) .assertEntity("3 * 6 = 18"); } @@ -142,7 +258,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("color", color).route(toStringEcho(color)) + path("color", color).route(completeWithValueToString(color)) ); route.run(HttpRequest.GET("/color/a0c2ef")) @@ -155,7 +271,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("bigage", bigAge).route(toStringEcho(bigAge)) + path("bigage", bigAge).route(completeWithValueToString(bigAge)) ); route.run(HttpRequest.GET("/bigage/12345678901")) @@ -168,7 +284,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("code", code).route(toStringEcho(code)) + path("code", code).route(completeWithValueToString(code)) ); route.run(HttpRequest.GET("/code/a0b1c2d3e4f5")) @@ -181,7 +297,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("pets", rest).route(toStringEcho(rest)) + path("pets", rest).route(completeWithValueToString(rest)) ); route.run(HttpRequest.GET("/pets/afdaoisd/asda/sfasfasf/asf")) @@ -194,7 +310,7 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("by-uuid", uuid).route(toStringEcho(uuid)) + path("by-uuid", uuid).route(completeWithValueToString(uuid)) ); route.run(HttpRequest.GET("/by-uuid/6ba7b811-9dad-11d1-80b4-00c04fd430c8")) @@ -207,19 +323,42 @@ public class PathDirectivesTest extends JUnitRouteTest { TestRoute route = testRoute( - path("pets", segments).route(toStringEcho(segments)) + path("pets", segments).route(completeWithValueToString(segments)) ); route.run(HttpRequest.GET("/pets/cat/dog")) .assertEntity("[cat, dog]"); } - private Route toStringEcho(RequestVal value) { - return handleWith(value, new Handler1() { - @Override - public RouteResult handle(RequestContext ctx, T t) { - return ctx.complete(t.toString()); - } - }); + @Test + public void testRedirectToTrailingSlashIfMissing() { + TestRoute route = + testRoute( + redirectToTrailingSlashIfMissing(StatusCodes.FOUND, complete("Ok")) + ); + + route.run(HttpRequest.GET("/home")) + .assertStatusCode(302) + .assertHeaderExists("Location", "/home/"); + + route.run(HttpRequest.GET("/home/")) + .assertStatusCode(200) + .assertEntity("Ok"); + } + + @Test + public void testRedirectToNoTrailingSlashIfPresent() { + TestRoute route = + testRoute( + redirectToNoTrailingSlashIfPresent(StatusCodes.FOUND, complete("Ok")) + ); + + route.run(HttpRequest.GET("/home/")) + .assertStatusCode(302) + .assertHeaderExists("Location", "/home"); + + route.run(HttpRequest.GET("/home")) + .assertStatusCode(200) + .assertEntity("Ok"); } } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java new file mode 100644 index 0000000000..e4111b7e32 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/RouteDirectivesTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.model.Uri; +import akka.http.javadsl.model.headers.Location; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +public class RouteDirectivesTest extends JUnitRouteTest { + @Test + public void testRedirection() { + Uri targetUri = Uri.create("http://example.com"); + TestRoute route = + testRoute( + redirect(targetUri, StatusCodes.FOUND) + ); + + route + .run(HttpRequest.create()) + .assertStatusCode(302) + .assertHeaderExists(Location.create(targetUri)); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/SchemeDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/SchemeDirectivesTest.java new file mode 100644 index 0000000000..45fe646a4c --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/SchemeDirectivesTest.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.Uri; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +public class SchemeDirectivesTest extends JUnitRouteTest { + @Test + public void testSchemeFilter() { + TestRoute route = testRoute(scheme("http", complete("OK!"))); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("OK!"); + + route + .run(HttpRequest.create().withUri(Uri.create("https://example.org"))) + .assertStatusCode(400) + .assertEntity("Uri scheme not allowed, supported schemes: http"); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/examples/simple/SimpleServerTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/examples/simple/SimpleServerTest.java index 6c5e23fa5a..5c65cbb947 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/examples/simple/SimpleServerTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/examples/simple/SimpleServerTest.java @@ -19,4 +19,13 @@ public class SimpleServerTest extends JUnitRouteTest { .assertStatusCode(200) .assertEntity("42 + 23 = 65"); } + + @Test + public void testMultiplyAsync() { + TestResponse response = route.run(HttpRequest.GET("/multiplyAsync/42/23")); + + response + .assertStatusCode(200) + .assertEntity("42 * 23 = 966"); + } } diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/CookiesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/CookiesTest.java new file mode 100644 index 0000000000..2bfc8c3400 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/CookiesTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values; + +import akka.http.javadsl.model.DateTime; +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.headers.HttpCookie; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +public class CookiesTest extends JUnitRouteTest { + Cookie userIdCookie = Cookies.create("userId").withDomain("example.com").withPath("/admin"); + + @Test + public void testCookieValue() { + TestRoute route = + testRoute(completeWithValueToString(userIdCookie.value())); + + route.run(HttpRequest.create()) + .assertStatusCode(400) + .assertEntity("Request is missing required cookie 'userId'"); + + route.run(HttpRequest.create().addHeader(akka.http.javadsl.model.headers.Cookie.create("userId", "12345"))) + .assertStatusCode(200) + .assertEntity("12345"); + } + @Test + public void testCookieOptionalValue() { + TestRoute route = + testRoute(completeWithValueToString(userIdCookie.optionalValue())); + + route.run(HttpRequest.create()) + .assertStatusCode(200) + .assertEntity("None"); + + route.run(HttpRequest.create().addHeader(akka.http.javadsl.model.headers.Cookie.create("userId", "12345"))) + .assertStatusCode(200) + .assertEntity("Some(12345)"); + } + @Test + public void testCookieSet() { + TestRoute route = + testRoute( + userIdCookie.set("12").route( + complete("OK!") + ) + ); + + route.run(HttpRequest.create()) + .assertStatusCode(200) + .assertHeaderExists("Set-Cookie", "userId=12; Domain=example.com; Path=/admin") + .assertEntity("OK!"); + } + @Test + public void testDeleteCookie() { + TestRoute route = + testRoute( + userIdCookie.delete( + complete("OK!") + ) + ); + + route.run(HttpRequest.create()) + .assertStatusCode(200) + .assertHeaderExists("Set-Cookie", "userId=deleted; Expires=Wed, 01 Jan 1800 00:00:00 GMT; Domain=example.com; Path=/admin") + .assertEntity("OK!"); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java new file mode 100644 index 0000000000..9ecbe73229 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.MediaTypes; +import akka.http.javadsl.server.RequestVal; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import akka.japi.Option; +import org.junit.Test; + +import java.util.AbstractMap; +import java.util.Map; + +public class FormFieldsTest extends JUnitRouteTest { + static FormField stringParam = FormFields.stringValue("stringParam"); + static FormField byteParam = FormFields.byteValue("byteParam"); + static FormField shortParam = FormFields.shortValue("shortParam"); + static FormField intParam = FormFields.intValue("intParam"); + static FormField longParam = FormFields.longValue("longParam"); + static FormField floatParam = FormFields.floatValue("floatParam"); + static FormField doubleParam = FormFields.doubleValue("doubleParam"); + + static FormField hexByteParam = FormFields.hexByteValue("hexByteParam"); + static FormField hexShortParam = FormFields.hexShortValue("hexShortParam"); + static FormField hexIntParam = FormFields.hexIntValue("hexIntParam"); + static FormField hexLongParam = FormFields.hexLongValue("hexLongParam"); + + static RequestVal nameWithDefault = FormFields.stringValue("nameWithDefault").withDefault("John Doe"); + static RequestVal> optionalIntParam = FormFields.intValue("optionalIntParam").optional(); + + private Map.Entry entry(String name, String value) { + return new AbstractMap.SimpleImmutableEntry(name, value); + } + private HttpRequest urlEncodedRequest(Map.Entry... entries) { + StringBuilder sb = new StringBuilder(); + boolean next = false; + for (Map.Entry entry: entries) { + if (next) { + sb.append('&'); + next = true; + } + sb.append(entry.getKey()); + sb.append('='); + sb.append(entry.getValue()); + } + + return + HttpRequest.POST("/test") + .withEntity(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(), sb.toString()); + } + private HttpRequest singleParameterUrlEncodedRequest(String name, String value) { + return urlEncodedRequest(entry(name, value)); + } + + @Test + public void testStringFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(stringParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'stringParam'"); + + route + .run(singleParameterUrlEncodedRequest("stringParam", "john")) + .assertStatusCode(200) + .assertEntity("john"); + } + + @Test + public void testByteFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(byteParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'byteParam'"); + + route + .run(singleParameterUrlEncodedRequest("byteParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'byteParam' was malformed:\n'test' is not a valid 8-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("byteParam", "1000")) + .assertStatusCode(400) + .assertEntity("The form field 'byteParam' was malformed:\n'1000' is not a valid 8-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("byteParam", "48")) + .assertStatusCode(200) + .assertEntity("48"); + } + + @Test + public void testShortFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(shortParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'shortParam'"); + + route + .run(singleParameterUrlEncodedRequest("shortParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'shortParam' was malformed:\n'test' is not a valid 16-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("shortParam", "100000")) + .assertStatusCode(400) + .assertEntity("The form field 'shortParam' was malformed:\n'100000' is not a valid 16-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("shortParam", "1234")) + .assertStatusCode(200) + .assertEntity("1234"); + } + + @Test + public void testIntegerFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(intParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'intParam'"); + + route + .run(singleParameterUrlEncodedRequest("intParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'intParam' was malformed:\n'test' is not a valid 32-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("intParam", "48")) + .assertStatusCode(200) + .assertEntity("48"); + } + + @Test + public void testLongFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(longParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'longParam'"); + + route + .run(singleParameterUrlEncodedRequest("longParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'longParam' was malformed:\n'test' is not a valid 64-bit signed integer value"); + + route + .run(singleParameterUrlEncodedRequest("longParam", "123456")) + .assertStatusCode(200) + .assertEntity("123456"); + } + + @Test + public void testFloatFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(floatParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'floatParam'"); + + route + .run(singleParameterUrlEncodedRequest("floatParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'floatParam' was malformed:\n'test' is not a valid 32-bit floating point value"); + + route + .run(singleParameterUrlEncodedRequest("floatParam", "48.123")) + .assertStatusCode(200) + .assertEntity("48.123"); + } + + @Test + public void testDoubleFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(doubleParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'doubleParam'"); + + route + .run(singleParameterUrlEncodedRequest("doubleParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'doubleParam' was malformed:\n'test' is not a valid 64-bit floating point value"); + + route + .run(singleParameterUrlEncodedRequest("doubleParam", "0.234213235987")) + .assertStatusCode(200) + .assertEntity("0.234213235987"); + } + + @Test + public void testHexByteFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexByteParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'hexByteParam'"); + + route + .run(singleParameterUrlEncodedRequest("hexByteParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'hexByteParam' was malformed:\n'test' is not a valid 8-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexByteParam", "1000")) + .assertStatusCode(400) + .assertEntity("The form field 'hexByteParam' was malformed:\n'1000' is not a valid 8-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexByteParam", "48")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x48)); + } + + @Test + public void testHexShortFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexShortParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'hexShortParam'"); + + route + .run(singleParameterUrlEncodedRequest("hexShortParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'hexShortParam' was malformed:\n'test' is not a valid 16-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexShortParam", "100000")) + .assertStatusCode(400) + .assertEntity("The form field 'hexShortParam' was malformed:\n'100000' is not a valid 16-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexShortParam", "1234")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x1234)); + } + + @Test + public void testHexIntegerFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexIntParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'hexIntParam'"); + + route + .run(singleParameterUrlEncodedRequest("hexIntParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'hexIntParam' was malformed:\n'test' is not a valid 32-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexIntParam", "12345678")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x12345678)); + } + + @Test + public void testHexLongFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexLongParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(400) + .assertEntity("Request is missing required form field 'hexLongParam'"); + + route + .run(singleParameterUrlEncodedRequest("hexLongParam", "test")) + .assertStatusCode(400) + .assertEntity("The form field 'hexLongParam' was malformed:\n'test' is not a valid 64-bit hexadecimal integer value"); + + route + .run(singleParameterUrlEncodedRequest("hexLongParam", "123456789a")) + .assertStatusCode(200) + .assertEntity(Long.toString(0x123456789aL)); + } + + @Test + public void testOptionalIntFormFieldExtraction() { + TestRoute route = testRoute(completeWithValueToString(optionalIntParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(singleParameterUrlEncodedRequest("optionalIntParam", "23")) + .assertStatusCode(200) + .assertEntity("Some(23)"); + } + + @Test + public void testStringFormFieldExtractionWithDefaultValue() { + TestRoute route = testRoute(completeWithValueToString(nameWithDefault)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("John Doe"); + + route + .run(singleParameterUrlEncodedRequest("nameWithDefault", "paul")) + .assertStatusCode(200) + .assertEntity("paul"); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HeadersTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HeadersTest.java new file mode 100644 index 0000000000..402766c276 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/HeadersTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values; + +import org.junit.Test; + +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; + +import akka.http.javadsl.model.*; +import akka.http.javadsl.model.headers.Age; +import akka.http.javadsl.model.headers.RawHeader; +import akka.http.javadsl.server.values.*; + +public class HeadersTest extends JUnitRouteTest { + Header XTestHeader = Headers.byName("X-Test-Header"); + Header AgeHeader = Headers.byClass(Age.class); + + RawHeader testHeaderInstance = RawHeader.create("X-Test-Header", "abcdef-test"); + Age ageHeaderInstance = Age.create(1000); + + @Test + public void testValueByName() { + TestRoute route = testRoute(completeWithValueToString(XTestHeader.value())); + + route + .run(HttpRequest.create()) + .assertStatusCode(400) + .assertEntity("Request is missing required HTTP header 'X-Test-Header'"); + + route + .run(HttpRequest.create().addHeader(testHeaderInstance)) + .assertStatusCode(200) + .assertEntity("abcdef-test"); + } + @Test + public void testOptionalValueByName() { + TestRoute route = testRoute(completeWithValueToString(XTestHeader.optionalValue())); + + route + .run(HttpRequest.create()) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(HttpRequest.create().addHeader(testHeaderInstance)) + .assertStatusCode(200) + .assertEntity("Some(abcdef-test)"); + } + @Test + public void testInstanceByName() { + TestRoute route = testRoute(completeWithValueToString(XTestHeader.instance())); + + route + .run(HttpRequest.create()) + .assertStatusCode(400) + .assertEntity("Request is missing required HTTP header 'X-Test-Header'"); + + route + .run(HttpRequest.create().addHeader(testHeaderInstance)) + .assertStatusCode(200) + .assertEntity("X-Test-Header: abcdef-test"); + } + @Test + public void testOptionalInstanceByName() { + TestRoute route = testRoute(completeWithValueToString(XTestHeader.optionalInstance())); + + route + .run(HttpRequest.create()) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(HttpRequest.create().addHeader(testHeaderInstance)) + .assertStatusCode(200) + .assertEntity("Some(X-Test-Header: abcdef-test)"); + } + @Test + public void testValueByClass() { + TestRoute route = testRoute(completeWithValueToString(AgeHeader.value())); + + route + .run(HttpRequest.create()) + .assertStatusCode(400) + .assertEntity("Request is missing required HTTP header 'Age'"); + + route + .run(HttpRequest.create().addHeader(ageHeaderInstance)) + .assertStatusCode(200) + .assertEntity("1000"); + } + @Test + public void testOptionalValueByClass() { + TestRoute route = testRoute(completeWithValueToString(AgeHeader.optionalValue())); + + route + .run(HttpRequest.create()) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(HttpRequest.create().addHeader(ageHeaderInstance)) + .assertStatusCode(200) + .assertEntity("Some(1000)"); + } + @Test + public void testInstanceByClass() { + TestRoute route = testRoute(completeWithValueToString(AgeHeader.instance())); + + route + .run(HttpRequest.create()) + .assertStatusCode(400) + .assertEntity("Request is missing required HTTP header 'Age'"); + + route + .run(HttpRequest.create().addHeader(ageHeaderInstance)) + .assertStatusCode(200) + .assertEntity("Age: 1000"); + } + @Test + public void testOptionalInstanceByClass() { + TestRoute route = testRoute(completeWithValueToString(AgeHeader.optionalInstance())); + + route + .run(HttpRequest.create()) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(HttpRequest.create().addHeader(ageHeaderInstance)) + .assertStatusCode(200) + .assertEntity("Some(Age: 1000)"); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/ParametersTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/ParametersTest.java new file mode 100644 index 0000000000..45b14c895e --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/ParametersTest.java @@ -0,0 +1,411 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.server.Handler1; +import akka.http.javadsl.server.RequestContext; +import akka.http.javadsl.server.RequestVal; +import akka.http.javadsl.server.RouteResult; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import akka.japi.Option; +import org.junit.Test; + +import java.util.*; + +public class ParametersTest extends JUnitRouteTest { + static Parameter stringParam = Parameters.stringValue("stringParam"); + static Parameter byteParam = Parameters.byteValue("byteParam"); + static Parameter shortParam = Parameters.shortValue("shortParam"); + static Parameter intParam = Parameters.intValue("intParam"); + static Parameter longParam = Parameters.longValue("longParam"); + static Parameter floatParam = Parameters.floatValue("floatParam"); + static Parameter doubleParam = Parameters.doubleValue("doubleParam"); + + static Parameter hexByteParam = Parameters.hexByteValue("hexByteParam"); + static Parameter hexShortParam = Parameters.hexShortValue("hexShortParam"); + static Parameter hexIntParam = Parameters.hexIntValue("hexIntParam"); + static Parameter hexLongParam = Parameters.hexLongValue("hexLongParam"); + + static RequestVal nameWithDefault = Parameters.stringValue("nameWithDefault").withDefault("John Doe"); + static RequestVal> optionalIntParam = Parameters.intValue("optionalIntParam").optional(); + + static RequestVal> paramMap = Parameters.asMap(); + static RequestVal>> paramMultiMap = Parameters.asMultiMap(); + static RequestVal>> paramEntries = Parameters.asCollection(); + + @Test + public void testStringParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(stringParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'stringParam'"); + + route + .run(HttpRequest.create().withUri("/abc?stringParam=john")) + .assertStatusCode(200) + .assertEntity("john"); + } + + @Test + public void testByteParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(byteParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'byteParam'"); + + route + .run(HttpRequest.create().withUri("/abc?byteParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'byteParam' was malformed:\n'test' is not a valid 8-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?byteParam=1000")) + .assertStatusCode(400) + .assertEntity("The query parameter 'byteParam' was malformed:\n'1000' is not a valid 8-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?byteParam=48")) + .assertStatusCode(200) + .assertEntity("48"); + } + + @Test + public void testShortParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(shortParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'shortParam'"); + + route + .run(HttpRequest.create().withUri("/abc?shortParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'shortParam' was malformed:\n'test' is not a valid 16-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?shortParam=100000")) + .assertStatusCode(400) + .assertEntity("The query parameter 'shortParam' was malformed:\n'100000' is not a valid 16-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?shortParam=1234")) + .assertStatusCode(200) + .assertEntity("1234"); + } + + @Test + public void testIntegerParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(intParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'intParam'"); + + route + .run(HttpRequest.create().withUri("/abc?intParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'intParam' was malformed:\n'test' is not a valid 32-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?intParam=48")) + .assertStatusCode(200) + .assertEntity("48"); + } + + @Test + public void testLongParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(longParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'longParam'"); + + route + .run(HttpRequest.create().withUri("/abc?longParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'longParam' was malformed:\n'test' is not a valid 64-bit signed integer value"); + + route + .run(HttpRequest.create().withUri("/abc?longParam=123456")) + .assertStatusCode(200) + .assertEntity("123456"); + } + + @Test + public void testFloatParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(floatParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'floatParam'"); + + route + .run(HttpRequest.create().withUri("/abc?floatParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'floatParam' was malformed:\n'test' is not a valid 32-bit floating point value"); + + route + .run(HttpRequest.create().withUri("/abc?floatParam=48")) + .assertStatusCode(200) + .assertEntity("48.0"); + } + + @Test + public void testDoubleParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(doubleParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'doubleParam'"); + + route + .run(HttpRequest.create().withUri("/abc?doubleParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'doubleParam' was malformed:\n'test' is not a valid 64-bit floating point value"); + + route + .run(HttpRequest.create().withUri("/abc?doubleParam=48")) + .assertStatusCode(200) + .assertEntity("48.0"); + } + + @Test + public void testHexByteParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexByteParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'hexByteParam'"); + + route + .run(HttpRequest.create().withUri("/abc?hexByteParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexByteParam' was malformed:\n'test' is not a valid 8-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexByteParam=1000")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexByteParam' was malformed:\n'1000' is not a valid 8-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexByteParam=48")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x48)); + } + + @Test + public void testHexShortParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexShortParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'hexShortParam'"); + + route + .run(HttpRequest.create().withUri("/abc?hexShortParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexShortParam' was malformed:\n'test' is not a valid 16-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexShortParam=100000")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexShortParam' was malformed:\n'100000' is not a valid 16-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexShortParam=1234")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x1234)); + } + + @Test + public void testHexIntegerParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexIntParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'hexIntParam'"); + + route + .run(HttpRequest.create().withUri("/abc?hexIntParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexIntParam' was malformed:\n'test' is not a valid 32-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexIntParam=12345678")) + .assertStatusCode(200) + .assertEntity(Integer.toString(0x12345678)); + } + + @Test + public void testHexLongParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(hexLongParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(404) + .assertEntity("Request is missing required query parameter 'hexLongParam'"); + + route + .run(HttpRequest.create().withUri("/abc?hexLongParam=test")) + .assertStatusCode(400) + .assertEntity("The query parameter 'hexLongParam' was malformed:\n'test' is not a valid 64-bit hexadecimal integer value"); + + route + .run(HttpRequest.create().withUri("/abc?hexLongParam=123456789a")) + .assertStatusCode(200) + .assertEntity(Long.toString(0x123456789aL)); + } + + @Test + public void testParametersAsMapExtraction() { + TestRoute route = testRoute(handleWith(paramMap, new Handler1>(){ + @Override + public RouteResult handle(RequestContext ctx, Map paramMap) { + ArrayList keys = new ArrayList(paramMap.keySet()); + Collections.sort(keys); + StringBuilder res = new StringBuilder(); + res.append(paramMap.size()).append(": ["); + for (String key: keys) + res.append(key).append(" -> ").append(paramMap.get(key)).append(", "); + res.append(']'); + return ctx.complete(res.toString()); + } + })); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("0: []"); + + route + .run(HttpRequest.create().withUri("/abc?a=b")) + .assertStatusCode(200) + .assertEntity("1: [a -> b, ]"); + + route + .run(HttpRequest.create().withUri("/abc?a=b&c=d")) + .assertStatusCode(200) + .assertEntity("2: [a -> b, c -> d, ]"); + } + @Test + public void testParametersAsMultiMapExtraction() { + TestRoute route = testRoute(handleWith(paramMultiMap, new Handler1>>(){ + @Override + public RouteResult handle(RequestContext ctx, Map> paramMap) { + ArrayList keys = new ArrayList(paramMap.keySet()); + Collections.sort(keys); + StringBuilder res = new StringBuilder(); + res.append(paramMap.size()).append(": ["); + for (String key: keys) { + res.append(key).append(" -> ["); + ArrayList values = new ArrayList(paramMap.get(key)); + Collections.sort(values); + for (String value: values) + res.append(value).append(", "); + res.append("], "); + } + res.append(']'); + return ctx.complete(res.toString()); + } + })); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("0: []"); + + route + .run(HttpRequest.create().withUri("/abc?a=b")) + .assertStatusCode(200) + .assertEntity("1: [a -> [b, ], ]"); + + route + .run(HttpRequest.create().withUri("/abc?a=b&c=d&a=a")) + .assertStatusCode(200) + .assertEntity("2: [a -> [a, b, ], c -> [d, ], ]"); + } + @Test + public void testParametersAsCollectionExtraction() { + TestRoute route = testRoute(handleWith(paramEntries, new Handler1>>(){ + @Override + public RouteResult handle(RequestContext ctx, Collection> paramEntries) { + ArrayList> entries = new ArrayList>(paramEntries); + Collections.sort(entries, new Comparator>() { + @Override + public int compare(Map.Entry e1, Map.Entry e2) { + int res = e1.getKey().compareTo(e2.getKey()); + return res == 0 ? e1.getValue().compareTo(e2.getValue()) : res; + } + }); + + StringBuilder res = new StringBuilder(); + res.append(paramEntries.size()).append(": ["); + for (Map.Entry entry: entries) + res.append(entry.getKey()).append(" -> ").append(entry.getValue()).append(", "); + res.append(']'); + return ctx.complete(res.toString()); + } + })); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("0: []"); + + route + .run(HttpRequest.create().withUri("/abc?a=b&e=f&c=d")) + .assertStatusCode(200) + .assertEntity("3: [a -> b, c -> d, e -> f, ]"); + + route + .run(HttpRequest.create().withUri("/abc?a=b&e=f&c=d&a=z")) + .assertStatusCode(200) + .assertEntity("4: [a -> b, a -> z, c -> d, e -> f, ]"); + } + + @Test + public void testOptionalIntParameterExtraction() { + TestRoute route = testRoute(completeWithValueToString(optionalIntParam)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("None"); + + route + .run(HttpRequest.create().withUri("/abc?optionalIntParam=23")) + .assertStatusCode(200) + .assertEntity("Some(23)"); + } + + @Test + public void testStringParameterExtractionWithDefaultValue() { + TestRoute route = testRoute(completeWithValueToString(nameWithDefault)); + + route + .run(HttpRequest.create().withUri("/abc")) + .assertStatusCode(200) + .assertEntity("John Doe"); + + route + .run(HttpRequest.create().withUri("/abc?nameWithDefault=paul")) + .assertStatusCode(200) + .assertEntity("paul"); + } +} diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java new file mode 100644 index 0000000000..4e1b1d3279 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/RequestValTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.RemoteAddress; +import akka.http.javadsl.model.Uri; +import akka.http.javadsl.model.headers.RawHeader; +import akka.http.javadsl.model.headers.XForwardedFor; +import akka.http.javadsl.server.RequestVals; +import akka.http.javadsl.server.Unmarshallers; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +import java.util.regex.Pattern; + +public class RequestValTest extends JUnitRouteTest { + @Test + public void testSchemeExtraction() { + TestRoute route = testRoute(completeWithValueToString(RequestVals.scheme())); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("http"); + + route + .run(HttpRequest.create().withUri(Uri.create("https://example.org"))) + .assertStatusCode(200) + .assertEntity("https"); + } + + @Test + public void testHostExtraction() { + TestRoute route = testRoute(completeWithValueToString(RequestVals.host())); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("example.org"); + } + + @Test + public void testHostPatternExtraction() { + TestRoute route = + testRoute( + completeWithValueToString( + RequestVals.matchAndExtractHost(Pattern.compile(".*\\.([^.]*)")))); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.org"))) + .assertStatusCode(200) + .assertEntity("org"); + + route + .run(HttpRequest.create().withUri(Uri.create("http://example.de"))) + .assertStatusCode(200) + .assertEntity("de"); + } + + @Test + public void testClientIpExtraction() { + TestRoute route = testRoute(completeWithValueToString(RequestVals.clientIP())); + + route + .run(HttpRequest.create().addHeader(XForwardedFor.create(RemoteAddress.create("127.0.0.2")))) + .assertStatusCode(200) + .assertEntity("127.0.0.2"); + + route + .run(HttpRequest.create().addHeader(akka.http.javadsl.model.headers.RemoteAddress.create(RemoteAddress.create("127.0.0.3")))) + .assertStatusCode(200) + .assertEntity("127.0.0.3"); + + route + .run(HttpRequest.create().addHeader(RawHeader.create("X-Real-IP", "127.0.0.4"))) + .assertStatusCode(200) + .assertEntity("127.0.0.4"); + + route + .run(HttpRequest.create()) + .assertStatusCode(404); + } + + @Test + public void testEntityAsString() { + TestRoute route = + testRoute( + completeWithValueToString(RequestVals.entityAs(Unmarshallers.String())) + ); + + HttpRequest request = + HttpRequest.POST("/") + .withEntity("abcdef"); + route.run(request) + .assertStatusCode(200) + .assertEntity("abcdef"); + } +} diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala index f96c33baed..4889fd9546 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/ParameterDirectivesSpec.scala @@ -9,7 +9,6 @@ import org.scalatest.{ FreeSpec, Inside } import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside { - "when used with 'as[Int]' the parameter directive should" - { "extract a parameter value as Int" in { Get("/?amount=123") ~> { @@ -205,4 +204,23 @@ class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Insi } ~> check { responseAs[String] === "List(3, 10)" } } } + + "The 'parameterSeq' directive should" - { + val completeAsList = + parameterSeq { params ⇒ + val sorted = params.sorted + complete(s"${sorted.size}: [${sorted.map(e ⇒ e._1 + " -> " + e._2).mkString(", ")}]") + } + + "extract parameters with different keys" in { + Get("/?a=b&e=f&c=d") ~> completeAsList ~> check { + responseAs[String] shouldEqual "3: [a -> b, c -> d, e -> f]" + } + } + "extract parameters with duplicate keys" in { + Get("/?a=b&e=f&c=d&a=z") ~> completeAsList ~> check { + responseAs[String] shouldEqual "4: [a -> b, a -> z, c -> d, e -> f]" + } + } + } } diff --git a/akka-http/src/main/scala/akka/http/impl/server/CookieImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/CookieImpl.scala new file mode 100644 index 0000000000..5b0623fcfc --- /dev/null +++ b/akka-http/src/main/scala/akka/http/impl/server/CookieImpl.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.impl.server + +import akka.http.javadsl.model.headers.HttpCookie +import akka.http.javadsl.server.values.Cookie +import akka.http.javadsl.server.{ Directive, Directives, RequestVal } +import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server.directives.CookieDirectives._ +import akka.japi.Option +import akka.http.impl.util.JavaMapping.Implicits._ + +case class CookieImpl(name: String, domain: Option[String] = None, path: Option[String] = None) extends Cookie { + def withDomain(domain: String): Cookie = copy(domain = Option.some(domain)) + def withPath(path: String): Cookie = copy(path = Option.some(path)) + + val value: RequestVal[String] = + new StandaloneExtractionImpl[String] { + def directive: Directive1[String] = cookie(name).map(_.value) + } + + def optionalValue(): RequestVal[Option[String]] = + new StandaloneExtractionImpl[Option[String]] { + def directive: Directive1[Option[String]] = optionalCookie(name).map(_.map(_.value).asJava) + } + + def set(value: String): Directive = + Directives.custom(Directives.setCookie( + HttpCookie.create(name, value, domain, path), _, _: _*)) +} diff --git a/akka-http/src/main/scala/akka/http/impl/server/FormFieldImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/FormFieldImpl.scala new file mode 100644 index 0000000000..9d8c3a31f3 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/impl/server/FormFieldImpl.scala @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.impl.server + +import akka.http.javadsl.server.RequestVal +import akka.http.javadsl.server.values.FormField +import akka.http.scaladsl.common.{ StrictForm, NameUnmarshallerReceptacle, NameReceptacle } +import akka.http.scaladsl.unmarshalling._ +import akka.http.scaladsl.util.FastFuture +import akka.japi.{ Option ⇒ JOption } + +import scala.concurrent.{ Future, ExecutionContext } +import scala.reflect.ClassTag +import akka.http.scaladsl.server.directives.FormFieldDirectives +import akka.http.scaladsl.server.{ Directives, Directive1 } +import FormFieldDirectives._ + +/** + * INTERNAL API + */ +private[http] class FormFieldImpl[T, U](receptacle: NameReceptacle[T])( + implicit fu: FromStrictFormFieldUnmarshaller[T], tTag: ClassTag[U], conv: T ⇒ U) + extends StandaloneExtractionImpl[U] with FormField[U] { + import Directives._ + + def directive: Directive1[U] = + extractMaterializer.flatMap { implicit fm ⇒ + formField(receptacle).map(conv) + } + + def optional: RequestVal[JOption[U]] = + new StandaloneExtractionImpl[JOption[U]] { + def directive: Directive1[JOption[U]] = optionalDirective + } + + private def optionalDirective: Directive1[JOption[U]] = + extractMaterializer.flatMap { implicit fm ⇒ + formField(receptacle.?).map(v ⇒ JOption.fromScalaOption(v.map(conv))) + } + + def withDefault(defaultValue: U): RequestVal[U] = + new StandaloneExtractionImpl[U] { + def directive: Directive1[U] = optionalDirective.map(_.getOrElse(defaultValue)) + } +} +object FormFieldImpl { + def apply[T, U](receptacle: NameReceptacle[T])(implicit fu: FromStrictFormFieldUnmarshaller[T], tTag: ClassTag[U], conv: T ⇒ U): FormField[U] = + new FormFieldImpl[T, U](receptacle)(fu, tTag, conv) + + def apply[T, U](receptacle: NameUnmarshallerReceptacle[T])(implicit tTag: ClassTag[U], conv: T ⇒ U): FormField[U] = + apply(new NameReceptacle[T](receptacle.name))(StrictForm.Field.unmarshallerFromFSU(receptacle.um), tTag, conv) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/impl/server/HeaderImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/HeaderImpl.scala new file mode 100644 index 0000000000..ab733ec90b --- /dev/null +++ b/akka-http/src/main/scala/akka/http/impl/server/HeaderImpl.scala @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.impl.server + +import akka.http.javadsl.model.HttpHeader +import akka.http.javadsl.server.RequestVal +import akka.http.javadsl.server.values.Header +import akka.http.scaladsl +import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.directives.BasicDirectives._ +import akka.http.scaladsl.server.directives.RouteDirectives._ + +import scala.reflect.ClassTag + +/** + * Internal API + */ +private[http] object HeaderImpl { + def apply[T <: HttpHeader]( + name: String, + optionalDirective: ClassTag[T with scaladsl.model.HttpHeader] ⇒ Directive1[Option[T with scaladsl.model.HttpHeader]], tClassTag: ClassTag[T]): Header[T] = { + type U = T with scaladsl.model.HttpHeader + + // cast is safe because creation of javadsl.model.HttpHeader that are not <: scaladsl.model.HttpHeader is forbidden + implicit def uClassTag = tClassTag.asInstanceOf[ClassTag[U]] + + new Header[U] { + val instanceDirective: Directive1[U] = + optionalDirective(uClassTag).flatMap { + case Some(v) ⇒ provide(v) + case None ⇒ reject(MissingHeaderRejection(name)) + } + + def instance(): RequestVal[U] = + new StandaloneExtractionImpl[U] { + def directive: Directive1[U] = instanceDirective + } + + def optionalInstance(): RequestVal[Option[U]] = + new StandaloneExtractionImpl[Option[U]] { + def directive: Directive1[Option[U]] = optionalDirective(uClassTag) + } + + def value(): RequestVal[String] = + new StandaloneExtractionImpl[String] { + def directive: Directive1[String] = instanceDirective.map(_.value) + } + + def optionalValue(): RequestVal[Option[String]] = + new StandaloneExtractionImpl[Option[String]] { + def directive: Directive1[Option[String]] = optionalDirective(uClassTag).map(_.map(_.value)) + } + }.asInstanceOf[Header[T]] // undeclared covariance + } +} diff --git a/akka-http/src/main/scala/akka/http/impl/server/ParameterImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/ParameterImpl.scala index 2b60dc5f29..895a0f45cf 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/ParameterImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/ParameterImpl.scala @@ -4,22 +4,46 @@ package akka.http.impl.server +import akka.http.javadsl.server.RequestVal import akka.http.javadsl.server.values.Parameter +import akka.http.scaladsl.common.{ NameUnmarshallerReceptacle, NameReceptacle } +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.unmarshalling._ +import akka.japi.{ Option ⇒ JOption } -import scala.concurrent.ExecutionContext import scala.reflect.ClassTag -import akka.http.scaladsl.server.directives.{ ParameterDirectives, BasicDirectives } +import akka.http.scaladsl.server.directives.ParameterDirectives import akka.http.scaladsl.server.Directive1 -import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet /** * INTERNAL API */ -private[http] class ParameterImpl[T: ClassTag](val underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] }) - extends StandaloneExtractionImpl[T] with Parameter[T] { +private[http] class ParameterImpl[T, U](receptacle: NameReceptacle[T])( + implicit fu: FromStringUnmarshaller[T], tTag: ClassTag[U], conv: T ⇒ U) + extends StandaloneExtractionImpl[U] with Parameter[U] { - def directive: Directive1[T] = - BasicDirectives.extractExecutionContext.flatMap { implicit ec ⇒ - ParameterDirectives.parameter(underlying(ec)) + import ParameterDirectives._ + def directive: Directive1[U] = parameter(receptacle).map(conv) + + def optional: RequestVal[JOption[U]] = + new StandaloneExtractionImpl[JOption[U]] { + def directive: Directive1[JOption[U]] = optionalDirective + } + + private def optionalDirective: Directive1[JOption[U]] = + extractMaterializer.flatMap { implicit fm ⇒ + parameter(receptacle.?).map(v ⇒ JOption.fromScalaOption(v.map(conv))) + } + + def withDefault(defaultValue: U): RequestVal[U] = + new StandaloneExtractionImpl[U] { + def directive: Directive1[U] = optionalDirective.map(_.getOrElse(defaultValue)) } } +private[http] object ParameterImpl { + def apply[T, U](receptacle: NameReceptacle[T])(implicit fu: FromStringUnmarshaller[T], tTag: ClassTag[U], conv: T ⇒ U): Parameter[U] = + new ParameterImpl(receptacle)(fu, tTag, conv) + + def apply[T, U](receptacle: NameUnmarshallerReceptacle[T])(implicit tTag: ClassTag[U], conv: T ⇒ U): Parameter[U] = + new ParameterImpl(new NameReceptacle(receptacle.name))(receptacle.um, tTag, conv) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/impl/server/PathMatcherImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/PathMatcherImpl.scala index 3fee85a517..37595c1821 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/PathMatcherImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/PathMatcherImpl.scala @@ -5,6 +5,7 @@ package akka.http.impl.server import akka.http.javadsl.server.values.PathMatcher +import akka.japi.Option import scala.reflect.ClassTag import akka.http.scaladsl.server.{ PathMatcher ⇒ ScalaPathMatcher } @@ -13,4 +14,6 @@ import akka.http.scaladsl.server.{ PathMatcher ⇒ ScalaPathMatcher } * INTERNAL API */ private[http] class PathMatcherImpl[T: ClassTag](val matcher: ScalaPathMatcher[Tuple1[T]]) - extends ExtractionImpl[T] with PathMatcher[T] \ No newline at end of file + extends ExtractionImpl[T] with PathMatcher[T] { + def optional: PathMatcher[Option[T]] = new PathMatcherImpl[Option[T]](matcher.?.map(Option.fromScalaOption)) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala index 69b02b31ca..f15a7aaf87 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala @@ -4,7 +4,7 @@ package akka.http.impl.server -import scala.concurrent.Future +import scala.concurrent.{ ExecutionContext, Future } import akka.http.javadsl.{ model ⇒ jm } import akka.http.impl.util.JavaMapping.Implicits._ import akka.http.scaladsl.server.{ RequestContext ⇒ ScalaRequestContext } @@ -14,8 +14,6 @@ import akka.http.javadsl.server._ * INTERNAL API */ private[http] final case class RequestContextImpl(underlying: ScalaRequestContext) extends RequestContext { - import underlying.executionContext - // provides auto-conversion to japi.RouteResult import RouteResultImpl._ @@ -25,7 +23,7 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex def completeWith(futureResult: Future[RouteResult]): RouteResult = futureResult.flatMap { case r: RouteResultImpl ⇒ r.underlying - } + }(executionContext) def complete(text: String): RouteResult = underlying.complete(text) def completeWithStatus(statusCode: Int): RouteResult = completeWithStatus(jm.StatusCodes.get(statusCode)) @@ -40,4 +38,6 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex def complete(response: jm.HttpResponse): RouteResult = underlying.complete(response.asScala) def notFound(): RouteResult = underlying.reject() + + def executionContext: ExecutionContext = underlying.executionContext } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala index 2e466c1c55..7bf603e686 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala @@ -4,7 +4,10 @@ package akka.http.impl.server +import akka.http.impl.util.JavaMapping import akka.http.javadsl.server.values.{ PathMatcher, BasicUserCredentials } +import akka.http.scaladsl.model.StatusCodes.Redirection +import akka.http.scaladsl.server.util.TupleOps.Join import scala.language.implicitConversions import scala.annotation.tailrec @@ -13,8 +16,8 @@ import akka.http.javadsl.model.ContentType import akka.http.scaladsl.server.directives.{ UserCredentials, ContentTypeResolver } import akka.http.scaladsl.server.directives.FileAndResourceDirectives.DirectoryRenderer import akka.http.scaladsl.model.HttpHeader -import akka.http.scaladsl.model.headers.CustomHeader -import akka.http.scaladsl.server.{ Route ⇒ ScalaRoute, Directive0, Directives } +import akka.http.scaladsl.model.headers.{ HttpCookie, CustomHeader } +import akka.http.scaladsl.server.{ Route ⇒ ScalaRoute, Directive ⇒ ScalaDirective, PathMatcher ⇒ ScalaPathMatcher, PathMatcher1, Directive0, Directive1, Directives } import akka.http.impl.util.JavaMapping.Implicits._ import akka.http.scaladsl.server import akka.http.javadsl.server._ @@ -26,11 +29,13 @@ import RouteStructure._ private[http] trait ExtractionMap extends CustomHeader { def get[T](key: RequestVal[T]): Option[T] def set[T](key: RequestVal[T], value: T): ExtractionMap + def addAll(values: Map[RequestVal[_], Any]): ExtractionMap } /** * INTERNAL API */ private[http] object ExtractionMap { + val Empty = ExtractionMap(Map.empty) implicit def apply(map: Map[RequestVal[_], Any]): ExtractionMap = new ExtractionMap { def get[T](key: RequestVal[T]): Option[T] = @@ -39,6 +44,9 @@ private[http] object ExtractionMap { def set[T](key: RequestVal[T], value: T): ExtractionMap = ExtractionMap(map.updated(key, value)) + def addAll(values: Map[RequestVal[_], Any]): ExtractionMap = + ExtractionMap(map ++ values) + // CustomHeader methods override def suppressRendering: Boolean = true def name(): String = "ExtractedValues" @@ -50,99 +58,146 @@ private[http] object ExtractionMap { * INTERNAL API */ private[http] object RouteImplementation extends Directives with server.RouteConcatenation { - def apply(route: Route): ScalaRoute = route match { - case RouteAlternatives(children) ⇒ - val converted = children.map(RouteImplementation.apply) - converted.reduce(_ ~ _) - case RawPathPrefix(elements, children) ⇒ - val inner = apply(RouteAlternatives(children)) + def apply(route: Route): ScalaRoute = { + def directiveFor(route: DirectiveRoute): Directive0 = route match { + case RouteAlternatives() ⇒ ScalaDirective.Empty + case RawPathPrefix(elements) ⇒ pathMatcherDirective[String](elements, rawPathPrefix) + case RawPathPrefixTest(elements) ⇒ pathMatcherDirective[String](elements, rawPathPrefixTest) + case PathSuffix(elements) ⇒ pathMatcherDirective[String](elements, pathSuffix) + case PathSuffixTest(elements) ⇒ pathMatcherDirective[String](elements, pathSuffixTest) + case RedirectToTrailingSlashIfMissing(code) ⇒ redirectToTrailingSlashIfMissing(code.asScala.asInstanceOf[Redirection]) + case RedirectToNoTrailingSlashIfPresent(code) ⇒ redirectToNoTrailingSlashIfPresent(code.asScala.asInstanceOf[Redirection]) - def one[T](matcher: PathMatcher[T]): Directive0 = - rawPathPrefix(matcher.asInstanceOf[PathMatcherImpl[T]].matcher) flatMap { value ⇒ - addExtraction(matcher, value) + case MethodFilter(m) ⇒ method(m.asScala) + case Extract(extractions) ⇒ + extractRequestContext.flatMap { ctx ⇒ + extractions.map { e ⇒ + e.directive.flatMap(addExtraction(e.asInstanceOf[RequestVal[Any]], _)) + }.reduce(_ & _) } - elements.map(one(_)).reduce(_ & _).apply(inner) - case GetFromResource(path, contentType, classLoader) ⇒ - getFromResource(path, contentType.asScala, classLoader) - case GetFromResourceDirectory(path, classLoader, resolver) ⇒ - getFromResourceDirectory(path, classLoader)(scalaResolver(resolver)) - case GetFromFile(file, contentType) ⇒ - getFromFile(file, contentType.asScala) - case GetFromDirectory(directory, true, resolver) ⇒ - extractExecutionContext { implicit ec ⇒ - getFromBrowseableDirectory(directory.getPath)(DirectoryRenderer.defaultDirectoryRenderer, scalaResolver(resolver)) - } - case FileAndResourceRouteWithDefaultResolver(constructor) ⇒ - RouteImplementation(constructor(new directives.ContentTypeResolver { - def resolve(fileName: String): ContentType = ContentTypeResolver.Default(fileName) - })) + case BasicAuthentication(authenticator) ⇒ + authenticateBasicAsync(authenticator.realm, { creds ⇒ + val javaCreds = + creds match { + case UserCredentials.Missing ⇒ + new BasicUserCredentials { + def available: Boolean = false + def userName: String = throw new IllegalStateException("Credentials missing") + def verifySecret(secret: String): Boolean = throw new IllegalStateException("Credentials missing") + } + case p @ UserCredentials.Provided(name) ⇒ + new BasicUserCredentials { + def available: Boolean = true + def userName: String = name + def verifySecret(secret: String): Boolean = p.verifySecret(secret) + } + } - case MethodFilter(m, children) ⇒ - val inner = apply(RouteAlternatives(children)) - method(m.asScala).apply(inner) + authenticator.authenticate(javaCreds) + }).flatMap { user ⇒ + addExtraction(authenticator.asInstanceOf[RequestVal[Any]], user) + } - case Extract(extractions, children) ⇒ - val inner = apply(RouteAlternatives(children)) - extractRequestContext.flatMap { ctx ⇒ - extractions.map { e ⇒ - e.directive.flatMap(addExtraction(e.asInstanceOf[RequestVal[Any]], _)) - }.reduce(_ & _) - }.apply(inner) + case EncodeResponse(coders) ⇒ + val scalaCoders = coders.map(_._underlyingScalaCoder()) + encodeResponseWith(scalaCoders.head, scalaCoders.tail: _*) - case BasicAuthentication(authenticator, children) ⇒ - val inner = apply(RouteAlternatives(children)) - authenticateBasicAsync(authenticator.realm, { creds ⇒ - val javaCreds = - creds match { - case UserCredentials.Missing ⇒ - new BasicUserCredentials { - def available: Boolean = false - def userName: String = throw new IllegalStateException("Credentials missing") - def verifySecret(secret: String): Boolean = throw new IllegalStateException("Credentials missing") - } - case p @ UserCredentials.Provided(name) ⇒ - new BasicUserCredentials { - def available: Boolean = true - def userName: String = name - def verifySecret(secret: String): Boolean = p.verifySecret(secret) - } - } + case DecodeRequest(coders) ⇒ decodeRequestWith(coders.map(_._underlyingScalaCoder()): _*) + case Conditional(eTag, lastModified) ⇒ conditional(eTag.map(_.asScala), lastModified.map(_.asScala)) + case h: HostFilter ⇒ host(h.filter _) + case SchemeFilter(schemeName) ⇒ scheme(schemeName) - authenticator.authenticate(javaCreds) - }).flatMap { user ⇒ - addExtraction(authenticator.asInstanceOf[RequestVal[Any]], user) - }.apply(inner) + case HandleExceptions(handler) ⇒ + val pf: akka.http.scaladsl.server.ExceptionHandler = akka.http.scaladsl.server.ExceptionHandler { + case e: RuntimeException ⇒ apply(handler.handle(e)) + } + handleExceptions(pf) - case EncodeResponse(coders, children) ⇒ - val scalaCoders = coders.map(_._underlyingScalaCoder()) - encodeResponseWith(scalaCoders.head, scalaCoders.tail: _*).apply(apply(RouteAlternatives(children))) + case Validated(isValid, errorMsg) ⇒ validate(isValid, errorMsg) + case RangeSupport() ⇒ withRangeSupport + case SetCookie(cookie) ⇒ setCookie(cookie.asScala) + case DeleteCookie(name, domain, path) ⇒ deleteCookie(HttpCookie(name, domain = domain, path = path, value = "deleted")) + } - case Conditional(eTag, lastModified, children) ⇒ - conditional(eTag.asScala, lastModified.asScala).apply(apply(RouteAlternatives(children))) + route match { + case route: DirectiveRoute ⇒ directiveFor(route).apply(fromAlternatives(route.children)) + case GetFromResource(path, contentType, classLoader) ⇒ getFromResource(path, contentType.asScala, classLoader) + case GetFromResourceDirectory(path, classLoader, resolver) ⇒ getFromResourceDirectory(path, classLoader)(scalaResolver(resolver)) + case GetFromFile(file, contentType) ⇒ getFromFile(file, contentType.asScala) + case GetFromDirectory(directory, true, resolver) ⇒ + extractExecutionContext { implicit ec ⇒ + getFromBrowseableDirectory(directory.getPath)(DirectoryRenderer.defaultDirectoryRenderer, scalaResolver(resolver)) + } + case FileAndResourceRouteWithDefaultResolver(constructor) ⇒ + RouteImplementation(constructor(new directives.ContentTypeResolver { + def resolve(fileName: String): ContentType = ContentTypeResolver.Default(fileName) + })) - case HandleExceptions(handler, children) ⇒ - val pf: akka.http.scaladsl.server.ExceptionHandler = akka.http.scaladsl.server.ExceptionHandler { - case e: RuntimeException ⇒ apply(handler.handle(e)) - } - handleExceptions(pf).apply(apply(RouteAlternatives(children))) + case HandleWebsocketMessages(handler) ⇒ handleWebsocketMessages(JavaMapping.toScala(handler)) + case Redirect(uri, code) ⇒ redirect(uri.asScala, code.asScala.asInstanceOf[Redirection]) // guarded by require in Redirect - case o: OpaqueRoute ⇒ - (ctx ⇒ o.handle(new RequestContextImpl(ctx)).asInstanceOf[RouteResultImpl].underlying) + case dyn: DynamicDirectiveRoute1[t1Type] ⇒ + def runToRoute(t1: t1Type): ScalaRoute = + apply(dyn.createDirective(t1).route(dyn.innerRoute, dyn.moreInnerRoutes: _*)) - case p: Product ⇒ extractExecutionContext { implicit ec ⇒ complete(500, s"Not implemented: ${p.productPrefix}") } + requestValToDirective(dyn.value1)(runToRoute) + + case dyn: DynamicDirectiveRoute2[t1Type, t2Type] ⇒ + def runToRoute(t1: t1Type, t2: t2Type): ScalaRoute = + apply(dyn.createDirective(t1, t2).route(dyn.innerRoute, dyn.moreInnerRoutes: _*)) + + (requestValToDirective(dyn.value1) & requestValToDirective(dyn.value2))(runToRoute) + + case o: OpaqueRoute ⇒ (ctx ⇒ o.handle(new RequestContextImpl(ctx)).asInstanceOf[RouteResultImpl].underlying) + case p: Product ⇒ extractExecutionContext { implicit ec ⇒ complete(500, s"Not implemented: ${p.productPrefix}") } + } + } + def pathMatcherDirective[T](matchers: immutable.Seq[PathMatcher[_]], + directive: PathMatcher1[T] ⇒ Directive1[T] // this type is too specific and only a placeholder for a proper polymorphic function + ): Directive0 = { + // Concatenating PathMatchers is a bit complicated as we don't want to build up a tuple + // but something which we can later split all the separate values and add them to the + // ExtractionMap. + // + // This is achieved by providing a specialized `Join` instance to use with PathMatcher + // which provides the desired behavior. + + type ValMap = Tuple1[Map[RequestVal[_], Any]] + object AddToMapJoin extends Join[ValMap, ValMap] { + type Out = ValMap + def apply(prefix: ValMap, suffix: ValMap): AddToMapJoin.Out = + Tuple1(prefix._1 ++ suffix._1) + } + def toScala(matcher: PathMatcher[_]): ScalaPathMatcher[ValMap] = + matcher.asInstanceOf[PathMatcherImpl[_]].matcher.transform(_.map(v ⇒ Tuple1(Map(matcher -> v._1)))) + def addExtractions(valMap: T): Directive0 = transformExtractionMap(_.addAll(valMap.asInstanceOf[Map[RequestVal[_], Any]])) + val reduced: ScalaPathMatcher[ValMap] = matchers.map(toScala).reduce(_.~(_)(AddToMapJoin)) + directive(reduced.asInstanceOf[PathMatcher1[T]]).flatMap(addExtractions) } - def addExtraction[T](key: RequestVal[T], value: T): Directive0 = { - @tailrec def addToExtractionMap(headers: immutable.Seq[HttpHeader], prefix: Vector[HttpHeader] = Vector.empty): immutable.Seq[HttpHeader] = + def fromAlternatives(alternatives: Seq[Route]): ScalaRoute = + alternatives.map(RouteImplementation.apply).reduce(_ ~ _) + + def addExtraction[T](key: RequestVal[T], value: T): Directive0 = + transformExtractionMap(_.set(key, value)) + + def transformExtractionMap(f: ExtractionMap ⇒ ExtractionMap): Directive0 = { + @tailrec def updateExtractionMap(headers: immutable.Seq[HttpHeader], prefix: Vector[HttpHeader] = Vector.empty): immutable.Seq[HttpHeader] = headers match { - case (m: ExtractionMap) +: rest ⇒ m.set(key, value) +: (prefix ++ rest) - case other +: rest ⇒ addToExtractionMap(rest, prefix :+ other) - case Nil ⇒ ExtractionMap(Map(key -> value)) +: prefix + case (m: ExtractionMap) +: rest ⇒ f(m) +: (prefix ++ rest) + case other +: rest ⇒ updateExtractionMap(rest, prefix :+ other) + case Nil ⇒ f(ExtractionMap.Empty) +: prefix } - mapRequest(_.mapHeaders(addToExtractionMap(_))) + mapRequest(_.mapHeaders(updateExtractionMap(_))) } private def scalaResolver(resolver: directives.ContentTypeResolver): ContentTypeResolver = ContentTypeResolver(f ⇒ resolver.resolve(f).asScala) + + def requestValToDirective[T](value: RequestVal[T]): Directive1[T] = + value match { + case s: StandaloneExtractionImpl[_] ⇒ s.directive + case v: RequestVal[_] ⇒ extract(ctx ⇒ v.get(new RequestContextImpl(ctx))) + } } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala index 5ef77b4191..b2a7fe90fb 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala @@ -5,12 +5,14 @@ package akka.http.impl.server import java.io.File +import akka.http.javadsl.model.ws.Message import akka.http.javadsl.server.values.{ PathMatcher, HttpBasicAuthenticator } +import akka.stream.javadsl.Flow import scala.language.existentials import scala.collection.immutable -import akka.http.javadsl.model.{ DateTime, ContentType, HttpMethod } -import akka.http.javadsl.model.headers.EntityTag +import akka.http.javadsl.model._ +import akka.http.javadsl.model.headers.{ HttpCookie, EntityTag } import akka.http.javadsl.server.directives.ContentTypeResolver import akka.http.javadsl.server._ @@ -19,13 +21,15 @@ import akka.http.javadsl.server._ */ private[http] object RouteStructure { trait DirectiveRoute extends Route { - def children: immutable.Seq[Route] + def innerRoute: Route + def moreInnerRoutes: immutable.Seq[Route] + + def children: immutable.Seq[Route] = innerRoute +: moreInnerRoutes require(children.nonEmpty) } - case class RouteAlternatives(children: immutable.Seq[Route]) extends DirectiveRoute - - case class MethodFilter(method: HttpMethod, children: immutable.Seq[Route]) extends DirectiveRoute { + case class RouteAlternatives()(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class MethodFilter(method: HttpMethod)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute { def filter(ctx: RequestContext): Boolean = ctx.request.method == method } @@ -34,15 +38,54 @@ private[http] object RouteStructure { case class GetFromResourceDirectory(resourceDirectory: String, classLoader: ClassLoader, resolver: ContentTypeResolver) extends Route case class GetFromFile(file: File, contentType: ContentType) extends Route case class GetFromDirectory(directory: File, browseable: Boolean, resolver: ContentTypeResolver) extends Route + case class Redirect(uri: Uri, redirectionType: StatusCode) extends Route { + require(redirectionType.isRedirection, s"`redirectionType` must be a redirection status code but was $redirectionType") + } - case class RawPathPrefix(pathElements: immutable.Seq[PathMatcher[_]], children: immutable.Seq[Route]) extends DirectiveRoute - case class Extract(extractions: Seq[StandaloneExtractionImpl[_]], children: immutable.Seq[Route]) extends DirectiveRoute - case class BasicAuthentication(authenticator: HttpBasicAuthenticator[_], children: immutable.Seq[Route]) extends DirectiveRoute - case class EncodeResponse(coders: immutable.Seq[Coder], children: immutable.Seq[Route]) extends DirectiveRoute + case class PathSuffix(pathElements: immutable.Seq[PathMatcher[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class PathSuffixTest(pathElements: immutable.Seq[PathMatcher[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class RawPathPrefix(pathElements: immutable.Seq[PathMatcher[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class RawPathPrefixTest(pathElements: immutable.Seq[PathMatcher[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class RedirectToTrailingSlashIfMissing(redirectionType: StatusCode)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute { + require(redirectionType.isRedirection, s"`redirectionType` must be a redirection status code but was $redirectionType") + } + case class RedirectToNoTrailingSlashIfPresent(redirectionType: StatusCode)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute { + require(redirectionType.isRedirection, s"`redirectionType` must be a redirection status code but was $redirectionType") + } + case class Extract(extractions: Seq[StandaloneExtractionImpl[_]])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class BasicAuthentication(authenticator: HttpBasicAuthenticator[_])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class EncodeResponse(coders: immutable.Seq[Coder])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class DecodeRequest(coders: immutable.Seq[Coder])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute - case class Conditional(entityTag: EntityTag, lastModified: DateTime, children: immutable.Seq[Route]) extends DirectiveRoute + case class Conditional(entityTag: Option[EntityTag] = None, lastModified: Option[DateTime] = None)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute { + require(entityTag.isDefined || lastModified.isDefined) + } - case class HandleExceptions(handler: ExceptionHandler, children: immutable.Seq[Route]) extends DirectiveRoute + abstract class DynamicDirectiveRoute1[T1](val value1: RequestVal[T1])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends Route { + def createDirective(t1: T1): Directive + } + abstract class DynamicDirectiveRoute2[T1, T2](val value1: RequestVal[T1], val value2: RequestVal[T2])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends Route { + def createDirective(t1: T1, t2: T2): Directive + } + case class Validated(isValid: Boolean, errorMsg: String)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + + case class HandleExceptions(handler: ExceptionHandler)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + + sealed abstract class HostFilter extends DirectiveRoute { + def filter(hostName: String): Boolean + } + case class HostNameFilter(hostWhiteList: immutable.Seq[String])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends HostFilter { + def filter(hostName: String): Boolean = hostWhiteList.contains(hostName) + } + abstract class GenericHostFilter(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends HostFilter + case class SchemeFilter(scheme: String)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + + case class RangeSupport()(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + + case class HandleWebsocketMessages(handler: Flow[Message, Message, Any]) extends Route + + case class SetCookie(cookie: HttpCookie)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class DeleteCookie(name: String, domain: Option[String], path: Option[String])(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute abstract class OpaqueRoute(extractions: RequestVal[_]*) extends Route { def handle(ctx: RequestContext): RouteResult diff --git a/akka-http/src/main/scala/akka/http/impl/server/StandaloneExtractionImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/StandaloneExtractionImpl.scala index c6dde5e962..ef49b0e09a 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/StandaloneExtractionImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/StandaloneExtractionImpl.scala @@ -15,6 +15,12 @@ import akka.http.scaladsl.server._ private[http] abstract class StandaloneExtractionImpl[T: ClassTag] extends ExtractionImpl[T] with RequestVal[T] { def directive: Directive1[T] } +private[http] object StandaloneExtractionImpl { + def apply[T: ClassTag](extractionDirective: Directive1[T]): RequestVal[T] = + new StandaloneExtractionImpl[T] { + def directive: Directive1[T] = extractionDirective + } +} /** * INTERNAL API diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/Directives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/Directives.scala index e5cdf30d3d..a06592c0b2 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/Directives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/Directives.scala @@ -9,8 +9,7 @@ import scala.collection.immutable import scala.annotation.varargs import akka.http.javadsl.model.HttpMethods -// FIXME: add support for the remaining directives, see #16436 -abstract class AllDirectives extends PathDirectives +abstract class AllDirectives extends WebsocketDirectives /** * @@ -19,8 +18,8 @@ object Directives extends AllDirectives { /** * INTERNAL API */ - private[http] def custom(f: immutable.Seq[Route] ⇒ Route): Directive = + private[http] def custom(f: (Route, immutable.Seq[Route]) ⇒ Route): Directive = new AbstractDirective { - def createRoute(first: Route, others: Array[Route]): Route = f(first +: others.toVector) + def createRoute(first: Route, others: Array[Route]): Route = f(first, others.toList) } } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala index d5c3cd4410..8c3332d511 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala @@ -7,7 +7,7 @@ package akka.http.javadsl.server import akka.http.javadsl.model._ import akka.util.ByteString -import scala.concurrent.Future +import scala.concurrent.{ ExecutionContext, Future } /** * The RequestContext represents the state of the request while it is routed through @@ -61,5 +61,8 @@ trait RequestContext { */ def notFound(): RouteResult + /** Returns the ExecutionContext of this RequestContext */ + def executionContext(): ExecutionContext + // FIXME: provide proper support for rejections, see #16438 } \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RequestVals.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RequestVals.scala index 0fa9d21f29..77e1286b16 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/RequestVals.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RequestVals.scala @@ -4,13 +4,14 @@ package akka.http.javadsl.server +import java.util.regex.Pattern import java.{ util ⇒ ju } import scala.concurrent.Future import scala.reflect.ClassTag -import akka.http.javadsl.model.HttpMethod +import akka.http.javadsl.model.{ RemoteAddress, HttpMethod } import akka.http.scaladsl.server import akka.http.scaladsl.server._ -import akka.http.scaladsl.server.directives.{ RouteDirectives, BasicDirectives } +import akka.http.scaladsl.server.directives._ import akka.http.impl.server.{ UnmarshallerImpl, ExtractingStandaloneExtractionImpl, RequestContextImpl, StandaloneExtractionImpl } import akka.http.scaladsl.util.FastFuture import akka.http.impl.util.JavaMapping.Implicits._ @@ -38,6 +39,58 @@ object RequestVals { def extract(ctx: server.RequestContext): Future[HttpMethod] = FastFuture.successful(ctx.request.method.asJava) } + /** + * Extracts the scheme used for this request. + */ + def requestContext: RequestVal[RequestContext] = + new StandaloneExtractionImpl[RequestContext] { + def directive: Directive1[RequestContext] = BasicDirectives.extractRequestContext.map(RequestContextImpl(_): RequestContext) + } + + /** + * Extracts the unmatched path of the request context. + */ + def unmatchedPath: RequestVal[String] = + new ExtractingStandaloneExtractionImpl[String] { + def extract(ctx: server.RequestContext): Future[String] = FastFuture.successful(ctx.unmatchedPath.toString) + } + + /** + * Extracts the scheme used for this request. + */ + def scheme: RequestVal[String] = + new StandaloneExtractionImpl[String] { + def directive: Directive1[String] = SchemeDirectives.extractScheme + } + + /** + * Extracts the host name this request targeted. + */ + def host: RequestVal[String] = + new StandaloneExtractionImpl[String] { + def directive: Directive1[String] = HostDirectives.extractHost + } + + /** + * Extracts the host name this request targeted. + */ + def matchAndExtractHost(regex: Pattern): RequestVal[String] = + new StandaloneExtractionImpl[String] { + def directive: Directive1[String] = HostDirectives.host(regex.pattern().r) + } + + /** + * Directive extracting the IP of the client from either the X-Forwarded-For, Remote-Address or X-Real-IP header + * (in that order of priority). + * + * TODO: add link to the configuration entry that would add a remote-address header + */ + def clientIP(): RequestVal[RemoteAddress] = + new StandaloneExtractionImpl[RemoteAddress] { + def directive: Directive1[RemoteAddress] = + MiscDirectives.extractClientIP.map(x ⇒ x: RemoteAddress) // missing covariance of Directive + } + /** * Creates a new [[RequestVal]] given a [[ju.Map]] and a [[RequestVal]] that represents the key. * The new RequestVal represents the existing value as looked up in the map. If the key doesn't diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/Unmarshallers.scala b/akka-http/src/main/scala/akka/http/javadsl/server/Unmarshallers.scala new file mode 100644 index 0000000000..eadb955780 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/Unmarshallers.scala @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server + +import akka.http.impl.server.UnmarshallerImpl +import akka.http.scaladsl.unmarshalling.{ FromMessageUnmarshaller, PredefinedFromEntityUnmarshallers } + +object Unmarshallers { + def String: Unmarshaller[String] = + new UnmarshallerImpl[String]({ (ec, mat) ⇒ + implicit val _ = mat + implicitly[FromMessageUnmarshaller[String]] + }) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala index 1184a27a00..04b66d51a5 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala @@ -5,20 +5,22 @@ package akka.http.javadsl.server.directives import scala.annotation.varargs -import java.lang.reflect.Method +import java.lang.reflect.{ ParameterizedType, Method } import akka.http.javadsl.model.{ StatusCode, HttpResponse } import akka.http.javadsl.server._ import akka.http.impl.server.RouteStructure._ import akka.http.impl.server._ +import scala.concurrent.Future + abstract class BasicDirectives { /** * Tries the given routes in sequence until the first one matches. */ @varargs - def route(route: Route, others: Route*): Route = - RouteAlternatives(route +: others.toVector) + def route(innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteAlternatives()(innerRoute, moreInnerRoutes.toList) /** * A route that completes the request with a static text @@ -64,7 +66,7 @@ abstract class BasicDirectives { */ @varargs def extractHere(extractions: RequestVal[_]*): Directive = - Directives.custom(Extract(extractions.map(_.asInstanceOf[StandaloneExtractionImpl[_ <: AnyRef]]), _)) + Directives.custom(Extract(extractions.map(_.asInstanceOf[StandaloneExtractionImpl[_ <: AnyRef]]))) /** * A route that handles the request with the given opaque handler. Specify a set of extractions @@ -168,22 +170,35 @@ abstract class BasicDirectives { res } def returnTypeMatches(method: Method): Boolean = - method.getReturnType == classOf[RouteResult] + method.getReturnType == classOf[RouteResult] || returnsFuture(method) + + def returnsFuture(method: Method): Boolean = + method.getReturnType == classOf[Future[_]] && + method.getGenericReturnType.isInstanceOf[ParameterizedType] && + method.getGenericReturnType.asInstanceOf[ParameterizedType].getActualTypeArguments()(0) == classOf[RouteResult] + + /** Makes sure both RouteResult and Future[RouteResult] are acceptable result types. */ + def adaptResult(method: Method): (RequestContext, AnyRef) ⇒ RouteResult = + if (returnsFuture(method)) (ctx, v) ⇒ ctx.completeWith(v.asInstanceOf[Future[RouteResult]]) + else (_, v) ⇒ v.asInstanceOf[RouteResult] + + val IdentityAdaptor: (RequestContext, Seq[Any]) ⇒ Seq[Any] = (_, ps) ⇒ ps + def methodInvocator(method: Method, adaptParams: (RequestContext, Seq[Any]) ⇒ Seq[Any]): (RequestContext, Seq[Any]) ⇒ RouteResult = { + val resultAdaptor = adaptResult(method) + if (!method.isAccessible) method.setAccessible(true) + if (adaptParams == IdentityAdaptor) + (ctx, params) ⇒ resultAdaptor(ctx, method.invoke(instance, params.toArray.asInstanceOf[Array[AnyRef]]: _*)) + else + (ctx, params) ⇒ resultAdaptor(ctx, method.invoke(instance, adaptParams(ctx, params).toArray.asInstanceOf[Array[AnyRef]]: _*)) + } object ParameterTypes { def unapply(method: Method): Option[List[Class[_]]] = Some(method.getParameterTypes.toList) } methods.filter(returnTypeMatches).collectFirst { - case method @ ParameterTypes(RequestContextClass :: rest) if paramsMatch(rest) ⇒ { - if (!method.isAccessible) method.setAccessible(true) // FIXME: test what happens if this fails - (ctx: RequestContext, params: Seq[Any]) ⇒ method.invoke(instance, (ctx +: params).toArray.asInstanceOf[Array[AnyRef]]: _*).asInstanceOf[RouteResult] - } - - case method @ ParameterTypes(rest) if paramsMatch(rest) ⇒ { - if (!method.isAccessible) method.setAccessible(true) - (ctx: RequestContext, params: Seq[Any]) ⇒ method.invoke(instance, params.toArray.asInstanceOf[Array[AnyRef]]: _*).asInstanceOf[RouteResult] - } + case method @ ParameterTypes(RequestContextClass :: rest) if paramsMatch(rest) ⇒ methodInvocator(method, _ +: _) + case method @ ParameterTypes(rest) if paramsMatch(rest) ⇒ methodInvocator(method, IdentityAdaptor) }.getOrElse(throw new RuntimeException("No suitable method found")) } def lookupMethod() = { diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/CacheConditionDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CacheConditionDirectives.scala index 32e2b7006c..d4c5f90396 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/CacheConditionDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CacheConditionDirectives.scala @@ -14,12 +14,46 @@ import scala.annotation.varargs abstract class CacheConditionDirectives extends BasicDirectives { /** * Wraps its inner route with support for Conditional Requests as defined - * by tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26 + * by http://tools.ietf.org/html/rfc7232 * - * In particular the algorithm defined by tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26#section-6 + * In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6 * is implemented by this directive. + * + * Note: if you want to combine this directive with `withRangeSupport(...)` you need to put + * it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)` + * must be on a deeper level in your route structure in order to function correctly. */ @varargs - def conditional(entityTag: EntityTag, lastModified: DateTime, innerRoutes: Route*): Route = - RouteStructure.Conditional(entityTag, lastModified, innerRoutes.toVector) + def conditional(entityTag: EntityTag, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.Conditional(entityTag = Some(entityTag))(innerRoute, moreInnerRoutes.toList) + + /** + * Wraps its inner route with support for Conditional Requests as defined + * by http://tools.ietf.org/html/rfc7232 + * + * In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6 + * is implemented by this directive. + * + * Note: if you want to combine this directive with `withRangeSupport(...)` you need to put + * it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)` + * must be on a deeper level in your route structure in order to function correctly. + */ + @varargs + def conditional(lastModified: DateTime, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.Conditional(lastModified = Some(lastModified))(innerRoute, moreInnerRoutes.toList) + + /** + * Wraps its inner route with support for Conditional Requests as defined + * by http://tools.ietf.org/html/rfc7232 + * + * In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6 + * is implemented by this directive. + * + * Note: if you want to combine this directive with `withRangeSupport(...)` you need to put + * it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)` + * must be on a deeper level in your route structure in order to function correctly. + */ + @varargs + def conditional(entityTag: EntityTag, lastModified: DateTime, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.Conditional(Some(entityTag), Some(lastModified))(innerRoute, moreInnerRoutes.toList) } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/CodingDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CodingDirectives.scala index 191ff9054c..72f0dc94a9 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/CodingDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CodingDirectives.scala @@ -2,11 +2,18 @@ * Copyright (C) 2009-2014 Typesafe Inc. */ -package akka.http.javadsl.server.directives +package akka.http.javadsl.server +package directives + +import akka.http.javadsl.server.Directive +import akka.http.javadsl.server.Directives +import akka.http.javadsl.server.Route +import akka.http.scaladsl.coding.Decoder +import akka.http.scaladsl.server._ import scala.annotation.varargs +import akka.http.scaladsl import akka.http.impl.server.RouteStructure -import akka.http.javadsl.server.{ Coder, Directive, Directives, Route } abstract class CodingDirectives extends CacheConditionDirectives { /** @@ -14,9 +21,8 @@ abstract class CodingDirectives extends CacheConditionDirectives { * using one of the predefined coders, `Gzip`, `Deflate`, or `NoCoding` depending on * a potential [[akka.http.javadsl.model.headers.AcceptEncoding]] header from the client. */ - @varargs def encodeResponse(innerRoutes: Route*): Route = - // FIXME: make sure this list stays synchronized with the Scala one - RouteStructure.EncodeResponse(List(Coder.NoCoding, Coder.Gzip, Coder.Deflate), innerRoutes.toVector) + @varargs def encodeResponse(innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.EncodeResponse(CodingDirectives._DefaultCodersToEncodeResponse)(innerRoute, moreInnerRoutes.toList) /** * A directive that Wraps its inner routes with encoding support. @@ -27,5 +33,42 @@ abstract class CodingDirectives extends CacheConditionDirectives { * will be respected (or otherwise, if no matching . */ @varargs def encodeResponse(coders: Coder*): Directive = - Directives.custom(RouteStructure.EncodeResponse(coders.toList, _)) + Directives.custom(RouteStructure.EncodeResponse(coders.toList)) + + /** + * Decodes the incoming request using the given Decoder. + * If the request encoding doesn't match the request is rejected with an `UnsupportedRequestEncodingRejection`. + */ + @varargs def decodeRequestWith(decoder: Coder, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.DecodeRequest(decoder :: Nil)(innerRoute, moreInnerRoutes.toList) + + /** + * Decodes the incoming request if it is encoded with one of the given + * encoders. If the request encoding doesn't match one of the given encoders + * the request is rejected with an `UnsupportedRequestEncodingRejection`. + * If no decoders are given the default encoders (``Gzip``, ``Deflate``, ``NoCoding``) are used. + */ + @varargs def decodeRequestWith(decoders: Coder*): Directive = + Directives.custom(RouteStructure.DecodeRequest(decoders.toList)) + + /** + * Decompresses the incoming request if it is ``gzip`` or ``deflate`` compressed. + * Uncompressed requests are passed through untouched. + * If the request encoded with another encoding the request is rejected with an `UnsupportedRequestEncodingRejection`. + */ + @varargs def decodeRequest(innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.DecodeRequest(CodingDirectives._DefaultCodersToDecodeRequest)(innerRoute, moreInnerRoutes.toList) } + +/** + * Internal API + */ +private[http] object CodingDirectives { + private[http] val _DefaultCodersToEncodeResponse = + scaladsl.server.directives.CodingDirectives.DefaultEncodeResponseEncoders + .map(c ⇒ Coder.values().find(_._underlyingScalaCoder() == c).get) + + private[http] val _DefaultCodersToDecodeRequest = + scaladsl.server.directives.CodingDirectives.DefaultCoders + .map(c ⇒ Coder.values().find(_._underlyingScalaCoder() == c).get) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/CookieDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CookieDirectives.scala new file mode 100644 index 0000000000..948c8a9cd3 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/CookieDirectives.scala @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives + +import akka.http.impl.server.RouteStructure +import akka.http.javadsl.model.headers.HttpCookie +import akka.http.javadsl.server.Route + +import scala.annotation.varargs + +abstract class CookieDirectives extends CodingDirectives { + /** + * Adds a Set-Cookie header with the given cookies to all responses of its inner route. + */ + @varargs def setCookie(cookie: HttpCookie, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.SetCookie(cookie)(innerRoute, moreInnerRoutes.toList) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala index 54ebc1a793..b6d18757cc 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala @@ -9,11 +9,11 @@ import akka.http.impl.server.RouteStructure import scala.annotation.varargs -abstract class ExecutionDirectives extends CodingDirectives { +abstract class ExecutionDirectives extends CookieDirectives { /** * Handles exceptions in the inner routes using the specified handler. */ @varargs - def handleExceptions(handler: ExceptionHandler, innerRoutes: Route*): Route = - RouteStructure.HandleExceptions(handler, innerRoutes.toVector) + def handleExceptions(handler: ExceptionHandler, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.HandleExceptions(handler)(innerRoute, moreInnerRoutes.toList) } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/HostDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/HostDirectives.scala new file mode 100644 index 0000000000..de905a0453 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/HostDirectives.scala @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package directives + +import java.lang.{ Iterable ⇒ JIterable, Boolean ⇒ JBoolean } + +import scala.annotation.varargs +import scala.collection.JavaConverters._ + +import akka.http.impl.server.RouteStructure.{ GenericHostFilter, HostNameFilter } + +abstract class HostDirectives extends FileAndResourceDirectives { + /** + * Rejects all requests with a host name different from the given one. + */ + @varargs + def host(hostName: String, innerRoute: Route, moreInnerRoutes: Route*): Route = + HostNameFilter(hostName :: Nil)(innerRoute, moreInnerRoutes.toList) + + /** + * Rejects all requests with a host name different from the given ones. + */ + @varargs + def host(hostNames: JIterable[String], innerRoute: Route, moreInnerRoutes: Route*): Route = + HostNameFilter(hostNames.asScala.toList)(innerRoute, moreInnerRoutes.toList) + + /** + * Rejects all requests for whose host name the given predicate function returns false. + */ + @varargs + def host(predicate: akka.japi.function.Function[String, JBoolean], innerRoute: Route, moreInnerRoutes: Route*): Route = + new GenericHostFilter(innerRoute, moreInnerRoutes.toList) { + def filter(hostName: String): Boolean = predicate.apply(hostName) + } +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/MethodDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/MethodDirectives.scala index 4f94060ec2..58d3871648 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/MethodDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/MethodDirectives.scala @@ -10,36 +10,36 @@ import akka.http.impl.server.RouteStructure import scala.annotation.varargs -abstract class MethodDirectives extends FileAndResourceDirectives { +abstract class MethodDirectives extends HostDirectives { /** Handles the inner routes if the incoming request is a GET request, rejects the request otherwise */ @varargs - def get(innerRoutes: Route*): Route = method(HttpMethods.GET, innerRoutes: _*) + def get(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.GET, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a POST request, rejects the request otherwise */ @varargs - def post(innerRoutes: Route*): Route = method(HttpMethods.POST, innerRoutes: _*) + def post(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.POST, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a PUT request, rejects the request otherwise */ @varargs - def put(innerRoutes: Route*): Route = method(HttpMethods.PUT, innerRoutes: _*) + def put(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.PUT, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a DELETE request, rejects the request otherwise */ @varargs - def delete(innerRoutes: Route*): Route = method(HttpMethods.DELETE, innerRoutes: _*) + def delete(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.DELETE, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a HEAD request, rejects the request otherwise */ @varargs - def head(innerRoutes: Route*): Route = method(HttpMethods.HEAD, innerRoutes: _*) + def head(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.HEAD, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a OPTIONS request, rejects the request otherwise */ @varargs - def options(innerRoutes: Route*): Route = method(HttpMethods.OPTIONS, innerRoutes: _*) + def options(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.OPTIONS, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a PATCH request, rejects the request otherwise */ @varargs - def patch(innerRoutes: Route*): Route = method(HttpMethods.PATCH, innerRoutes: _*) + def patch(innerRoute: Route, moreInnerRoutes: Route*): Route = method(HttpMethods.PATCH, innerRoute, moreInnerRoutes: _*) /** Handles the inner routes if the incoming request is a request with the given method, rejects the request otherwise */ @varargs - def method(method: HttpMethod, innerRoutes: Route*): Route = RouteStructure.MethodFilter(method, innerRoutes.toVector) + def method(method: HttpMethod, innerRoute: Route, moreInnerRoutes: Route*): Route = RouteStructure.MethodFilter(method)(innerRoute, moreInnerRoutes.toList) } \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/MiscDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/MiscDirectives.scala new file mode 100644 index 0000000000..275e80f305 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/MiscDirectives.scala @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package directives + +import java.lang.{ Boolean ⇒ JBoolean } + +import akka.http.impl.server.RouteStructure.{ DynamicDirectiveRoute2, Validated, DynamicDirectiveRoute1 } + +import scala.annotation.varargs +import akka.japi.function.{ Function2, Function } + +abstract class MiscDirectives extends MethodDirectives { + /** + * Returns a Route which checks the given condition on the request context before running its inner Route. + * If the condition fails the route is rejected with a [[spray.routing.ValidationRejection]]. + */ + @varargs + def validate(check: Function[RequestContext, JBoolean], errorMsg: String, innerRoute: Route, moreInnerRoutes: Route*): Route = + validate(RequestVals.requestContext, check, errorMsg, innerRoute, moreInnerRoutes: _*) + + /** + * Returns a Route which checks the given condition before running its inner Route. If the condition fails the + * route is rejected with a [[spray.routing.ValidationRejection]]. + */ + @varargs + def validate[T](value: RequestVal[T], check: Function[T, JBoolean], errorMsg: String, innerRoute: Route, moreInnerRoutes: Route*): Route = + new DynamicDirectiveRoute1[T](value)(innerRoute, moreInnerRoutes.toList) { + def createDirective(t1: T): Directive = Directives.custom(Validated(check.apply(t1), errorMsg)) + } + + /** + * Returns a Route which checks the given condition before running its inner Route. If the condition fails the + * route is rejected with a [[spray.routing.ValidationRejection]]. + */ + @varargs + def validate[T1, T2](value1: RequestVal[T1], + value2: RequestVal[T2], + check: Function2[T1, T2, JBoolean], + errorMsg: String, + innerRoute: Route, moreInnerRoutes: Route*): Route = + new DynamicDirectiveRoute2[T1, T2](value1, value2)(innerRoute, moreInnerRoutes.toList) { + def createDirective(t1: T1, t2: T2): Directive = Directives.custom(Validated(check.apply(t1, t2), errorMsg)) + } +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/PathDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/PathDirectives.scala index af96feab9d..2ac704df21 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/PathDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/PathDirectives.scala @@ -6,12 +6,14 @@ package akka.http.javadsl.server package directives import akka.http.impl.server.RouteStructure +import akka.http.impl.server.RouteStructure.{ RedirectToNoTrailingSlashIfPresent, RedirectToTrailingSlashIfMissing } +import akka.http.javadsl.model.StatusCode import akka.http.javadsl.server.values.{ PathMatchers, PathMatcher } import scala.annotation.varargs import scala.collection.immutable -abstract class PathDirectives extends MethodDirectives { +abstract class PathDirectives extends MiscDirectives { /** * Tries to consumes the complete unmatched path given a number of PathMatchers. Between each * of the matchers a `/` will be matched automatically. @@ -20,20 +22,48 @@ abstract class PathDirectives extends MethodDirectives { */ @varargs def path(matchers: AnyRef*): Directive = - forMatchers(joinWithSlash(convertMatchers(matchers)) :+ PathMatchers.END) + RawPathPrefixForMatchers(joinWithSlash(convertMatchers(matchers)) :+ PathMatchers.END) @varargs def pathPrefix(matchers: AnyRef*): Directive = - forMatchers(joinWithSlash(convertMatchers(matchers))) + RawPathPrefixForMatchers(joinWithSlash(convertMatchers(matchers))) - def pathSingleSlash: Directive = forMatchers(List(PathMatchers.SLASH, PathMatchers.END)) + @varargs + def pathPrefixTest(matchers: AnyRef*): Directive = + RawPathPrefixTestForMatchers(joinWithSlash(convertMatchers(matchers))) @varargs def rawPathPrefix(matchers: AnyRef*): Directive = - forMatchers(convertMatchers(matchers)) + RawPathPrefixForMatchers(convertMatchers(matchers)) - private def forMatchers(matchers: immutable.Seq[PathMatcher[_]]): Directive = - Directives.custom(RouteStructure.RawPathPrefix(matchers, _)) + @varargs + def rawPathPrefixTest(matchers: AnyRef*): Directive = + RawPathPrefixTestForMatchers(convertMatchers(matchers)) + + @varargs + def pathSuffix(matchers: AnyRef*): Directive = + Directives.custom(RouteStructure.PathSuffix(convertMatchers(matchers))) + + @varargs + def pathSuffixTest(matchers: AnyRef*): Directive = + Directives.custom(RouteStructure.PathSuffixTest(convertMatchers(matchers))) + + def pathEnd: Directive = RawPathPrefixForMatchers(PathMatchers.END :: Nil) + def pathSingleSlash: Directive = RawPathPrefixForMatchers(List(PathMatchers.SLASH, PathMatchers.END)) + def pathEndOrSingleSlash: Directive = RawPathPrefixForMatchers(List(PathMatchers.SLASH.optional, PathMatchers.END)) + + @varargs + def redirectToTrailingSlashIfMissing(redirectionStatusCode: StatusCode, innerRoute: Route, moreInnerRoutes: Route*): Route = + RedirectToTrailingSlashIfMissing(redirectionStatusCode)(innerRoute, moreInnerRoutes.toList) + @varargs + def redirectToNoTrailingSlashIfPresent(redirectionStatusCode: StatusCode, innerRoute: Route, moreInnerRoutes: Route*): Route = + RedirectToNoTrailingSlashIfPresent(redirectionStatusCode)(innerRoute, moreInnerRoutes.toList) + + private def RawPathPrefixForMatchers(matchers: immutable.Seq[PathMatcher[_]]): Directive = + Directives.custom(RouteStructure.RawPathPrefix(matchers)) + + private def RawPathPrefixTestForMatchers(matchers: immutable.Seq[PathMatcher[_]]): Directive = + Directives.custom(RouteStructure.RawPathPrefixTest(matchers)) private def joinWithSlash(matchers: immutable.Seq[PathMatcher[_]]): immutable.Seq[PathMatcher[_]] = { def join(result: immutable.Seq[PathMatcher[_]], next: PathMatcher[_]): immutable.Seq[PathMatcher[_]] = @@ -46,6 +76,7 @@ abstract class PathDirectives extends MethodDirectives { def parse(matcher: AnyRef): PathMatcher[_] = matcher match { case p: PathMatcher[_] ⇒ p case name: String ⇒ PathMatchers.segment(name) + case x ⇒ throw new IllegalArgumentException(s"Matcher of class ${x.getClass} is unsupported for PathDirectives") } matchers.map(parse).toVector diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/RangeDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/RangeDirectives.scala new file mode 100644 index 0000000000..70dcdc4f66 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/RangeDirectives.scala @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package directives + +import akka.http.impl.server.RouteStructure.RangeSupport + +import scala.annotation.varargs + +abstract class RangeDirectives extends PathDirectives { + /** + * Answers GET requests with an `Accept-Ranges: bytes` header and converts HttpResponses coming back from its inner + * route into partial responses if the initial request contained a valid `Range` request header. The requested + * byte-ranges may be coalesced. + * This directive is transparent to non-GET requests + * Rejects requests with unsatisfiable ranges `UnsatisfiableRangeRejection`. + * Rejects requests with too many expected ranges. + * + * Note: if you want to combine this directive with `conditional(...)` you need to put + * it on the *inside* of the `conditional(...)` directive, i.e. `conditional(...)` must be + * on a higher level in your route structure in order to function correctly. + * + * @see https://tools.ietf.org/html/rfc7233 + */ + @varargs def withRangeSupport(innerRoute: Route, moreInnerRoutes: Route*): Route = RangeSupport()(innerRoute, moreInnerRoutes.toList) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/RouteDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/RouteDirectives.scala new file mode 100644 index 0000000000..4e005935c9 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/RouteDirectives.scala @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package directives + +import akka.http.impl.server.RouteStructure.Redirect +import akka.http.javadsl.model.{ StatusCode, Uri } + +abstract class RouteDirectives extends RangeDirectives { + /** + * Completes the request with redirection response of the given type to the given URI. + * + * The ``redirectionType`` must be a StatusCode for which ``isRedirection`` returns true. + */ + def redirect(uri: Uri, redirectionType: StatusCode): Route = Redirect(uri, redirectionType) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/SchemeDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/SchemeDirectives.scala new file mode 100644 index 0000000000..36fbca898e --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/SchemeDirectives.scala @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.directives + +import akka.http.impl.server.RouteStructure.SchemeFilter +import akka.http.javadsl.server.Route + +import scala.annotation.varargs + +abstract class SchemeDirectives extends RouteDirectives { + /** + * Rejects all requests whose Uri scheme does not match the given one. + */ + @varargs + def scheme(scheme: String, innerRoute: Route, moreInnerRoutes: Route*): Route = SchemeFilter(scheme)(innerRoute, moreInnerRoutes.toList) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/WebsocketDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/WebsocketDirectives.scala new file mode 100644 index 0000000000..763991cf4e --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/WebsocketDirectives.scala @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package directives + +import akka.http.impl.server.RouteStructure +import akka.http.javadsl.model.ws.Message +import akka.stream.javadsl.Flow + +abstract class WebsocketDirectives extends SchemeDirectives { + /** + * Handles websocket requests with the given handler and rejects other requests with a + * [[ExpectedWebsocketRequestRejection]]. + */ + def handleWebsocketMessages(handler: Flow[Message, Message, Any]): Route = + RouteStructure.HandleWebsocketMessages(handler) +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/Cookie.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/Cookie.scala new file mode 100644 index 0000000000..b0327d7c99 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/Cookie.scala @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values + +import akka.http.impl.server.{ RouteStructure, CookieImpl } +import akka.http.javadsl.server.{ Directive, RequestVal, Route } +import akka.japi.Option + +import scala.annotation.varargs +import scala.collection.immutable + +abstract class Cookie { + def name(): String + def domain(): Option[String] + def path(): Option[String] + + def withDomain(domain: String): Cookie + def withPath(path: String): Cookie + + def value(): RequestVal[String] + def optionalValue(): RequestVal[Option[String]] + + def set(value: String): Directive + + @varargs + def delete(innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.DeleteCookie(name(), domain(), path())(innerRoute, moreInnerRoutes.toList) +} +object Cookies { + def create(name: String): Cookie = new CookieImpl(name) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/FormField.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/FormField.scala new file mode 100644 index 0000000000..15ca4a9b2f --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/FormField.scala @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server +package values + +import java.{ lang ⇒ jl } +import akka.japi.{ Option ⇒ JOption } + +import akka.http.impl.server.FormFieldImpl +import akka.http.scaladsl.unmarshalling._ + +trait FormField[T] extends RequestVal[T] { + def optional: RequestVal[JOption[T]] + def withDefault(defaultValue: T): RequestVal[T] +} + +object FormFields { + import akka.http.scaladsl.common.ToNameReceptacleEnhancements._ + + def stringValue(name: String): FormField[String] = FormFieldImpl(name) + def intValue(name: String): FormField[jl.Integer] = FormFieldImpl(name.as[Int]) + def byteValue(name: String): FormField[jl.Byte] = FormFieldImpl(name.as[Byte]) + def shortValue(name: String): FormField[jl.Short] = FormFieldImpl(name.as[Short]) + def longValue(name: String): FormField[jl.Long] = FormFieldImpl(name.as[Long]) + def floatValue(name: String): FormField[jl.Float] = FormFieldImpl(name.as[Float]) + def doubleValue(name: String): FormField[jl.Double] = FormFieldImpl(name.as[Double]) + def booleanValue(name: String): FormField[jl.Boolean] = FormFieldImpl(name.as[Boolean]) + + def hexByteValue(name: String): FormField[jl.Byte] = FormFieldImpl(name.as(Unmarshaller.HexByte)) + def hexShortValue(name: String): FormField[jl.Short] = FormFieldImpl(name.as(Unmarshaller.HexShort)) + def hexIntValue(name: String): FormField[jl.Integer] = FormFieldImpl(name.as(Unmarshaller.HexInt)) + def hexLongValue(name: String): FormField[jl.Long] = FormFieldImpl(name.as(Unmarshaller.HexLong)) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala new file mode 100644 index 0000000000..5cb079ae27 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server.values + +import akka.http.impl.server.HeaderImpl +import akka.http.javadsl.model.HttpHeader +import akka.http.javadsl.server.RequestVal +import akka.http.scaladsl.model +import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server.util.ClassMagnet + +import scala.reflect.{ ClassTag, classTag } + +trait Header[T <: HttpHeader] { + def instance(): RequestVal[T] + def optionalInstance(): RequestVal[Option[T]] + + def value(): RequestVal[String] + def optionalValue(): RequestVal[Option[String]] +} +object Headers { + import akka.http.scaladsl.server.directives.BasicDirectives._ + import akka.http.scaladsl.server.directives.HeaderDirectives._ + + def byName(name: String): Header[HttpHeader] = + HeaderImpl[HttpHeader](name, _ ⇒ optionalHeaderInstanceByName(name.toLowerCase()), classTag[HttpHeader]) + + def byClass[T <: HttpHeader](clazz: Class[T]): Header[T] = + HeaderImpl[T](clazz.getSimpleName, ct ⇒ optionalHeaderValueByType(ClassMagnet()(ct)), ClassTag(clazz)) + + private def optionalHeaderInstanceByName(lowercaseName: String): Directive1[Option[model.HttpHeader]] = + extract(_.request.headers.collectFirst { + case h @ model.HttpHeader(`lowercaseName`, _) ⇒ h + }) +} \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala index 932cd3cdb6..4eb6a562b4 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/HttpBasicAuthenticator.scala @@ -54,5 +54,5 @@ abstract class HttpBasicAuthenticator[T](val realm: String) extends AbstractDire * INTERNAL API */ protected[http] final def createRoute(first: Route, others: Array[Route]): Route = - RouteStructure.BasicAuthentication(this, (first +: others).toVector) + RouteStructure.BasicAuthentication(this)(first, others.toList) } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/Parameter.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/Parameter.scala index baf7d34a5a..ddd6fed5ed 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/values/Parameter.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/Parameter.scala @@ -4,20 +4,30 @@ package akka.http.javadsl.server.values +import java.util.AbstractMap.SimpleEntry import java.{ lang ⇒ jl } -import akka.http.impl.server.ParameterImpl +import java.util.{ Map ⇒ JMap, Collection ⇒ JCollection } + +import akka.http.scaladsl.server.directives.ParameterDirectives +import akka.http.scaladsl.unmarshalling.Unmarshaller + +import scala.reflect.ClassTag + +import akka.japi.{ Option ⇒ JOption } + +import akka.http.impl.server.{ StandaloneExtractionImpl, ParameterImpl } import akka.http.javadsl.server.RequestVal import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet -import scala.concurrent.ExecutionContext -import scala.reflect.ClassTag - /** * A RequestVal representing a query parameter of type T. */ -trait Parameter[T] extends RequestVal[T] +trait Parameter[T] extends RequestVal[T] { + def optional: RequestVal[JOption[T]] + def withDefault(defaultValue: T): RequestVal[T] +} /** * A collection of predefined parameters. @@ -29,17 +39,29 @@ object Parameters { /** * A string query parameter. */ - def string(name: String): Parameter[String] = - fromScalaParam(implicit ec ⇒ ParamMagnet(name)) + def stringValue(name: String): Parameter[String] = ParameterImpl(name) /** * An integer query parameter. */ - def integer(name: String): Parameter[jl.Integer] = - fromScalaParam[jl.Integer](implicit ec ⇒ - ParamMagnet(name.as[Int]).asInstanceOf[ParamMagnet { type Out = Directive1[jl.Integer] }]) + def intValue(name: String): Parameter[jl.Integer] = ParameterImpl(name.as[Int]) + def byteValue(name: String): Parameter[jl.Byte] = ParameterImpl(name.as[Byte]) + def shortValue(name: String): Parameter[jl.Short] = ParameterImpl(name.as[Short]) + def longValue(name: String): Parameter[jl.Long] = ParameterImpl(name.as[Long]) + def floatValue(name: String): Parameter[jl.Float] = ParameterImpl(name.as[Float]) + def doubleValue(name: String): Parameter[jl.Double] = ParameterImpl(name.as[Double]) + def booleanValue(name: String): Parameter[jl.Boolean] = ParameterImpl(name.as[Boolean]) - private def fromScalaParam[T: ClassTag](underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] }): Parameter[T] = - new ParameterImpl[T](underlying) + def hexByteValue(name: String): Parameter[jl.Byte] = ParameterImpl(name.as(Unmarshaller.HexByte)) + def hexShortValue(name: String): Parameter[jl.Short] = ParameterImpl(name.as(Unmarshaller.HexShort)) + def hexIntValue(name: String): Parameter[jl.Integer] = ParameterImpl(name.as(Unmarshaller.HexInt)) + def hexLongValue(name: String): Parameter[jl.Long] = ParameterImpl(name.as(Unmarshaller.HexLong)) + + import scala.collection.JavaConverters._ + def asMap: RequestVal[JMap[String, String]] = StandaloneExtractionImpl(ParameterDirectives.parameterMap.map(_.asJava)) + def asMultiMap: RequestVal[JMap[String, JCollection[String]]] = + StandaloneExtractionImpl(ParameterDirectives.parameterMultiMap.map(_.mapValues(_.asJavaCollection).asJava)) + def asCollection: RequestVal[JCollection[JMap.Entry[String, String]]] = + StandaloneExtractionImpl(ParameterDirectives.parameterSeq.map(_.map(e ⇒ new SimpleEntry(e._1, e._2): JMap.Entry[String, String]).asJavaCollection)) } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/PathMatchers.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/PathMatchers.scala index 0a5c55e9c7..9871328141 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/values/PathMatchers.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/PathMatchers.scala @@ -9,6 +9,7 @@ import java.{ lang ⇒ jl, util ⇒ ju } import akka.http.impl.server.PathMatcherImpl import akka.http.javadsl.server.RequestVal import akka.http.scaladsl.server.{ PathMatcher0, PathMatcher1, PathMatchers ⇒ ScalaPathMatchers } +import akka.japi.Option import scala.collection.JavaConverters._ import scala.reflect.ClassTag @@ -21,7 +22,9 @@ import scala.reflect.ClassTag * Using a PathMatcher with the [[Directives.path]] or [[Directives.pathPrefix]] directives * "consumes" a part of the path which is recorded in [[RequestContext.unmatchedPath]]. */ -trait PathMatcher[T] extends RequestVal[T] +trait PathMatcher[T] extends RequestVal[T] { + def optional: PathMatcher[Option[T]] +} /** * A collection of predefined path matchers. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala b/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala index ea7a5d3c73..cd6744aaad 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala @@ -42,6 +42,8 @@ sealed abstract class StrictForm { object StrictForm { sealed trait Field object Field { + private[http] def fromString(value: String): Field = FromString(value) + private[StrictForm] final case class FromString(value: String) extends Field private[StrictForm] final case class FromPart(value: Multipart.FormData.BodyPart.Strict) extends Field diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala index 5a5c46c9a5..b5325ae560 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala @@ -38,7 +38,7 @@ trait CodingDirectives { * identity, gzip or deflate then no encoding is used. */ def encodeResponse: Directive0 = - encodeResponseWith(NoCoding, Gzip, Deflate) + _encodeResponse(DefaultEncodeResponseEncoders) /** * Encodes the response with the encoding that is requested by the client via the `Accept- @@ -110,6 +110,8 @@ trait CodingDirectives { object CodingDirectives extends CodingDirectives { val DefaultCoders: immutable.Seq[Coder] = immutable.Seq(Gzip, Deflate, NoCoding) + private[http] val DefaultEncodeResponseEncoders = immutable.Seq(NoCoding, Gzip, Deflate) + def theseOrDefault[T >: Coder](these: Seq[T]): Seq[T] = if (these.isEmpty) DefaultCoders else these import BasicDirectives._