diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/HandlerExampleSpec.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/HandlerExampleSpec.java index 79ce476add..22c61fb79c 100644 --- a/akka-docs-dev/rst/java/code/docs/http/javadsl/HandlerExampleSpec.java +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/HandlerExampleSpec.java @@ -63,8 +63,8 @@ public class HandlerExampleSpec extends JUnitRouteTest { RequestVal xParam = Parameters.intValue("x"); RequestVal yParam = Parameters.intValue("y"); - RequestVal xSegment = PathMatchers.integerNumber(); - RequestVal ySegment = PathMatchers.integerNumber(); + RequestVal xSegment = PathMatchers.intValue(); + RequestVal ySegment = PathMatchers.intValue(); //#handler2 Handler2 multiply = @@ -114,8 +114,8 @@ public class HandlerExampleSpec extends JUnitRouteTest { RequestVal xParam = Parameters.intValue("x"); RequestVal yParam = Parameters.intValue("y"); - RequestVal xSegment = PathMatchers.integerNumber(); - RequestVal ySegment = PathMatchers.integerNumber(); + RequestVal xSegment = PathMatchers.intValue(); + RequestVal ySegment = PathMatchers.intValue(); //#reflective diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/PathDirectiveExampleTest.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/PathDirectiveExampleTest.java new file mode 100644 index 0000000000..de41f7ace4 --- /dev/null +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/PathDirectiveExampleTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package docs.http.javadsl; + +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.server.*; +import akka.http.javadsl.server.values.Parameters; +import akka.http.javadsl.server.values.PathMatcher; +import akka.http.javadsl.server.values.PathMatchers; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.Test; + +public class PathDirectiveExampleTest extends JUnitRouteTest { + @Test + public void testPathPrefix() { + //#path-examples + // matches "/test" + path("test").route( + completeWithStatus(StatusCodes.OK) + ); + + // matches "/test", as well + path(PathMatchers.segment("test")).route( + completeWithStatus(StatusCodes.OK) + ); + + // matches "/admin/user" + path("admin", "user").route( + completeWithStatus(StatusCodes.OK) + ); + + // matches "/admin/user", as well + pathPrefix("admin").route( + path("user").route( + completeWithStatus(StatusCodes.OK) + ) + ); + + // matches "/admin/user/" + Handler1 completeWithUserId = + new Handler1() { + @Override + public RouteResult handle(RequestContext ctx, Integer userId) { + return ctx.complete("Hello user " + userId); + } + }; + PathMatcher userId = PathMatchers.intValue(); + pathPrefix("admin", "user").route( + path(userId).route( + handleWith1(userId, completeWithUserId) + ) + ); + + // matches "/admin/user/", as well + path("admin", "user", userId).route( + handleWith1(userId, completeWithUserId) + ); + + // never matches + path("admin").route( // oops this only matches "/admin" + path("user").route( + completeWithStatus(StatusCodes.OK) + ) + ); + + // matches "/user/" with the first subroute, "/user" (without a trailing slash) + // with the second subroute, and "/user/" with the last one. + pathPrefix("user").route( + pathSingleSlash().route( + completeWithStatus(StatusCodes.OK) + ), + pathEnd().route( + completeWithStatus(StatusCodes.OK) + ), + path(userId).route( + handleWith1(userId, completeWithUserId) + ) + ); + //#path-examples + } + + // FIXME: remove once #17988 is merged + public static Route handleWith1(RequestVal val, Object o) { + return null; + } +} \ No newline at end of file diff --git a/akka-docs-dev/rst/java/http/routing-dsl/directives/index.rst b/akka-docs-dev/rst/java/http/routing-dsl/directives/index.rst index da85f99cfc..846e1d1756 100644 --- a/akka-docs-dev/rst/java/http/routing-dsl/directives/index.rst +++ b/akka-docs-dev/rst/java/http/routing-dsl/directives/index.rst @@ -46,7 +46,7 @@ MethodDirectives MiscDirectives Contains directives that validate a request by user-defined logic. -PathDirectives +:ref:`PathDirectives-java` Contains directives to match and filter on the URI path of the incoming request. RangeDirectives @@ -58,4 +58,9 @@ SchemeDirectives WebsocketDirectives Contains directives to support answering Websocket requests. +.. toctree:: + :maxdepth: 1 + + path-directives + .. _`RFC 7234`: http://tools.ietf.org/html/rfc7234 \ No newline at end of file diff --git a/akka-docs-dev/rst/java/http/routing-dsl/directives/path-directives.rst b/akka-docs-dev/rst/java/http/routing-dsl/directives/path-directives.rst new file mode 100644 index 0000000000..1530003fae --- /dev/null +++ b/akka-docs-dev/rst/java/http/routing-dsl/directives/path-directives.rst @@ -0,0 +1,95 @@ +.. _PathDirectives-java: + +PathDirectives +============== + +Path directives are the most basic building blocks for routing requests depending on the URI path. + +When a request (or rather the respective ``RequestContext`` instance) enters the route structure it has an +"unmatched path" that is identical to the ``request.uri.path``. As it descends the routing tree and passes through one +or more ``pathPrefix`` or ``path`` directives the "unmatched path" progressively gets "eaten into" from the +left until, in most cases, it eventually has been consumed completely. + +The two main directives are ``path`` and ``pathPrefix``. The ``path`` directive tries to match the complete remaining +unmatched path against the specified "path matchers", the ``pathPrefix`` directive only matches a prefix and passes the +remaining unmatched path to nested directives. Both directives automatically match a slash from the beginning, so +that matching slashes in a hierarchy of nested ``pathPrefix`` and ``path`` directives is usually not needed. + +Path directives take a variable amount of arguments. Each argument must be a ``PathMatcher`` or a string (which is +automatically converted to a path matcher using ``PathMatchers.segment``). In the case of ``path`` and ``pathPrefix``, +if multiple arguments are supplied, a slash is assumed between any of the supplied path matchers. The ``rawPathX`` +variants of those directives on the other side do no such preprocessing, so that slashes must be matched manually. + +Path Matchers +------------- + +A path matcher is a description of a part of a path to match. The simplest path matcher is ``PathMatcher.segment`` which +matches exactly one path segment against the supplied constant string. + +Other path matchers defined in ``PathMatchers`` match the end of the path (``PathMatchers.END``), a single slash +(``PathMatchers.SLASH``), or nothing at all (``PathMatchers.NEUTRAL``). + +Many path matchers are hybrids that can both match (by using them with one of the PathDirectives) and extract values, +i.e. they are :ref:`request-vals-java`. Extracting a path matcher value (i.e. using it with ``handleWithX``) is only +allowed if it nested inside a path directive that uses that path matcher and so specifies at which position the value +should be extracted from the path. + +Predefined path matchers allow extraction of various types of values: + +``PathMatchers.segment(String)`` + Strings simply match themselves and extract no value. + Note that strings are interpreted as the decoded representation of the path, so if they include a '/' character + this character will match "%2F" in the encoded raw URI! + +``PathMatchers.regex`` + You can use a regular expression instance as a path matcher, which matches whatever the regex matches and extracts + one ``String`` value. A ``PathMatcher`` created from a regular expression extracts either the complete match (if the + regex doesn't contain a capture group) or the capture group (if the regex contains exactly one capture group). + If the regex contains more than one capture group an ``IllegalArgumentException`` will be thrown. + +``PathMatchers.SLASH`` + Matches exactly one path-separating slash (``/``) character. + +``PathMatchers.END`` + Matches the very end of the path, similar to ``$`` in regular expressions. + +``PathMatchers.Segment`` + Matches if the unmatched path starts with a path segment (i.e. not a slash). + If so the path segment is extracted as a ``String`` instance. + +``PathMatchers.Rest`` + Matches and extracts the complete remaining unmatched part of the request's URI path as an (encoded!) String. + If you need access to the remaining *decoded* elements of the path use ``RestPath`` instead. + +``PathMatchers.intValue`` + Efficiently matches a number of decimal digits (unsigned) and extracts their (non-negative) ``Int`` value. The matcher + will not match zero digits or a sequence of digits that would represent an ``Int`` value larger than ``Integer.MAX_VALUE``. + +``PathMatchers.longValue`` + Efficiently matches a number of decimal digits (unsigned) and extracts their (non-negative) ``Long`` value. The matcher + will not match zero digits or a sequence of digits that would represent an ``Long`` value larger than ``Long.MAX_VALUE``. + +``PathMatchers.hexIntValue`` + Efficiently matches a number of hex digits and extracts their (non-negative) ``Int`` value. The matcher will not match + zero digits or a sequence of digits that would represent an ``Int`` value larger than ``Integer.MAX_VALUE``. + +``PathMatchers.hexLongValue`` + Efficiently matches a number of hex digits and extracts their (non-negative) ``Long`` value. The matcher will not + match zero digits or a sequence of digits that would represent an ``Long`` value larger than ``Long.MAX_VALUE``. + +``PathMatchers.uuid`` + Matches and extracts a ``java.util.UUID`` instance. + +``PathMatchers.NEUTRAL`` + A matcher that always matches, doesn't consume anything and extracts nothing. + Serves mainly as a neutral element in ``PathMatcher`` composition. + +``PathMatchers.segments`` + Matches all remaining segments as a list of strings. Note that this can also be "no segments" resulting in the empty + list. If the path has a trailing slash this slash will *not* be matched, i.e. remain unmatched and to be consumed by + potentially nested directives. + +Here's a collection of path matching examples: + +.. includecode:: ../../../code/docs/http/javadsl/PathDirectiveExampleTest.java + :include: path-examples \ No newline at end of file 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 5b23a230f3..d223cadc24 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 @@ -14,8 +14,8 @@ public class SimpleServerApp8 extends HttpApp { static Parameter x = Parameters.intValue("x"); static Parameter y = Parameters.intValue("y"); - static PathMatcher xSegment = PathMatchers.integerNumber(); - static PathMatcher ySegment = PathMatchers.integerNumber(); + static PathMatcher xSegment = PathMatchers.intValue(); + static PathMatcher ySegment = PathMatchers.intValue(); public static RouteResult multiply(RequestContext ctx, int x, int y) { int result = x * y; diff --git a/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/petstore/PetStoreExample.java b/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/petstore/PetStoreExample.java index c01dccfe35..9adaae6d01 100644 --- a/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/petstore/PetStoreExample.java +++ b/akka-http-tests/src/main/java/akka/http/javadsl/server/examples/petstore/PetStoreExample.java @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap; import static akka.http.javadsl.server.Directives.*; public class PetStoreExample { - static PathMatcher petId = PathMatchers.integerNumber(); + static PathMatcher petId = PathMatchers.intValue(); static RequestVal petEntity = RequestVals.entityAs(Jackson.jsonAs(Pet.class)); public static Route appRoute(final Map pets) { 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 983382b727..b17e6fbd43 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 @@ -20,8 +20,8 @@ public class SimpleServerApp extends HttpApp { static Parameter x = Parameters.intValue("x"); static Parameter y = Parameters.intValue("y"); - static PathMatcher xSegment = PathMatchers.integerNumber(); - static PathMatcher ySegment = PathMatchers.integerNumber(); + static PathMatcher xSegment = PathMatchers.intValue(); + static PathMatcher ySegment = PathMatchers.intValue(); static RequestVal bodyAsName = RequestVals.entityAs(Unmarshallers.String()); 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 54ffc0a955..ce46bdac60 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 @@ -216,7 +216,7 @@ public class PathDirectivesTest extends JUnitRouteTest { @Test public void testIntegerMatcher() { - PathMatcher age = PathMatchers.integerNumber(); + PathMatcher age = PathMatchers.intValue(); TestRoute route = testRoute( @@ -233,8 +233,8 @@ public class PathDirectivesTest extends JUnitRouteTest { public void testTwoVals() { // tests that `x` and `y` have different identities which is important for // retrieving the values - PathMatcher x = PathMatchers.integerNumber(); - PathMatcher y = PathMatchers.integerNumber(); + PathMatcher x = PathMatchers.intValue(); + PathMatcher y = PathMatchers.intValue(); TestRoute route = testRoute( @@ -254,7 +254,7 @@ public class PathDirectivesTest extends JUnitRouteTest { @Test public void testHexIntegerMatcher() { - PathMatcher color = PathMatchers.hexIntegerNumber(); + PathMatcher color = PathMatchers.hexIntValue(); TestRoute route = testRoute( @@ -267,7 +267,7 @@ public class PathDirectivesTest extends JUnitRouteTest { @Test public void testLongMatcher() { - PathMatcher bigAge = PathMatchers.longNumber(); + PathMatcher bigAge = PathMatchers.longValue(); TestRoute route = testRoute( @@ -280,7 +280,7 @@ public class PathDirectivesTest extends JUnitRouteTest { @Test public void testHexLongMatcher() { - PathMatcher code = PathMatchers.hexLongNumber(); + PathMatcher code = PathMatchers.hexLongValue(); TestRoute route = testRoute( 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 5bcf35e56e..4f874dfbfb 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 @@ -4,6 +4,7 @@ package akka.http.javadsl.server.values +import java.util.regex.Pattern import java.{ lang ⇒ jl, util ⇒ ju } import akka.http.impl.server.PathMatcherImpl @@ -14,6 +15,7 @@ import akka.japi.function.Function import scala.collection.JavaConverters._ import scala.reflect.ClassTag +import scala.util.matching.Regex /** * A PathMatcher is used to match the (yet unmatched) URI path of incoming requests. @@ -31,24 +33,96 @@ trait PathMatcher[T] extends RequestVal[T] { * A collection of predefined path matchers. */ object PathMatchers { + /** + * A PathMatcher that always matches, doesn't consume anything and extracts nothing. + * Serves mainly as a neutral element in PathMatcher composition. + */ val NEUTRAL: PathMatcher[Void] = matcher0(_.Neutral) + + /** + * A PathMatcher that matches a single slash character ('/'). + */ val SLASH: PathMatcher[Void] = matcher0(_.Slash) + + /** + * A PathMatcher that matches the very end of the requests URI path. + */ val END: PathMatcher[Void] = matcher0(_.PathEnd) + /** + * Creates a PathMatcher that consumes (a prefix of) the first path segment + * (if the path begins with a segment) and extracts a given value. + */ def segment(name: String): PathMatcher[String] = matcher(_ ⇒ name -> name) - def integerNumber: PathMatcher[jl.Integer] = matcher(_.IntNumber.asInstanceOf[PathMatcher1[jl.Integer]]) - def hexIntegerNumber: PathMatcher[jl.Integer] = matcher(_.HexIntNumber.asInstanceOf[PathMatcher1[jl.Integer]]) + /** + * A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Int value. + * The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger + * than [[java.lang.Integer.MAX_VALUE]]. + */ + def intValue: PathMatcher[jl.Integer] = matcher(_.IntNumber.asInstanceOf[PathMatcher1[jl.Integer]]) - def longNumber: PathMatcher[jl.Long] = matcher(_.LongNumber.asInstanceOf[PathMatcher1[jl.Long]]) - def hexLongNumber: PathMatcher[jl.Long] = matcher(_.HexLongNumber.asInstanceOf[PathMatcher1[jl.Long]]) + /** + * A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Int value. + * The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger + * than [[java.lang.Integer.MAX_VALUE]]. + */ + def hexIntValue: PathMatcher[jl.Integer] = matcher(_.HexIntNumber.asInstanceOf[PathMatcher1[jl.Integer]]) + /** + * A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Long value. + * The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger + * than [[java.lang.Long.MAX_VALUE]]. + */ + def longValue: PathMatcher[jl.Long] = matcher(_.LongNumber.asInstanceOf[PathMatcher1[jl.Long]]) + + /** + * A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Long value. + * The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger + * than [[java.lang.Long.MAX_VALUE]]. + */ + def hexLongValue: PathMatcher[jl.Long] = matcher(_.HexLongNumber.asInstanceOf[PathMatcher1[jl.Long]]) + + /** + * Creates a PathMatcher that consumes (a prefix of) the first path segment + * if the path begins with a segment (a prefix of) which matches the given regex. + * Extracts either the complete match (if the regex doesn't contain a capture group) or + * the capture group (if the regex contains exactly one). + * If the regex contains more than one capture group the method throws an IllegalArgumentException. + */ + def regex(regex: String): PathMatcher[String] = matcher(_ ⇒ regex.r) + + /** + * A PathMatcher that matches and extracts a java.util.UUID instance. + */ def uuid: PathMatcher[ju.UUID] = matcher(_.JavaUUID) + /** + * A PathMatcher that matches if the unmatched path starts with a path segment. + * If so the path segment is extracted as a String. + */ def segment: PathMatcher[String] = matcher(_.Segment) + + /** + * A PathMatcher that matches up to 128 remaining segments as a List[String]. + * This can also be no segments resulting in the empty list. + * If the path has a trailing slash this slash will *not* be matched. + */ def segments: PathMatcher[ju.List[String]] = matcher(_.Segments.map(_.asJava)) + + /** + * A PathMatcher that matches the given number of path segments (separated by slashes) as a List[String]. + * If there are more than ``count`` segments present the remaining ones will be left unmatched. + * If the path has a trailing slash this slash will *not* be matched. + */ def segments(maxNumber: Int): PathMatcher[ju.List[String]] = matcher(_.Segments(maxNumber).map(_.asJava)) + /** + * A PathMatcher that matches and extracts the complete remaining, + * unmatched part of the request's URI path as an (encoded!) String. + * If you need access to the remaining unencoded elements of the path + * use the `RestPath` matcher! + */ def rest: PathMatcher[String] = matcher(_.Rest) def segmentFromString[T](convert: Function[String, T], clazz: Class[T]): PathMatcher[T] =