aboutsummaryrefslogtreecommitdiffstats
path: root/container-core
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@gmail.com>2022-04-07 18:44:22 +0200
committerGitHub <noreply@github.com>2022-04-07 18:44:22 +0200
commit7563ab1357379a560de5622750d817aac6bd117c (patch)
tree20aa20fb7e61838a2fe2b0f9dacfafcd5e5bc540 /container-core
parentfac065affc2d04e4b927e98a732b046fa73b43cf (diff)
parent3efc8f5ab5d8e8788dc4e2f921c95d03a672d1e1 (diff)
Merge branch 'master' into bratseth/inputs
Diffstat (limited to 'container-core')
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java2
-rw-r--r--container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java2
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/HttpURL.java451
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/Path.java9
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApi.java12
-rw-r--r--container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java13
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/HttpURLTest.java202
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/PathTest.java9
-rw-r--r--container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java2
9 files changed, 31 insertions, 671 deletions
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java
index 2580b4a6ac0..a5d133b9eb4 100644
--- a/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/DiscFilterRequest.java
@@ -52,7 +52,7 @@ public class DiscFilterRequest {
untreatedHeaders = new HeaderFields();
parent.copyHeaders(untreatedHeaders);
- untreatedParams = new HashMap<>(parent.parameters());
+ untreatedParams = new HashMap<>(parent.parameters()); // TODO jonmv: probably a bug that this is not deep-copied
}
public String getMethod() { return parent.getMethod().name(); }
diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
index 84d343c0c8e..72057563e36 100644
--- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
+++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/FormPostRequestHandler.java
@@ -37,7 +37,6 @@ import static com.yahoo.jdisc.http.server.jetty.CompletionHandlerUtils.NOOP_COMP
* parsed the form parameters and merged them into the request's parameters.
*
* @author bakksjo
- * $Id$
*/
class FormPostRequestHandler extends AbstractRequestHandler implements ContentChannel, DelegatedRequestHandler {
@@ -84,7 +83,6 @@ class FormPostRequestHandler extends AbstractRequestHandler implements ContentCh
completionHandler.completed();
}
- @SuppressWarnings("try")
@Override
public void close(final CompletionHandler completionHandler) {
try (final ResourceReference ref = requestReference) {
diff --git a/container-core/src/main/java/com/yahoo/restapi/HttpURL.java b/container-core/src/main/java/com/yahoo/restapi/HttpURL.java
deleted file mode 100644
index e890b0fe71a..00000000000
--- a/container-core/src/main/java/com/yahoo/restapi/HttpURL.java
+++ /dev/null
@@ -1,451 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.restapi;
-
-import ai.vespa.validation.StringWrapper;
-import com.yahoo.net.DomainName;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.OptionalInt;
-import java.util.StringJoiner;
-import java.util.function.Consumer;
-import java.util.function.UnaryOperator;
-
-import static ai.vespa.validation.Validation.require;
-import static ai.vespa.validation.Validation.requireInRange;
-import static java.net.URLDecoder.decode;
-import static java.net.URLEncoder.encode;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Collections.unmodifiableMap;
-import static java.util.Objects.requireNonNull;
-
-/**
- * This is the best class for creating, manipulating and inspecting HTTP URLs, because:
- * <ul>
- * <li>It is more restrictive than {@link URI}, but with a richer construction API, reducing risk of blunder.
- * <ul>
- * <li>Scheme must be HTTP or HTTPS.</li>
- * <li>Authority must be a {@link DomainName}, with an optional port.</li>
- * <li>{@link Path} must be normalized at all times.</li>
- * <li>Only {@link Query} is allowed, in addition to the above.</li>
- * </ul>
- * </li>
- * <li>
- * It contains all those helpful builder methods that {@link URI} has none of.
- * <ul>
- * <li>{@link Path} can be parsed, have segments or other paths appended, and cut.</li>
- * <li>{@link Query} can be parsed, and keys and key-value pairs can be inserted or removed.</li>
- * </ul>
- * All these (except the parse methods) operate on <em>decoded</em> values.
- * </li>
- * <li>It makes it super-easy to use a {@link StringWrapper} for validation of path and query segments.</li>
- * </ul>
- *
- * @author jonmv
- */
-public class HttpURL {
-
- private final Scheme scheme;
- private final DomainName domain;
- private final int port;
- private final Path path;
- private final Query query;
-
- private HttpURL(Scheme scheme, DomainName domain, int port, Path path, Query query) {
- this.scheme = requireNonNull(scheme);
- this.domain = requireNonNull(domain);
- this.port = requireInRange(port, "port number", -1, (1 << 16) - 1);
- this.path = requireNonNull(path);
- this.query = requireNonNull(query);
- }
-
- public static HttpURL create(Scheme scheme, DomainName domain, int port, Path path, Query query) {
- return new HttpURL(scheme, domain, port, path, query);
- }
-
- public static HttpURL create(Scheme scheme, DomainName domain, int port, Path path) {
- return create(scheme, domain, port, path, Query.empty());
- }
-
- public static HttpURL create(Scheme scheme, DomainName domain, int port) {
- return create(scheme, domain, port, Path.empty(), Query.empty());
- }
-
- public static HttpURL create(Scheme scheme, DomainName domain) {
- return create(scheme, domain, -1);
- }
-
- public static HttpURL from(URI uri) {
- return from(uri, HttpURL::requirePathSegment, HttpURL::requireNothing);
- }
-
- public static HttpURL from(URI uri, Consumer<String> pathValidator, Consumer<String> queryValidator) {
- if ( ! uri.normalize().equals(uri))
- throw new IllegalArgumentException("uri should be normalized, but got: " + uri);
-
- return create(Scheme.of(uri.getScheme()),
- DomainName.of(requireNonNull(uri.getHost(), "URI must specify a host")),
- uri.getPort(),
- Path.parse(uri.getRawPath(), pathValidator),
- Query.parse(uri.getRawQuery(), queryValidator));
- }
-
- public HttpURL withScheme(Scheme scheme) {
- return create(scheme, domain, port, path, query);
- }
-
- public HttpURL withDomain(DomainName domain) {
- return create(scheme, domain, port, path, query);
- }
-
- public HttpURL withPort(int port) {
- return create(scheme, domain, port, path, query);
- }
-
- public HttpURL withoutPort() {
- return create(scheme, domain, -1, path, query);
- }
-
- public HttpURL withPath(Path path) {
- return create(scheme, domain, port, path, query);
- }
-
- public HttpURL withQuery(Query query) {
- return create(scheme, domain, port, path, query);
- }
-
- public Scheme scheme() {
- return scheme;
- }
-
- public DomainName domain() {
- return domain;
- }
-
- public OptionalInt port() {
- return port == -1 ? OptionalInt.empty() : OptionalInt.of(port);
- }
-
- public Path path() {
- return path;
- }
-
- public Query query() {
- return query;
- }
-
- /** Returns an absolute, hierarchical URI representing this HTTP URL. */
- public URI asURI() {
- try {
- return new URI(scheme.name() + "://" + domain.value() + (port == -1 ? "" : ":" + port) + path.raw() + query.raw());
- }
- catch (URISyntaxException e) {
- throw new IllegalStateException("invalid URI, this should not happen", e);
- }
- }
-
- /** Require that the given string (possibly decoded multiple times) contains none of {@code '/', '?', '#'}, and isn't either of {@code "", ".", ".."}. */
- public static String requirePathSegment(String value) {
- while ( ! value.equals(value = decode(value, UTF_8)));
- require( ! value.contains("/"), value, "path segment decoded cannot contain '/'");
- require( ! value.contains("?"), value, "path segment decoded cannot contain '?'");
- require( ! value.contains("#"), value, "path segment decoded cannot contain '#'");
- return Path.requireNonNormalizable(value);
- }
-
- private static void requireNothing(String value) { }
-
- public static class Path {
-
- private final List<String> segments;
- private final boolean trailingSlash;
- private final UnaryOperator<String> validator;
-
- private Path(List<String> segments, boolean trailingSlash, UnaryOperator<String> validator) {
- this.segments = requireNonNull(segments);
- this.trailingSlash = trailingSlash;
- this.validator = requireNonNull(validator);
- }
-
- /** Creates a new, empty path, with a trailing slash, using {@link HttpURL#requirePathSegment} for segment validation. */
- public static Path empty() {
- return empty(HttpURL::requirePathSegment);
- }
-
- /** Creates a new, empty path, with a trailing slash, using the indicated validator for segments. */
- public static Path empty(Consumer<String> validator) {
- return new Path(List.of(), true, segmentValidator(validator));
- }
-
- /** Creates a new path with the given <em>decoded</em> segments. */
- public static Path from(List<String> segments) {
- return from(segments, __ -> { });
- }
-
- /** Creates a new path with the given <em>decoded</em> segments, and the validator applied to each segment. */
- public static Path from(List<String> segments, Consumer<String> validator) {
- return empty(validator).append(segments, true);
- }
-
- /** Parses the given raw, normalized path string; this ignores whether the path is absolute or relative. */
- public static Path parse(String raw) {
- return parse(raw, HttpURL::requirePathSegment);
- }
-
- /** Parses the given raw, normalized path string; this ignores whether the path is absolute or relative.) */
- public static Path parse(String raw, Consumer<String> validator) {
- Path base = new Path(List.of(), raw.endsWith("/"), segmentValidator(validator));
- if (raw.startsWith("/")) raw = raw.substring(1);
- if (raw.isEmpty()) return base;
- List<String> segments = new ArrayList<>();
- for (String segment : raw.split("/")) segments.add(decode(segment, UTF_8));
- if (segments.isEmpty()) requireNonNormalizable(""); // Raw path was only slashes.
- return base.append(segments);
- }
-
- private static UnaryOperator<String> segmentValidator(Consumer<String> validator) {
- requireNonNull(validator, "segment validator cannot be null");
- return value -> {
- requireNonNormalizable(value);
- validator.accept(value);
- return value;
- };
- }
-
- private static String requireNonNormalizable(String segment) {
- return require( ! (segment.isEmpty() || segment.equals(".") || segment.equals("..")),
- segment, "path segments cannot be \"\", \".\", or \"..\"");
- }
-
- /** Returns a copy of this where only the first segments are retained, and with a trailing slash. */
- public Path head(int count) {
- return count == segments.size() ? this : new Path(segments.subList(0, count), true, validator);
- }
-
- /** Returns a copy of this where only the last segments are retained. */
- public Path tail(int count) {
- return count == segments.size() ? this : new Path(segments.subList(segments.size() - count, segments.size()), trailingSlash, validator);
- }
-
- /** Returns a copy of this where the first segments are skipped. */
- public Path skip(int count) {
- return count == 0 ? this : new Path(segments.subList(count, segments.size()), trailingSlash, validator);
- }
-
- /** Returns a copy of this where the last segments are cut off, and with a trailing slash. */
- public Path cut(int count) {
- return count == 0 ? this : new Path(segments.subList(0, segments.size() - count), true, validator);
- }
-
- /** Returns a copy of this with the <em>decoded</em> segment appended at the end; it may not be either of {@code ""}, {@code "."} or {@code ".."}. */
- public Path append(String segment) {
- return append(List.of(segment), trailingSlash);
- }
-
- /** Returns a copy of this all segments of the other path appended, with a trailing slash as per the appendage. */
- public Path append(Path other) {
- return append(other.segments, other.trailingSlash);
- }
-
- /** Returns a copy of this all given segments appended, with a trailing slash as per this path. */
- public Path append(List<String> segments) {
- return append(segments, trailingSlash);
- }
-
- private Path append(List<String> segments, boolean trailingSlash) {
- List<String> copy = new ArrayList<>(this.segments);
- for (String segment : segments) copy.add(validator.apply(segment));
- return new Path(copy, trailingSlash, validator);
- }
-
- /** Whether this path has a trailing slash. */
- public boolean hasTrailingSlash() {
- return trailingSlash;
- }
-
- /** Returns a copy of this which encodes a trailing slash. */
- public Path withTrailingSlash() {
- return new Path(segments, true, validator);
- }
-
- /** Returns a copy of this which does not encode a trailing slash. */
- public Path withoutTrailingSlash() {
- return new Path(segments, false, validator);
- }
-
- /** The <em>URL decoded</em> segments that make up this path; never {@code null}, {@code ""}, {@code "."} or {@code ".."}. */
- public List<String> segments() {
- return Collections.unmodifiableList(segments);
- }
-
- /** A raw path string which parses to this, by splitting on {@code "/"}, and then URL decoding. */
- String raw() {
- StringJoiner joiner = new StringJoiner("/", "/", trailingSlash ? "/" : "").setEmptyValue(trailingSlash ? "/" : "");
- for (String segment : segments) joiner.add(encode(segment, UTF_8));
- return joiner.toString();
- }
-
- /** Intentionally not usable for constructing new URIs. Use {@link HttpURL} for that instead. */
- @Override
- public String toString() {
- return "path '" + raw() + "'";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Path path = (Path) o;
- return trailingSlash == path.trailingSlash && segments.equals(path.segments);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(segments, trailingSlash);
- }
-
- }
-
-
- public static class Query {
-
- private final Map<String, String> values;
- private final UnaryOperator<String> validator;
-
- private Query(Map<String, String> values, UnaryOperator<String> validator) {
- this.values = requireNonNull(values);
- this.validator = requireNonNull(validator);
- }
-
- /** Creates a new, empty query part. */
- public static Query empty() {
- return empty(__ -> { });
- }
-
- /** Creates a new, empty query part, using the indicated string wrapper for keys and non-null values. */
- public static Query empty(Consumer<String> validator) {
- return new Query(Map.of(), entryValidator(validator));
- }
-
- /** Creates a new query part with the given <em>decoded</em> values. */
- public static Query from(Map<String, String> values) {
- return from(values, __ -> { });
- }
-
- /** Creates a new query part with the given <em>decoded</em> values, and the validator applied to each pair. */
- public static Query from(Map<String, String> values, Consumer<String> validator) {
- return empty(validator).merge(values);
- }
-
- /** Parses the given raw query string. */
- public static Query parse(String raw) {
- return parse(raw, __-> { });
- }
- /** Parses the given raw query string, using the indicated string wrapper to hold keys and non-null values. */
- public static Query parse(String raw, Consumer<String> validator) {
- if (raw == null) return empty(validator);
- Map<String, String> values = new LinkedHashMap<>();
- for (String pair : raw.split("&")) {
- int split = pair.indexOf("=");
- String key, value;
- if (split == -1) { key = pair; value = null; }
- else { key = pair.substring(0, split); value = pair.substring(split + 1); }
- values.put(decode(key, UTF_8), value == null ? null : decode(value, UTF_8));
- }
- return empty(validator).merge(values);
- }
-
- private static UnaryOperator<String> entryValidator(Consumer<String> validator) {
- requireNonNull(validator);
- return value -> {
- validator.accept(value);
- return value;
- };
- }
-
- /** Returns a copy of this with the <em>decoded</em> non-null key pointing to the <em>decoded</em> non-null value. */
- public Query put(String key, String value) {
- Map<String, String> copy = new LinkedHashMap<>(values);
- copy.put(validator.apply(requireNonNull(key)), validator.apply(requireNonNull(value)));
- return new Query(copy, validator);
- }
-
- /** Returns a copy of this with the <em>decoded</em> non-null key pointing to "nothing". */
- public Query add(String key) {
- Map<String, String> copy = new LinkedHashMap<>(values);
- copy.put(validator.apply(requireNonNull(key)), null);
- return new Query(copy, validator);
- }
-
- /** Returns a copy of this without any key-value pair with the <em>decoded</em> key. */
- public Query remove(String key) {
- Map<String, String> copy = new LinkedHashMap<>(values);
- copy.remove(validator.apply(requireNonNull(key)));
- return new Query(copy, validator);
- }
-
- /** Returns a copy of this with all mappings from the other query added to this, possibly overwriting existing mappings. */
- public Query merge(Query other) {
- return merge(other.values);
- }
-
- /** Returns a copy of this with all given mappings added to this, possibly overwriting existing mappings. */
- public Query merge(Map<String, String> values) {
- Map<String, String> copy = new LinkedHashMap<>(this.values);
- values.forEach((key, value) -> copy.put(validator.apply(requireNonNull(key, "keys cannot be null")),
- value == null ? null : validator.apply(value)));
- return new Query(copy, validator);
- }
-
- /** The <em>URL decoded</em> key-value pairs that make up this query; keys and values may be {@code ""}, and values are {@code null} when only key was specified. */
- public Map<String, String> entries() {
- return unmodifiableMap(values);
- }
-
- /** A raw query string, with {@code '?'} prepended, that parses to this, by splitting on {@code "&"}, then on {@code "="}, and then URL decoding; or the empty string if this is empty. */
- private String raw() {
- StringJoiner joiner = new StringJoiner("&", "?", "").setEmptyValue("");
- values.forEach((key, value) -> joiner.add(encode(key, UTF_8) +
- (value == null ? "" : "=" + encode(value, UTF_8))));
- return joiner.toString();
- }
-
- /** Intentionally not usable for constructing new URIs. Use {@link HttpURL} for that instead. */
- @Override
- public String toString() {
- return "query '" + raw() + "'";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Query query = (Query) o;
- return values.equals(query.values);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(values);
- }
-
- }
-
-
- public enum Scheme {
- http,
- https;
- public static Scheme of(String scheme) {
- if (scheme.equalsIgnoreCase(http.name())) return http;
- if (scheme.equalsIgnoreCase(https.name())) return https;
- throw new IllegalArgumentException("scheme must be HTTP or HTTPS");
- }
- }
-
-}
diff --git a/container-core/src/main/java/com/yahoo/restapi/Path.java b/container-core/src/main/java/com/yahoo/restapi/Path.java
index c639432db89..40f281a948e 100644
--- a/container-core/src/main/java/com/yahoo/restapi/Path.java
+++ b/container-core/src/main/java/com/yahoo/restapi/Path.java
@@ -1,6 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;
+import ai.vespa.http.HttpURL;
+
import java.net.URI;
import java.util.HashMap;
import java.util.List;
@@ -103,6 +105,13 @@ public class Path {
return rest;
}
+ /**
+ * The path this holds.
+ */
+ public HttpURL.Path getPath() {
+ return path;
+ }
+
@Override
public String toString() {
return path.toString();
diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApi.java b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
index 353ac3eb5cc..5dfb19029cc 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;
+import ai.vespa.http.HttpURL;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.container.jdisc.AclMapping;
import com.yahoo.container.jdisc.HttpRequest;
@@ -128,12 +129,9 @@ public interface RestApi {
Optional<RequestContent> requestContent();
RequestContent requestContentOrThrow();
ObjectMapper jacksonJsonMapper();
- /**
- * Creates a URI builder pre-initialized with scheme, host and port.
- * Intended for response generation (e.g for interactive REST APIs).
- * DO NOT USE FOR CUSTOM ROUTING.
- */
- UriBuilder uriBuilder();
+ /** Scheme, domain and port, for the original request. <em>Use this only for generating resources links, not for custom routing!</em> */
+ // TODO: this needs to include path and query as well, to be useful for generating resource links that need not be rewritten.
+ HttpURL baseRequestURL();
AclMapping.Action aclAction();
Optional<Principal> userPrincipal();
Principal userPrincipalOrThrow();
@@ -154,9 +152,11 @@ public interface RestApi {
}
interface PathParameters extends Parameters {
+ HttpURL.Path getFullPath();
Optional<HttpURL.Path> getRest();
}
interface QueryParameters extends Parameters {
+ HttpURL.Query getFullQuery();
List<String> getStringList(String name);
}
interface Headers extends Parameters {}
diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
index cc243a3e92b..1bde8d635a5 100644
--- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
+++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java
@@ -1,6 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;
+import ai.vespa.http.HttpURL;
+import ai.vespa.http.HttpURL.Query;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yahoo.container.jdisc.AclMapping;
import com.yahoo.container.jdisc.HttpRequest;
@@ -398,7 +400,7 @@ class RestApiImpl implements RestApi {
return requestContent().orElseThrow(() -> new RestApiException.BadRequest("Request content missing"));
}
@Override public ObjectMapper jacksonJsonMapper() { return jacksonJsonMapper; }
- @Override public UriBuilder uriBuilder() {
+ @Override public HttpURL baseRequestURL() {
URI uri = request.getUri();
// Reconstruct the URI used by the client to access the API.
// This is needed for producing URIs in the response that links to other parts of the Rest API.
@@ -408,7 +410,7 @@ class RestApiImpl implements RestApi {
if (hostHeader == null || hostHeader.isBlank()) {
hostHeader = request.getHeader("Host");
}
- if (hostHeader != null && !hostHeader.isBlank()) {
+ if (hostHeader != null && ! hostHeader.isBlank()) {
sb.append(hostHeader);
} else {
sb.append(uri.getHost());
@@ -416,7 +418,7 @@ class RestApiImpl implements RestApi {
sb.append(":").append(uri.getPort());
}
}
- return new UriBuilder(sb.toString());
+ return HttpURL.from(URI.create(sb.toString()));
}
@Override public AclMapping.Action aclAction() { return aclAction; }
@Override public Optional<Principal> userPrincipal() {
@@ -435,10 +437,12 @@ class RestApiImpl implements RestApi {
return getString(name)
.orElseThrow(() -> new RestApiException.BadRequest("Path parameter '" + name + "' is missing"));
}
+ @Override public HttpURL.Path getFullPath() {
+ return pathMatcher.getPath();
+ }
@Override public Optional<HttpURL.Path> getRest() {
return Optional.ofNullable(pathMatcher.getRest());
}
-
}
private class QueryParametersImpl implements RestApi.RequestContext.QueryParameters {
@@ -452,6 +456,7 @@ class RestApiImpl implements RestApi {
if (result == null) return List.of();
return List.copyOf(result);
}
+ @Override public HttpURL.Query getFullQuery() { return Query.empty().add(request.getJDiscRequest().parameters()); }
}
private class HeadersImpl implements RestApi.RequestContext.Headers {
diff --git a/container-core/src/test/java/com/yahoo/restapi/HttpURLTest.java b/container-core/src/test/java/com/yahoo/restapi/HttpURLTest.java
deleted file mode 100644
index 858513c2a69..00000000000
--- a/container-core/src/test/java/com/yahoo/restapi/HttpURLTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.restapi;
-
-import ai.vespa.validation.Name;
-import com.yahoo.net.DomainName;
-import com.yahoo.restapi.HttpURL.Query;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.OptionalInt;
-import java.util.function.Consumer;
-
-import static com.yahoo.net.DomainName.localhost;
-import static com.yahoo.restapi.HttpURL.Scheme.http;
-import static com.yahoo.restapi.HttpURL.Scheme.https;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/**
- * @author jonmv
- */
-class HttpURLTest {
-
- @Test
- void testConversionBackAndForth() {
- for (String uri : List.of("http://minimal",
- "http://empty.query?",
- "http://zero-port:0?no=path",
- "http://only-path/",
- "https://strange/queries?=&foo",
- "https://weirdness?=foo",
- "https://encoded/%3F%3D%26%2F?%3F%3D%26%2F=%3F%3D%26%2F",
- "https://host.at.domain:123/one/two/?three=four&five")) {
- Consumer<String> pathValidator = __ -> { };
- assertEquals(uri, HttpURL.from(URI.create(uri), pathValidator, pathValidator).asURI().toString(),
- "uri '" + uri + "' should be returned unchanged");
- }
- }
-
- @Test
- void testModification() {
- HttpURL url = HttpURL.create(http, localhost).withPath(HttpURL.Path.empty(Name::of));
- assertEquals(http, url.scheme());
- assertEquals(localhost, url.domain());
- assertEquals(OptionalInt.empty(), url.port());
- assertEquals(HttpURL.Path.empty(Name::of), url.path());
- assertEquals(HttpURL.Query.empty(Name::of), url.query());
-
- url = url.withScheme(https)
- .withDomain(DomainName.of("domain"))
- .withPort(0)
- .withPath(url.path().append("foo").withoutTrailingSlash())
- .withQuery(url.query().put("boo", "bar").add("baz"));
- assertEquals(https, url.scheme());
- assertEquals(DomainName.of("domain"), url.domain());
- assertEquals(OptionalInt.of(0), url.port());
- assertEquals(HttpURL.Path.parse("/foo", Name::of), url.path());
- assertEquals(HttpURL.Query.parse("boo=bar&baz", Name::of), url.query());
- }
-
- @Test
- void testInvalidURIs() {
- assertEquals("scheme must be HTTP or HTTPS",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("file:/txt"))).getMessage());
-
- assertEquals("URI must specify a host",
- assertThrows(NullPointerException.class,
- () -> HttpURL.from(URI.create("http:///foo"))).getMessage());
-
- assertEquals("port number must be at least '-1' and at most '65535', but got: '65536'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo:65536/bar"))).getMessage());
-
- assertEquals("uri should be normalized, but got: http://foo//",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo//"))).getMessage());
-
- assertEquals("uri should be normalized, but got: http://foo/./",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo/./"))).getMessage());
-
- assertEquals("path segments cannot be \"\", \".\", or \"..\", but got: '..'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo/.."))).getMessage());
-
- assertEquals("path segments cannot be \"\", \".\", or \"..\", but got: '..'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo/.%2E"))).getMessage());
-
- assertEquals("name must match '[A-Za-z][A-Za-z0-9_-]{0,63}', but got: '/'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo/%2F"), Name::of, Name::of)).getMessage());
-
- assertEquals("name must match '[A-Za-z][A-Za-z0-9_-]{0,63}', but got: '/'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo?%2F"), Name::of, Name::of)).getMessage());
-
- assertEquals("name must match '[A-Za-z][A-Za-z0-9_-]{0,63}', but got: ''",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.from(URI.create("http://foo?"), Name::of, Name::of)).getMessage());
- }
-
- @Test
- void testPath() {
- HttpURL.Path path = HttpURL.Path.parse("foo/bar/baz", Name::of);
- List<String> expected = List.of("foo", "bar", "baz");
- assertEquals(expected, path.segments());
-
- assertEquals(expected.subList(1, 3), path.skip(1).segments());
- assertEquals(expected.subList(0, 2), path.cut(1).segments());
- assertEquals(expected.subList(1, 2), path.skip(1).cut(1).segments());
-
- assertEquals("path '/foo/bar/baz/'", path.withTrailingSlash().toString());
- assertEquals(path, path.withoutTrailingSlash().withoutTrailingSlash());
-
- assertEquals(List.of("one", "foo", "bar", "baz", "two"),
- HttpURL.Path.from(List.of("one")).append(path).append("two").segments());
-
- assertEquals(List.of(expected.get(2), expected.get(0)),
- path.append(path).cut(2).skip(2).segments());
-
- for (int i = 0; i < 3; i++) {
- assertEquals(path.head(i), path.cut(3 - i));
- assertEquals(path.tail(i), path.skip(3 - i));
- }
-
- assertThrows(NullPointerException.class,
- () -> path.append((String) null));
-
- List<String> names = new ArrayList<>();
- names.add(null);
- assertThrows(NullPointerException.class,
- () -> path.append(names));
-
- assertEquals("name must match '[A-Za-z][A-Za-z0-9_-]{0,63}', but got: '???'",
- assertThrows(IllegalArgumentException.class,
- () -> path.append("???")).getMessage());
-
- assertEquals("fromIndex(2) > toIndex(1)",
- assertThrows(IllegalArgumentException.class,
- () -> path.cut(2).skip(2)).getMessage());
-
- assertEquals("path segment decoded cannot contain '/', but got: '/'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.Path.empty().append("%2525252525252525%2525252525253%25252532%252525%252534%36")).getMessage());
-
- assertEquals("path segment decoded cannot contain '?', but got: '?'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.Path.empty().append("?")).getMessage());
-
- assertEquals("path segment decoded cannot contain '#', but got: '#'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.Path.empty().append("#")).getMessage());
-
- assertEquals("path segments cannot be \"\", \".\", or \"..\", but got: '..'",
- assertThrows(IllegalArgumentException.class,
- () -> HttpURL.Path.empty().append("%2E%25252E")).getMessage());
- }
-
- @Test
- void testQuery() {
- Query query = Query.parse("foo=bar&baz", Name::of);
- Map<String, String> expected = new LinkedHashMap<>();
- expected.put("foo", "bar");
- expected.put("baz", null);
- assertEquals(expected, query.entries());
-
- expected.remove("baz");
- assertEquals(expected, query.remove("baz").entries());
-
- expected.put("baz", null);
- expected.remove("foo");
- assertEquals(expected, query.remove("foo").entries());
- assertEquals(expected, Query.empty(Name::of).add("baz").entries());
-
- assertEquals("query '?foo=bar&baz=bax&quu=fez&moo'",
- query.put("baz", "bax").merge(Query.from(Map.of("quu", "fez"))).add("moo").toString());
-
- assertThrows(NullPointerException.class,
- () -> query.remove(null));
-
- assertThrows(NullPointerException.class,
- () -> query.add(null));
-
- assertThrows(NullPointerException.class,
- () -> query.put(null, "hax"));
-
- assertThrows(NullPointerException.class,
- () -> query.put("hax", null));
-
- Map<String, String> names = new LinkedHashMap<>();
- names.put(null, "hax");
- assertThrows(NullPointerException.class,
- () -> query.merge(names));
- }
-
-}
diff --git a/container-core/src/test/java/com/yahoo/restapi/PathTest.java b/container-core/src/test/java/com/yahoo/restapi/PathTest.java
index 4786eb9775c..17b35a6343c 100644
--- a/container-core/src/test/java/com/yahoo/restapi/PathTest.java
+++ b/container-core/src/test/java/com/yahoo/restapi/PathTest.java
@@ -4,6 +4,7 @@ package com.yahoo.restapi;
import org.junit.Test;
import java.net.URI;
+import java.util.List;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
@@ -35,7 +36,7 @@ public class PathTest {
assertTrue(path.matches("/a/{foo}/bar/{b}/{*}"));
assertEquals("1", path.get("foo"));
assertEquals("fuz", path.get("b"));
- assertEquals("/", path.getRest().raw());
+ assertEquals(List.of(), path.getRest().segments());
}
{
@@ -43,7 +44,7 @@ public class PathTest {
assertTrue(path.matches("/a/{foo}/bar/{b}/{*}"));
assertEquals("1", path.get("foo"));
assertEquals("fuz", path.get("b"));
- assertEquals("/kanoo", path.getRest().raw());
+ assertEquals(List.of("kanoo"), path.getRest().segments());
}
{
@@ -51,7 +52,7 @@ public class PathTest {
assertTrue(path.matches("/a/{foo}/bar/{b}/{*}"));
assertEquals("1", path.get("foo"));
assertEquals("fuz", path.get("b"));
- assertEquals("/kanoo/trips", path.getRest().raw());
+ assertEquals(List.of("kanoo", "trips"), path.getRest().segments());
}
{
@@ -59,7 +60,7 @@ public class PathTest {
assertTrue(path.matches("/a/{foo}/bar/{b}/{*}"));
assertEquals("1", path.get("foo"));
assertEquals("fuz", path.get("b"));
- assertEquals("/kanoo/trips/", path.getRest().raw());
+ assertEquals(List.of("kanoo", "trips"), path.getRest().segments());
}
}
diff --git a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
index 1b03d87f405..d4a53fb5d85 100644
--- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
+++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java
@@ -111,7 +111,7 @@ class RestApiImplTest {
@Test
void uri_builder_creates_valid_uri_prefix() {
RestApi restApi = RestApi.builder()
- .addRoute(route("/test").get(ctx -> new MessageResponse(ctx.uriBuilder().toString())))
+ .addRoute(route("/test").get(ctx -> new MessageResponse(ctx.baseRequestURL().toString())))
.build();
verifyJsonResponse(restApi, Method.GET, "/test", null, 200, "{\"message\":\"http://localhost\"}");
verifyJsonResponse(restApi, Method.GET, "/test", null, 200, "{\"message\":\"http://mydomain:81\"}", Map.of("Host", "mydomain:81"));