Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
|@ref[post](method-directives/post.md) | Rejects all non-POST requests |
|@ref[provide](basic-directives/provide.md) | Injects a given value into a directive |
|@ref[put](method-directives/put.md) | Rejects all non-PUT requests |
|@ref[query](method-directives/query.md) | Rejects all non-QUERY requests |
|@ref[rawPathPrefix](path-directives/rawPathPrefix.md) | Applies the given matcher directly to a prefix of the unmatched path of the @apidoc[RequestContext], without implicitly consuming a leading slash |
|@ref[rawPathPrefixTest](path-directives/rawPathPrefixTest.md) | Checks whether the unmatchedPath has a prefix matched by the given `PathMatcher` |
|@ref[recoverRejections](basic-directives/recoverRejections.md) | Transforms rejections from the inner route with an @scala[`immutable.Seq[Rejection] => RouteResult`]@java[`Function<List<Rejection>, RouteResult>`] function |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
* [patch](patch.md)
* [post](post.md)
* [put](put.md)
* [query](query.md)

@@@
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# query

Matches requests with HTTP method `QUERY` (RFC 10008).

@@@ div { .group-scala }

## Signature

@@signature [MethodDirectives.scala](/http/src/main/scala/org/apache/pekko/http/scaladsl/server/directives/MethodDirectives.scala) { #query }

@@@

## Description

This directive filters the incoming request by its HTTP method. Only requests with
method `QUERY` are passed on to the inner route. All others are rejected with a
@apidoc[MethodRejection], which is translated into a `405 Method Not Allowed` response
by the default @ref[RejectionHandler](../../rejections.md#the-rejectionhandler).

The `QUERY` method is defined in [RFC 10008](https://www.rfc-editor.org/rfc/rfc10008).
It is a safe and idempotent method that requests the target resource to process the
enclosed content and respond with the result. Unlike `GET`, the `QUERY` method expects
a request body containing the query payload.

## Example

Scala
: @@snip [MethodDirectivesExamplesSpec.scala](/docs/src/test/scala/docs/http/scaladsl/server/directives/MethodDirectivesExamplesSpec.scala) { #query-method }

Java
: @@snip [MethodDirectivesExamplesTest.java](/docs/src/test/java/docs/http/javadsl/server/directives/MethodDirectivesExamplesTest.java) { #query }
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
import static org.apache.pekko.http.javadsl.server.Directives.put;

// #put
// #query
import static org.apache.pekko.http.javadsl.server.Directives.complete;
import static org.apache.pekko.http.javadsl.server.Directives.query;

// #query
// #method-example
import static org.apache.pekko.http.javadsl.server.Directives.complete;
import static org.apache.pekko.http.javadsl.server.Directives.method;
Expand Down Expand Up @@ -140,6 +145,17 @@ public void testPut() {
// #put
}

@Test
public void testQuery() {
// #query
final Route route = query(() -> complete("This is a QUERY request."));

testRoute(route)
.run(HttpRequest.QUERY("/").withEntity("query content"))
.assertEntity("This is a QUERY request.");
// #query
}

@Test
public void testMethodExample() {
// #method-example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ class MethodDirectivesExamplesSpec extends RoutingSpec with CompileOnlySpec {
// #put-method
}

"query-method" in {
// #query-method
val route = query { complete("This is a QUERY request.") }

// tests:
HttpRequest(method = HttpMethods.QUERY, uri = "/") ~> route ~> check {
responseAs[String] shouldEqual "This is a QUERY request."
}
// #query-method
}

"method-example" in {
// #method-example
val route = method(HttpMethods.PUT) { complete("This is a PUT request.") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private HttpMethods() {}
public static final HttpMethod PATCH = org.apache.pekko.http.scaladsl.model.HttpMethods.PATCH();
public static final HttpMethod POST = org.apache.pekko.http.scaladsl.model.HttpMethods.POST();
public static final HttpMethod PUT = org.apache.pekko.http.scaladsl.model.HttpMethods.PUT();
public static final HttpMethod QUERY = org.apache.pekko.http.scaladsl.model.HttpMethods.QUERY();
public static final HttpMethod TRACE = org.apache.pekko.http.scaladsl.model.HttpMethods.TRACE();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,9 @@ public static HttpRequest PATCH(String uri) {
public static HttpRequest OPTIONS(String uri) {
return create(uri).withMethod(HttpMethods.OPTIONS);
}

/** A default QUERY request to be modified using the `withX` methods. */
public static HttpRequest QUERY(String uri) {
return create(uri).withMethod(HttpMethods.QUERY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.pekko.http.javadsl.model.headers;

import org.apache.pekko.http.impl.util.Util;
import org.apache.pekko.http.javadsl.model.MediaRange;

/**
* Model for the `Accept-Query` header. Specification:
* https://www.rfc-editor.org/rfc/rfc10008.html#section-3
*/
public abstract class AcceptQuery extends org.apache.pekko.http.scaladsl.model.HttpHeader {
public abstract Iterable<MediaRange> getMediaRanges();

public static AcceptQuery create(MediaRange... mediaRanges) {
return new org.apache.pekko.http.scaladsl.model.headers.Accept$minusQuery(
Util.<MediaRange, org.apache.pekko.http.scaladsl.model.MediaRange>convertArray(
mediaRanges));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,104 @@ package org.apache.pekko.http.impl.model.parser
import scala.collection.immutable.TreeMap

import org.apache.pekko
import org.parboiled2.Parser
import org.parboiled2.{ CharPredicate, Parser, Rule1 }
import pekko.http.scaladsl.model.headers._
import pekko.http.scaladsl.model.{ MediaRange, MediaRanges }
import pekko.http.scaladsl.model.{ MediaRange, MediaRanges, ParsingException }
import pekko.http.impl.util._

private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonActions =>
import CharacterClasses._

private val acceptQueryMediaRangeChar = tchar ++ '/'
private val sfStringChar = CharPredicate('\u0020' to '\u0021', '\u0023' to '\u005B', '\u005D' to '\u007E')
private val sfStringEscapedChar = CharPredicate('"', '\\')

// http://tools.ietf.org/html/rfc7231#section-5.3.2
def accept = rule {
zeroOrMore(`media-range-decl`).separatedBy(listSep) ~ EOI ~> (Accept(_))
}

// https://www.rfc-editor.org/rfc/rfc10008.html#section-3
// Accept-Query uses Structured Fields syntax (RFC 9651): media types may appear
// as tokens (application/json) or quoted strings ("application/jsonpath").
def `accept-query` = rule {
zeroOrMore(`accept-query-media-range-decl`).separatedBy(listSep) ~ EOI ~> (`Accept-Query`(_))
}

def `accept-query-media-range-decl` = rule {
`accept-query-media-range-def` ~ zeroOrMore(`accept-query-param`) ~> {
(mediaRange: (String, String), params: Seq[(String, String)]) =>
val (main, sub) = mediaRange
val mediaRangeParams = TreeMap(params: _*)
if (sub == "*") {
val mainLower = main.toRootLowerCase
MediaRanges.getForKey(mainLower) match {
case Some(registered) =>
if (mediaRangeParams.isEmpty) registered else MediaRange.customWithParams(mainLower, mediaRangeParams)
case None => MediaRange.customWithParams(mainLower, mediaRangeParams)
}
} else {
MediaRange(getMediaType(main, sub, mediaRangeParams contains "charset", mediaRangeParams))
}
}
}

def `accept-query-media-range-def` = rule {
`sf-token` ~> (parseAcceptQueryMediaRange _) |
`sf-string` ~> (parseAcceptQueryMediaRange _)
}

def `accept-query-param` = rule {
';' ~ OWS ~ `sf-key` ~ '=' ~ `sf-param-value` ~> ((_, _))
}

def `sf-param-value`: Rule1[String] = rule {
`sf-string` | `sf-token`
}

def `sf-key`: Rule1[String] = rule {
capture((LOWER_ALPHA | '*') ~ zeroOrMore(LOWER_ALPHA | DIGIT | '_' | '-' | '.' | '*'))
}

def `sf-token`: Rule1[String] = rule {
capture((ALPHA | '*') ~ zeroOrMore(tchar | ':' | '/')) ~ OWS
}

def `sf-string`: Rule1[String] = rule {
DQUOTE ~ clearSB() ~ zeroOrMore(`sf-string-char` ~ appendSB() | '\\' ~ `sf-string-escaped-char` ~ appendSB()) ~
push(sb.toString) ~ DQUOTE ~ OWS
}

def `sf-string-char` = rule { sfStringChar }

def `sf-string-escaped-char` = rule { sfStringEscapedChar }

private def parseAcceptQueryMediaRange(value: String): (String, String) = {
var slashIdx = -1
var ix = 0
while (ix < value.length) {
val ch = value.charAt(ix)
if (ch == '/') {
if (slashIdx >= 0) invalidAcceptQueryMediaRange(value)
slashIdx = ix
} else if (!acceptQueryMediaRangeChar(ch)) invalidAcceptQueryMediaRange(value)
ix += 1
}

if (slashIdx <= 0 || slashIdx == value.length - 1) invalidAcceptQueryMediaRange(value)

val main = value.substring(0, slashIdx)
val sub = value.substring(slashIdx + 1)
if (main.indexOf('*') >= 0 && main != "*") invalidAcceptQueryMediaRange(value)
if (sub.indexOf('*') >= 0 && sub != "*") invalidAcceptQueryMediaRange(value)
if (main == "*" && sub != "*") invalidAcceptQueryMediaRange(value)

(main, sub)
}

private def invalidAcceptQueryMediaRange(value: String): Nothing =
throw ParsingException(s"Illegal Accept-Query media range '$value'")

def `media-range-decl` = rule {
`media-range-def` ~ OWS ~ zeroOrMore(ws(';') ~ parameter) ~> { (main, sub, params) =>
if (sub == "*") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ private[http] object HeaderParser {
"accept-charset",
"accept-encoding",
"accept-language",
"accept-query",
"accept-ranges",
"access-control-allow-credentials",
"access-control-allow-headers",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ object HttpMethods extends ObjectRegistry[String, HttpMethod] {
val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon))
val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon))
val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon))
val QUERY = register(HttpMethod("QUERY" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Expected, contentLengthAllowed = contentLengthAllowedCommon))
val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , requestEntityAcceptance = Disallowed, contentLengthAllowed = contentLengthAllowedCommon))
// format: ON

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ object MediaRange {
Custom(mainType.toRootLowerCase, ps, q)
}

private[http] def customWithParams(mainType: String, params: Map[String, String],
qValue: Float = 1.0f): MediaRange =
Custom(mainType.toRootLowerCase, params, qValue)

final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable {
require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0")
def mainType = mediaType.mainType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ import scala.reflect.ClassTag
import scala.util.{ Failure, Success, Try }
import scala.annotation.tailrec
import scala.collection.immutable
import org.parboiled2.CharPredicate
import org.parboiled2.util.Base64
import pekko.event.Logging
import pekko.http.impl.util._
import pekko.http.impl.model.parser.CharacterClasses
import pekko.http.impl.model.parser.CharacterClasses.`attr-char`
import pekko.http.javadsl.{ model => jm }
import pekko.http.scaladsl.model._
Expand Down Expand Up @@ -232,6 +234,60 @@ final case class `Accept-Ranges`(rangeUnits: immutable.Seq[RangeUnit]) extends j
def getRangeUnits: Iterable[jm.headers.RangeUnit] = rangeUnits.asJava
}

// https://www.rfc-editor.org/rfc/rfc10008.html#section-3
object `Accept-Query` extends ModeledCompanion[`Accept-Query`] {
def apply(firstMediaRange: MediaRange, otherMediaRanges: MediaRange*): `Accept-Query` =
apply(firstMediaRange +: otherMediaRanges)

private val sfTokenChar: CharPredicate = CharacterClasses.tchar ++ ":/"

private implicit val mediaRangeRenderer: Renderer[MediaRange] = new Renderer[MediaRange] {
def render[R <: Rendering](r: R, mediaRange: MediaRange): r.type = {
renderSfTokenOrString(r, mediaRangeValue(mediaRange))
mediaRange.params.foreach {
case (key, value) =>
r ~~ ';' ~~ key.toRootLowerCase ~~ '='
renderSfTokenOrString(r, value)
}
r
}
}

implicit val mediaRangesRenderer: Renderer[immutable.Iterable[MediaRange]] = Renderer.defaultSeqRenderer[MediaRange] // cache

private def mediaRangeValue(mediaRange: MediaRange): String =
mediaRange match {
case MediaRange.One(mediaType, _) => mediaType.mainType + '/' + mediaType.subType
case _ => mediaRange.mainType + "/*"
}

private def renderSfTokenOrString[R <: Rendering](r: R, value: String): r.type =
if (isSfToken(value)) r ~~ value else r ~~#! value

private def isSfToken(value: String): Boolean =
value.nonEmpty && isSfTokenStart(value.charAt(0)) && {
var ix = 1
var valid = true
while (valid && ix < value.length) {
valid = sfTokenChar(value.charAt(ix))
ix += 1
}
valid
}

private def isSfTokenStart(ch: Char): Boolean =
CharacterClasses.ALPHA(ch) || ch == '*'
}
final case class `Accept-Query`(mediaRanges: immutable.Seq[MediaRange]) extends jm.headers.AcceptQuery
with ResponseHeader {
import `Accept-Query`.mediaRangesRenderer
def renderValue[R <: Rendering](r: R): r.type = r ~~ mediaRanges
protected def companion = `Accept-Query`

/** Java API */
def getMediaRanges: Iterable[jm.MediaRange] = mediaRanges.asJava
}

// https://www.w3.org/TR/cors/#access-control-allow-credentials-response-header
object `Access-Control-Allow-Credentials` extends ModeledCompanion[`Access-Control-Allow-Credentials`]
final case class `Access-Control-Allow-Credentials`(allow: Boolean)
Expand Down
Loading