Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Header and Media type API consistency #7351

Merged
merged 7 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion archetypes/helidon/src/main/archetype/common/media.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
<list key="Main-routing-builder" if="${flavor} == 'nima'">
<value order="1"><![CDATA[.any("/", (req, res) -> {
res.status(Http.Status.MOVED_PERMANENTLY_301);
res.header(Header.createCached(Header.LOCATION, "/ui"));
res.header(Http.Headers.createCached(Http.HeaderNames.LOCATION, "/ui"));
res.send();
})
.register("/ui", StaticContentService.builder("WEB")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,46 +29,46 @@ class TestCORS {
void testAnonymousGreetWithCors() {
Response r = target.path("/simple-greet")
.request()
.header(Http.Header.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.Header.HOST.defaultCase(), "here.com")
.header(Http.HeaderNames.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.HeaderNames.HOST.defaultCase(), "here.com")
.get();

assertThat("HTTP response", r.getStatus(), is(200));
String payload = fromPayload(r);
assertThat("HTTP response payload", payload.contains("Hello World!"), is(true));
assertThat("CORS header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN,
r.getHeaders().getFirst(Http.Header.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
r.getHeaders().getFirst(Http.HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
is("http://foo.com"));
}

@Test
void testCustomGreetingWithCors() {
Response r = target.path("/simple-greet")
.request()
.header(Http.Header.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.Header.HOST.defaultCase(), "here.com")
.header(Http.HeaderNames.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.HeaderNames.HOST.defaultCase(), "here.com")
.header("Access-Control-Request-Method", "PUT")
.options();

assertThat("pre-flight status", r.getStatus(), is(200));
MultivaluedMap<String, Object> responseHeaders = r.getHeaders();
assertThat("Header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_METHODS,
r.getHeaders().getFirst(Http.Header.ACCESS_CONTROL_ALLOW_METHODS.defaultCase()),
r.getHeaders().getFirst(Http.HeaderNames.ACCESS_CONTROL_ALLOW_METHODS.defaultCase()),
is("PUT"));
assertThat( "Header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN,
r.getHeaders().getFirst(Http.Header.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
r.getHeaders().getFirst(Http.HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
is("http://foo.com"));

Invocation.Builder builder = target.path("/simple-greet")
.request()
.headers(responseHeaders)
.header(Http.Header.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.Header.HOST.defaultCase(), "here.com");
.header(Http.HeaderNames.ORIGIN.defaultCase(), "http://foo.com")
.header(Http.HeaderNames.HOST.defaultCase(), "here.com");

r = putResponse("Cheers", builder);
assertThat("HTTP response3", r.getStatus(), is(200));
assertThat( "Header " + CrossOriginConfig.ACCESS_CONTROL_ALLOW_ORIGIN,
r.getHeaders().getFirst(Http.Header.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
r.getHeaders().getFirst(Http.HeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN.defaultCase()),
is("http://foo.com"));
assertThat(fromPayload(r), containsString("Cheers World!"));
}
Expand All @@ -77,8 +77,8 @@ class TestCORS {
void testGreetingChangeWithCorsAndOtherOrigin() {
Invocation.Builder builder = target.path("/simple-greet")
.request()
.header(Http.Header.ORIGIN.defaultCase(), "http://other.com")
.header(Http.Header.HOST.defaultCase(), "here.com");
.header(Http.HeaderNames.ORIGIN.defaultCase(), "http://other.com")
.header(Http.HeaderNames.HOST.defaultCase(), "here.com");

Response r = putResponse("Ahoy", builder);
boolean isOverriding = Config.create().get("cors").exists();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import static io.helidon.common.http.Http.Status.NOT_FOUND_404;
* File service.
*/
final class FileService implements HttpService {
private static final Http.HeaderValue UI_LOCATION = Header.createCached(Header.LOCATION, "/ui");
private static final Http.Header UI_LOCATION = Http.Headers.createCached(Http.HeaderNames.LOCATION, "/ui");
private final JsonBuilderFactory jsonFactory;
private final Path storage;

Expand Down
49 changes: 49 additions & 0 deletions common/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
HTTP
----

# HTTP Header name and value

Abstraction of a single header name (`Connection`), or a header value (`Connection: keep-alive`).


## Types

- `Http.HeaderName` - abstraction of a name of a header (such as `Content-Length`)
- `Http.HeaderNames` - constants with "known" header names (such as `HeaderNames.CONTENT_LENGTH`)
- `Http.Header` - abstraction of a header with a value (such as `Content-Length: 0`)
- `Http.Headers` - constants with commonly used headers with values (such as `Headers.CONTENT_LENGTH_ZERO`)

Internal types:
- `Http.HeaderNameEnum` - "known" headers, optimized for performance in header containers
- `Http.HeaderNameImpl` - custom headers

## Factory methods

Factory methods to create header names and values are located on their relevant type that contains constants (aligned
with how we treat `MediaTypes`:

Header names:
1. `Http.HeaderNames.create(String)` - create a header name from the provided name (uses known header if possible)
2. `Http.HeaderNames.create(String, String)` - create a header name (optimized) with lower case and name
3. `Http.HeaderNames.createFromLowercase(String)` - create a header name when we have a guaranteed lowercase name (such as in HTTP/2)

Headers (name and value):
1. `Http.Headers.create(....)` - create a header with the provided name and value
2. `Http.Headers.createCached(...)` - create a header that caches its HTTP/1.1 bytes (optimization for heavily used headers)

Methods with `changing` and `sensitive` - these options are used for HTTP/2 (and HTTP/3) to correctly cache names/values when talking over the network

# HTTP Header containers

Abstraction of a collection of headers, as used in server and client requests and responses.
The containers are optimized for read and write speed by using an array for "known" headers (Headers that are part of our
`HeaderNameEnum`). Other headers are stored in a map, keyed by `HeaderName`.

## Types

- `Headers` - a collection of HTTP headers that is read-only
- `WritableHeader` - a collection of HTTP headers that is mutable
- `ClientRequestHeaders` - writable headers to create client request
- `ClientResponseHeaders` - read-only headers with response from the server
- `ServerRequestHeaders` - read-only headers with request from client
- `ServerResponseHeaders` - writable header to create server response
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ default ClientRequestHeaders accept(MediaType... accepted) {
MediaType mediaType = accepted[i];
values[i] = mediaType.text();
}
set(Http.Header.create(Http.Header.ACCEPT, values));
set(Http.Headers.create(Http.HeaderNames.ACCEPT, values));
return this;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,9 +23,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;

import io.helidon.common.http.Http.Header;
import io.helidon.common.http.Http.HeaderName;
import io.helidon.common.http.Http.HeaderValue;

/**
* Client request headers.
Expand All @@ -50,12 +48,12 @@ public boolean contains(HeaderName name) {
}

@Override
public boolean contains(HeaderValue headerWithValue) {
public boolean contains(Http.Header headerWithValue) {
return delegate.contains(headerWithValue);
}

@Override
public HeaderValue get(HeaderName name) {
public Http.Header get(HeaderName name) {
return delegate.get(name);
}

Expand All @@ -67,8 +65,8 @@ public int size() {
@Override
public List<HttpMediaType> acceptedTypes() {
if (mediaTypes == null) {
if (delegate.contains(Header.ACCEPT)) {
List<String> accepts = delegate.get(Header.ACCEPT).allValues(true);
if (delegate.contains(Http.HeaderNames.ACCEPT)) {
List<String> accepts = delegate.get(Http.HeaderNames.ACCEPT).allValues(true);

List<HttpMediaType> mediaTypes = new ArrayList<>(accepts.size());
for (String accept : accepts) {
Expand All @@ -85,13 +83,13 @@ public List<HttpMediaType> acceptedTypes() {
}

@Override
public ClientRequestHeaders setIfAbsent(HeaderValue header) {
public ClientRequestHeaders setIfAbsent(Http.Header header) {
delegate.setIfAbsent(header);
return this;
}

@Override
public ClientRequestHeaders add(HeaderValue header) {
public ClientRequestHeaders add(Http.Header header) {
delegate.add(header);
return this;
}
Expand All @@ -103,19 +101,19 @@ public ClientRequestHeaders remove(HeaderName name) {
}

@Override
public ClientRequestHeaders remove(HeaderName name, Consumer<HeaderValue> removedConsumer) {
public ClientRequestHeaders remove(HeaderName name, Consumer<Http.Header> removedConsumer) {
delegate.remove(name, removedConsumer);
return this;
}

@Override
public ClientRequestHeaders set(HeaderValue header) {
public ClientRequestHeaders set(Http.Header header) {
delegate.set(header);
return this;
}

@Override
public Iterator<HeaderValue> iterator() {
public Iterator<Http.Header> iterator() {
return delegate.iterator();
}

Expand All @@ -129,4 +127,10 @@ public ClientRequestHeaders clear() {
delegate.clear();
return this;
}

@Override
public ClientRequestHeaders from(Headers headers) {
headers.forEach(this::set);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@
import java.util.Optional;

import io.helidon.common.http.Http.DateTime;
import io.helidon.common.http.Http.HeaderValue;
import io.helidon.common.media.type.ParserMode;

import static io.helidon.common.http.Http.Header.ACCEPT_PATCH;
import static io.helidon.common.http.Http.Header.EXPIRES;
import static io.helidon.common.http.Http.Header.LAST_MODIFIED;
import static io.helidon.common.http.Http.Header.LOCATION;
import static io.helidon.common.http.Http.HeaderNames.ACCEPT_PATCH;
import static io.helidon.common.http.Http.HeaderNames.EXPIRES;
import static io.helidon.common.http.Http.HeaderNames.LAST_MODIFIED;
import static io.helidon.common.http.Http.HeaderNames.LOCATION;

/**
* HTTP Headers of a client response.
Expand Down Expand Up @@ -72,7 +71,7 @@ default List<HttpMediaType> acceptPatches() {
}

/**
* Optionally gets the value of {@link io.helidon.common.http.Http.Header#LOCATION} header.
* Optionally gets the value of {@link io.helidon.common.http.Http.HeaderNames#LOCATION} header.
* <p>
* Used in redirection, or when a new resource has been created.
*
Expand All @@ -81,14 +80,14 @@ default List<HttpMediaType> acceptPatches() {
default Optional<URI> location() {
if (contains(LOCATION)) {
return Optional.of(get(LOCATION))
.map(HeaderValue::value)
.map(Http.Header::value)
.map(URI::create);
}
return Optional.empty();
}

/**
* Optionally gets the value of {@link io.helidon.common.http.Http.Header#LAST_MODIFIED} header.
* Optionally gets the value of {@link io.helidon.common.http.Http.HeaderNames#LAST_MODIFIED} header.
* <p>
* The last modified date for the requested object.
*
Expand All @@ -97,14 +96,14 @@ default Optional<URI> location() {
default Optional<ZonedDateTime> lastModified() {
if (contains(LAST_MODIFIED)) {
return Optional.of(get(LAST_MODIFIED))
.map(HeaderValue::value)
.map(Http.Header::value)
.map(DateTime::parse);
}
return Optional.empty();
}

/**
* Optionally gets the value of {@link io.helidon.common.http.Http.Header#EXPIRES} header.
* Optionally gets the value of {@link io.helidon.common.http.Http.HeaderNames#EXPIRES} header.
* <p>
* Gives the date/time after which the response is considered stale.
*
Expand All @@ -113,7 +112,7 @@ default Optional<ZonedDateTime> lastModified() {
default Optional<ZonedDateTime> expires() {
if (contains(EXPIRES)) {
return Optional.of(get(EXPIRES))
.map(HeaderValue::value)
.map(Http.Header::value)
.map(DateTime::parse);
}
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ public boolean contains(Http.HeaderName name) {
}

@Override
public boolean contains(Http.HeaderValue headerWithValue) {
public boolean contains(Http.Header headerWithValue) {
return headers.contains(headerWithValue);
}

@Override
public Http.HeaderValue get(Http.HeaderName name) {
public Http.Header get(Http.HeaderName name) {
return headers.get(name);
}

@Override
public Optional<HttpMediaType> contentType() {
if (parserMode == ParserMode.RELAXED) {
return contains(HeaderEnum.CONTENT_TYPE)
? Optional.of(HttpMediaType.create(get(HeaderEnum.CONTENT_TYPE).value(), parserMode))
return contains(HeaderNameEnum.CONTENT_TYPE)
? Optional.of(HttpMediaType.create(get(HeaderNameEnum.CONTENT_TYPE).value(), parserMode))
: Optional.empty();
}
return headers.contentType();
Expand All @@ -68,7 +68,7 @@ public int size() {
}

@Override
public Iterator<Http.HeaderValue> iterator() {
public Iterator<Http.Header> iterator() {
return headers.iterator();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -47,7 +47,7 @@
* </li>
* </ul>
*/
public class ContentDisposition implements Http.HeaderValue {
public class ContentDisposition implements Http.Header {
private static final String NAME_PARAMETER = "name";
private static final String FILENAME_PARAMETER = "filename";
private static final String CREATION_DATE_PARAMETER = "creation-date";
Expand Down Expand Up @@ -126,12 +126,12 @@ public static ContentDisposition empty() {

@Override
public String name() {
return Http.Header.CONTENT_DISPOSITION.defaultCase();
return Http.HeaderNames.CONTENT_DISPOSITION.defaultCase();
}

@Override
public Http.HeaderName headerName() {
return Http.Header.CONTENT_DISPOSITION;
return Http.HeaderNames.CONTENT_DISPOSITION;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,7 +43,7 @@ private CookieParser() {
* @param httpHeader cookie header
* @return a cookie name and values parsed into a parameter format.
*/
static Parameters parse(Http.HeaderValue httpHeader) {
static Parameters parse(Http.Header httpHeader) {
Map<String, List<String>> allCookies = new HashMap<>();
for (String value : httpHeader.allValues()) {
parse(allCookies, value);
Expand Down