Skip to content

Commit

Permalink
Move DirectHandler to HTTP common and refactor reactive and Nima to u…
Browse files Browse the repository at this point in the history
…se it.
  • Loading branch information
tomas-langer committed Aug 26, 2022
1 parent 06e089c commit b67c6d5
Show file tree
Hide file tree
Showing 43 changed files with 396 additions and 700 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2021, 2022 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 @@ -14,82 +14,117 @@
* limitations under the License.
*/

/*
* Copyright (c) 2022 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.
* 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 io.helidon.nima.webserver.http;
package io.helidon.common.http;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.helidon.common.buffers.BufferData;
import io.helidon.common.http.HeadersServerResponse;
import io.helidon.common.http.Http;
import io.helidon.common.http.Http.HeaderName;

/**
* A handler that is invoked when a response is sent outside of router.
* See {@link SimpleHandler.EventType} to see which types
* A handler that is invoked when a response is sent outside of routing.
* See {@link DirectHandler.EventType} to see which types
* of events are covered by this handler.
* Direct handlers can be used both with blocking and reactive servers in Helidon.
*/
@FunctionalInterface
public interface SimpleHandler {
public interface DirectHandler {
/**
* Default handler will HTML encode the message (if any),
* use the default status code for the event type, and copy all headers configured.
*
*
* @return default direct handler
*/
static DirectHandler defaultHandler() {
return DirectHandlerDefault.INSTANCE;
}

/**
* Handler of responses that bypass router.
* <p>
* This method should be used to return custom status, header and possible entity.
* If there is a need to handle more details, please redirect the client to a proper endpoint to handle them.
*
* @param request request as received with as much known information as possible
* @param eventType type of the event
* @param request request as received with as much known information as possible
* @param eventType type of the event
* @param defaultStatus default status expected to be returned
* @param responseHeaders headers to be added to response
* @param thrown throwable caught as part of processing with possible additional details about the reason of failure
* @param thrown throwable caught as part of processing with possible additional details about the reason of failure
* @return response to use to return to original request
*/
default SimpleResponse handle(SimpleRequest request,
EventType eventType,
Http.Status defaultStatus,
HeadersServerResponse responseHeaders,
Throwable thrown) {
default TransportResponse handle(TransportRequest request,
EventType eventType,
Http.Status defaultStatus,
HeadersServerResponse responseHeaders,
Throwable thrown) {
return handle(request, eventType, defaultStatus, responseHeaders, thrown.getMessage());
}

/**
* Handler of responses that bypass router.
* Handler of responses that bypass routing.
* <p>
* This method should be used to return custom status, header and possible entity.
* If there is a need to handle more details, please redirect the client to a proper endpoint to handle them.
*
* @param request request as received with as much known information as possible
* @param eventType type of the event
* @param request request as received with as much known information as possible
* @param eventType type of the event
* @param defaultStatus default status expected to be returned
* @param responseHeaders headers to be added to response
* @param message informative message for cases that are not triggered by an exception, by default this will be called
* @param message informative message for cases that are not triggered by an exception, by default this will be called
* also
* for exceptional cases with the exception message
* @return response to use to return to original request
*/
SimpleResponse handle(SimpleRequest request,
EventType eventType,
Http.Status defaultStatus,
HeadersServerResponse responseHeaders,
String message);
TransportResponse handle(TransportRequest request,
EventType eventType,
Http.Status defaultStatus,
HeadersServerResponse responseHeaders,
String message);

/**
* Request information.
* Note that the information may not be according to specification, as this marks a bad request (by definition).
*/
interface TransportRequest {
/**
* Create an empty transport request.
* This is usually used when an error occurs before we could parse request information.
*
* @return empty transport request
*/
static TransportRequest empty() {
return DirectHandlerEmptyRequest.INSTANCE;
}

/**
* Protocol version (either from actual request, or guessed).
*
* @return protocol version
*/
String protocolVersion();

/**
* HTTP method.
*
* @return method
*/
String method();

/**
* Requested path, if found in request.
*
* @return uri or an empty string
*/
String path();

/**
* Headers, if found in request.
*
* @return headers or an empty map
*/
HeadersServerRequest headers();
}

/**
* Types of events that can be triggered outside of router
Expand Down Expand Up @@ -149,82 +184,19 @@ public boolean keepAlive() {
}
}

/**
* Request information.
* Note that the information may not be according to specification, as this marks a bad request (by definition).
*/
interface SimpleRequest {
/**
* Empty request, for cases where no information is available.
*
* @return empty simple request
*/
static SimpleRequest empty() {
return new SimpleRequest() {
@Override
public String protocolVersion() {
return "";
}

@Override
public String method() {
return "";
}

@Override
public String path() {
return "";
}

@Override
public Map<String, List<String>> headers() {
return Map.of();
}
};
}

/**
* Protocol version (either from actual request, or guessed).
*
* @return protocol version
*/
String protocolVersion();

/**
* HTTP method.
*
* @return method
*/
String method();

/**
* Requested URI, if found in request.
*
* @return uri or an empty string
*/
String path();

/**
* Headers, if found in request.
*
* @return headers or an empty map
*/
Map<String, List<String>> headers();
}

/**
* Response to correctly reply to the original client.
*/
class SimpleResponse {
class TransportResponse {
private final Http.Status status;
private final byte[] message;
private final HeadersServerResponse headers;
private final byte[] entity;
private final boolean keepAlive;

private SimpleResponse(Builder builder) {
private TransportResponse(Builder builder) {
this.status = builder.status;
this.message = builder.message;
this.headers = builder.headers;
this.entity = builder.entity;
this.keepAlive = builder.keepAlive;
}

Expand Down Expand Up @@ -260,8 +232,8 @@ public HeadersServerResponse headers() {
*
* @return mesage bytes or empty if no message is configured
*/
public Optional<byte[]> message() {
return Optional.ofNullable(message);
public Optional<byte[]> entity() {
return Optional.ofNullable(entity);
}

/**
Expand All @@ -274,20 +246,20 @@ public boolean keepAlive() {
}

/**
* Fluent API builder for {@link SimpleHandler.SimpleResponse}.
* Fluent API builder for {@link DirectHandler.TransportResponse}.
*/
public static class Builder implements io.helidon.common.Builder<Builder, SimpleResponse> {
private Http.Status status = Http.Status.OK_200;
private byte[] message = BufferData.EMPTY_BYTES;
public static class Builder implements io.helidon.common.Builder<Builder, TransportResponse> {
private Http.Status status = Http.Status.BAD_REQUEST_400;
private byte[] entity;
private HeadersServerResponse headers = HeadersServerResponse.create();
private boolean keepAlive = true;

private Builder() {
}

@Override
public SimpleResponse build() {
return new SimpleResponse(this);
public TransportResponse build() {
return new TransportResponse(this);
}

/**
Expand All @@ -313,64 +285,73 @@ public Builder headers(HeadersServerResponse headers) {
}

/**
* Configure keep alive.
* Set a header (if exists, it would be replaced).
* Keep alive header is ignored, please use {@link #keepAlive(boolean)}.
*
* @param keepAlive whether to keep alive
* @param name name of the header
* @param values value of the header
* @return updated builder
*/
public Builder keepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
public Builder header(Http.HeaderName name, String... values) {
this.headers.set(name, List.of(values));
return this;
}

/**
* Custom entity. Uses the content, encodes it for HTML, reads it as {@code UTF-8}, configures
* {@code Content-Length} header, configures {@code Content-Type} header to {@code text/plain}.
* <p>
* Use {@link #message(byte[])} for custom encoding.
* Set a header (if exists, it would be replaced).
* Keep alive header is ignored, please use {@link #keepAlive(boolean)}.
*
* @param message response entity
* @param header header value
* @return updated builder
*/
public Builder message(String message) {
this.headers.setIfAbsent(Http.HeaderValues.CONTENT_TYPE_TEXT_PLAIN);
return message(message.getBytes(StandardCharsets.UTF_8));
public Builder header(Http.HeaderValue header) {
this.headers.add(header);
return this;
}

/**
* Custom entity. Uses the content, configures
* {@code Content-Length} header.
* <p>
* Use {@link #message(String)} for simple text messages.
* Configure keep alive.
*
* @param entity response entity
* @param keepAlive whether to keep alive
* @return updated builder
*/
public Builder message(byte[] entity) {
this.message = entity;
public Builder keepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
return this;
}

/**
* Configure an additional header.
* Custom entity. Uses the content, reads it as {@code UTF-8}, configures
* {@code Content-Length} header, configures {@code Content-Type} header to {@code text/plain}.
* <p>
* Use {@link #entity(byte[])} for custom encoding.
* <p>
* Note that this method does not do any escaping (such as HTML encoding), make sure the entity is safe.
*
* @param headerName header name
* @param headerValue header value
* @param entity response entity
* @return updated builder
*/
public Builder header(HeaderName headerName, String headerValue) {
this.headers.add(headerName.withValue(headerValue));
return this;
public Builder entity(String entity) {
this.headers.setIfAbsent(Http.HeaderValues.CONTENT_TYPE_TEXT_PLAIN);
return entity(entity.getBytes(StandardCharsets.UTF_8));
}

/**
* Configure an additional header.
* Custom entity. Uses the content, configures
* {@code Content-Length} header.
* <p>
* Use {@link #entity(String)} for simple text messages.
*
* @param header header value
* @param entity response entity
* @return updated builder
*/
public Builder header(Http.HeaderValue header) {
this.headers.add(header);
public Builder entity(byte[] entity) {
this.entity = Arrays.copyOf(entity, entity.length);
if (this.entity.length == 0) {
this.headers.remove(Http.Header.CONTENT_LENGTH);
} else {
header(Http.Header.CONTENT_LENGTH, String.valueOf(entity.length));
}
return this;
}
}
Expand Down
Loading

0 comments on commit b67c6d5

Please sign in to comment.