org.jgrapes.http.events.Request Maven / Gradle / Ivy
The newest version!
/*
* JGrapes Event Driven Framework
* Copyright (C) 2016-2018 Michael N. Lipp
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see .
*/
package org.jgrapes.http.events;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Stack;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpProtocol;
import org.jdrupes.httpcodec.protocols.http.HttpRequest;
import org.jgrapes.core.Channel;
import org.jgrapes.core.CompletionEvent;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
import org.jgrapes.http.ResourcePattern;
import org.jgrapes.http.ResourcePattern.PathSpliterator;
import org.jgrapes.io.events.Output;
import org.jgrapes.net.SocketIOChannel;
/**
* The base class for all HTTP requests such as {@link Request.In.Get},
* {@link Request.In.Post} etc.
*
* @param the generic type
*/
public class Request extends MessageReceived {
/**
* @param channels
*/
protected Request(Channel... channels) {
super(channels);
}
/**
* The base class for all incoming HTTP requests. Incoming
* request flow downstream and are served by the framework.
*
* A result of `true` indicates that the request has been processed,
* i.e. a response has been sent or will sent. Händlers MUST
* check that a request has not been {@link fulfilled} before
* firing a {@link Response} event to avoid duplicate response
* events. Handlers that have fired a response event and all
* related {@link Output} events SHOULD {@link Event#stop stop}
* the request event to avoid unnecessary subsequent invocations of
* handlers. Handlers that want to do "postprocessing" MUST
* therefore listen for the corresponding {@link Completed}
* event instead of defining a handler for the request event
* with low priority.
*/
@SuppressWarnings("PMD.ShortClassName")
public static class In extends Request {
@SuppressWarnings("PMD.AvoidFieldNameMatchingTypeName")
private final HttpRequest request;
private final int matchLevels;
private final MatchValue matchValue;
private final URI resourceUri;
private URI uri;
/**
* Creates a new request event with the associated {@link Completed}
* event.
*
* @param protocol the protocol as reported by {@link #requestUri()}
* @param request the request data
* @param matchLevels the number of elements from the request path
* to use in the match value (see {@link #matchValue})
* @param channels the channels associated with this event
* @throws URISyntaxException
*/
@SuppressWarnings({ "PMD.UselessParentheses",
"PMD.ConstructorCallsOverridableMethod" })
public In(String protocol, HttpRequest request,
int matchLevels, Channel... channels)
throws URISyntaxException {
super(channels);
new Completed(this);
this.request = request;
this.matchLevels = matchLevels;
// Do any required request specific processing of the original
// request URI
URI requestUri = effectiveRequestUri(protocol, request);
// Clean the request URI's path, keeping the segments for matchValue
List segs = pathToSegs(requestUri);
requestUri = new URI(requestUri.getScheme(),
requestUri.getUserInfo(), requestUri.getHost(),
requestUri.getPort(),
"/" + segs.stream().collect(Collectors.joining("/")),
requestUri.getQuery(), null);
setRequestUri(requestUri);
// The URI for handler selection ignores user info and query
resourceUri = new URI(requestUri.getScheme(), null,
requestUri.getHost(), requestUri.getPort(),
requestUri.getPath(), null, null);
@SuppressWarnings("PMD.InefficientStringBuffering")
StringBuilder matchPath = new StringBuilder("/" + segs.stream()
.limit(matchLevels).collect(Collectors.joining("/")));
if (segs.size() > matchLevels) {
if (!matchPath.toString().endsWith("/")) {
matchPath.append('/');
}
matchPath.append('…');
}
matchValue = new MatchValue(getClass(),
(requestUri.getScheme() == null ? ""
: (requestUri.getScheme() + "://"))
+ (requestUri.getHost() == null ? ""
: (requestUri.getHost()
+ (requestUri.getPort() == -1 ? ""
: (":" + requestUri.getPort()))))
+ matchPath);
}
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
private List pathToSegs(URI requestUri)
throws URISyntaxException {
Iterator origSegs = PathSpliterator.stream(
requestUri.getPath()).iterator();
// Path must not be empty and must be absolute
if (!origSegs.hasNext() || !origSegs.next().isEmpty()) {
throw new URISyntaxException(
requestUri().getPath(), "Must be absolute");
}
// Remove dot segments and check for "...//..."
@SuppressWarnings({ "PMD.LooseCoupling",
"PMD.ReplaceVectorWithList" })
Stack segs = new Stack<>();
while (origSegs.hasNext()) {
if (!segs.isEmpty() && segs.peek().isEmpty()) {
// Empty segment followed by more means "//"
segs.clear();
}
String seg = origSegs.next();
if (".".equals(seg)) {
continue;
}
if ("..".equals(seg)) {
if (!segs.isEmpty()) {
segs.pop();
}
continue;
}
segs.push(seg);
}
return segs;
}
/**
* Builds the URI that represents this request. The default
* implementation checks that request URI in the HTTP request
* is directed at this server as specified in the "Host"-header
* and adds the protocol, host and port if not specified
* in the request URI.
*
* @param protocol the protocol
* @param request the request
* @return the URI
* @throws URISyntaxException if the request is not acceptable
*/
protected URI effectiveRequestUri(String protocol, HttpRequest request)
throws URISyntaxException {
URI serverUri = new URI(protocol, null,
request.host(), request.port(), "/", null, null);
URI origRequest = request.requestUri();
URI result = serverUri.resolve(new URI(null, null, null, -1,
origRequest.getPath(), origRequest.getQuery(), null));
if (!result.getScheme().equals(protocol)
|| !result.getHost().equals(request.host())
|| result.getPort() != request.port()) {
throw new URISyntaxException(origRequest.toString(),
"Scheme, host or port not allowed");
}
return result;
}
/**
* Creates the appropriate derived request event type from
* a given {@link HttpRequest}.
*
* @param request the HTTP request
* @param secure whether the request was received over a secure channel
* @param matchLevels the match levels
* @return the request event
* @throws URISyntaxException
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public static In fromHttpRequest(
HttpRequest request, boolean secure, int matchLevels)
throws URISyntaxException {
switch (request.method()) {
case "OPTIONS":
return new Options(request, secure, matchLevels);
case "GET":
return new Get(request, secure, matchLevels);
case "HEAD":
return new Head(request, secure, matchLevels);
case "POST":
return new Post(request, secure, matchLevels);
case "PUT":
return new Put(request, secure, matchLevels);
case "DELETE":
return new Delete(request, secure, matchLevels);
case "TRACE":
return new Trace(request, secure, matchLevels);
case "CONNECT":
return new Connect(request, secure, matchLevels);
default:
return new In(secure ? "https" : "http", request, matchLevels);
}
}
/**
* Sets the request URI.
*
* @param uri the new request URI
*/
protected final void setRequestUri(URI uri) {
this.uri = uri;
}
/**
* Returns an absolute URI of the request. For incoming requests, the
* URI is built from the information provided by the decoder.
*
* @return the URI
*/
public final URI requestUri() {
return uri;
}
/**
* Returns the "raw" request as provided by the HTTP decoder.
*
* @return the request
*/
public HttpRequest httpRequest() {
return request;
}
/**
* The match value consists of the event class and a URI.
* The URI is similar to the request URI but its path elements
* are shortened as specified in the constructor.
*
* The match value is used as key in a map that speeds up
* the lookup of handlers. Having the complete URI in the match
* value would inflate this map.
*
* @return the object
* @see org.jgrapes.core.Event#defaultCriterion()
*/
@Override
public Object defaultCriterion() {
return matchValue;
}
/*
* (non-Javadoc)
*
* @see org.jgrapes.core.Event#isMatchedBy(java.lang.Object)
*/
@Override
public boolean isEligibleFor(Object value) {
if (!(value instanceof MatchValue)) {
return super.isEligibleFor(value);
}
MatchValue mval = (MatchValue) value;
if (!mval.type.isAssignableFrom(matchValue.type)) {
return false;
}
if (mval.resource instanceof ResourcePattern) {
return ((ResourcePattern) mval.resource).matches(resourceUri,
matchLevels) >= 0;
}
return mval.resource.equals(matchValue.resource);
}
/**
* Checks if the request has been processed, i.e. a response has
* been sent.
*
* @return true, if fulfilled
*/
public boolean fulfilled() {
return currentResults().size() > 0 && currentResults().get(0);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public String toString() {
StringBuilder builder = new StringBuilder(50);
builder.append(Components.objectName(this))
.append(" [\"");
String path = Optional.ofNullable(request.requestUri().getPath())
.orElse("");
if (path.length() > 15) {
builder.append("...")
.append(path.substring(path.length() - 12));
} else {
builder.append(path);
}
builder.append('\"');
if (channels().length > 0) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
/**
* Creates the match value.
*
* @param type the type
* @param resource the resource
* @return the object
*/
public static Object createMatchValue(
Class> type, ResourcePattern resource) {
return new MatchValue(type, resource);
}
/**
* Represents a match value.
*/
private static class MatchValue {
private final Class> type;
private final Object resource;
/**
* @param type
* @param resource
*/
public MatchValue(Class> type, Object resource) {
super();
this.type = type;
this.resource = resource;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public int hashCode() {
@SuppressWarnings("PMD.AvoidFinalLocalVariable")
final int prime = 31;
int result = 1;
result = prime * result
+ ((resource == null) ? 0 : resource.hashCode());
result
= prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MatchValue other = (MatchValue) obj;
if (resource == null) {
if (other.resource != null) {
return false;
}
} else if (!resource.equals(other.resource)) {
return false;
}
if (type == null) {
if (other.type != null) {
return false;
}
} else if (!type.equals(other.type)) {
return false;
}
return true;
}
}
/**
* The associated completion event.
*/
public static class Completed extends CompletionEvent {
/**
* Instantiates a new event.
*
* @param monitoredEvent the monitored event
* @param channels the channels
*/
public Completed(In monitoredEvent, Channel... channels) {
super(monitoredEvent, channels);
}
}
/**
* Represents a HTTP CONNECT request.
*/
public static class Connect extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Connect(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
/**
* Builds the URI that represents this request. This
* implementation returns the request URI without
* path and query component.
*
* @param protocol the protocol
* @param request the request
* @return the uri
* @throws URISyntaxException the URI syntax exception
* @see "[RFC 7230, Section 5.5](https://datatracker.ietf.org/doc/html/rfc7230#section-5.5)"
*/
@Override
protected URI effectiveRequestUri(String protocol,
HttpRequest request) throws URISyntaxException {
URI req = request.requestUri();
return new URI(req.getScheme(), req.getUserInfo(),
req.getHost(), req.getPort(), null, null, null);
}
}
/**
* The Class Delete.
*/
public static class Delete extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Delete(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP GET request.
*/
public static class Get extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Get(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP HEAD request.
*/
public static class Head extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Head(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP OPTIONS request.
*/
public static class Options extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Options(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP POST request.
*/
public static class Post extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Post(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP PUT request.
*/
public static class Put extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Put(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
/**
* Represents a HTTP TRACE request.
*/
public static class Trace extends In {
/**
* Create a new event.
*
* @param request the request data
* @param secure indicates whether the request was received on a
* secure channel
* @param matchLevels the number of elements from the request path
* to use in the match value
* @param channels the channels on which the event is to be
* fired (optional)
* @throws URISyntaxException
*/
public Trace(HttpRequest request, boolean secure, int matchLevels,
Channel... channels) throws URISyntaxException {
super(secure ? "https" : "http", request, matchLevels,
channels);
}
}
}
/**
* The base class for all outgoing HTTP requests. Outgoing
* request flow upstream and are served externally.
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Out extends Request {
private final HttpRequest request;
private BiConsumer connectedCallback;
/**
* Instantiates a new request.
*
* @param method the method
* @param url the url
*/
public Out(String method, URL url) {
try {
request = new HttpRequest(method, url.toURI(),
HttpProtocol.HTTP_1_1, false);
} catch (URISyntaxException e) {
// This should not happen because every valid URL can be
// converted to a URI.
throw new IllegalArgumentException(e);
}
}
/**
* Sets a "connected callback". When the {@link Out} event is
* created, the network connection is not yet known. Some
* header fields' values, however, need e.g. the port information
* from the connection. Therefore a callback may be set which is
* invoked when the connection has been obtained that will be used
* to send the request.
*
* @param connectedCallback the connected callback
* @return the out
*/
public Out setConnectedCallback(
BiConsumer connectedCallback) {
this.connectedCallback = connectedCallback;
return this;
}
/**
* Returns the connected callback.
*
* @return the connected callback, if set
*/
public Optional>
connectedCallback() {
return Optional.ofNullable(connectedCallback);
}
/**
* The HTTP request that will be sent by the event.
*
* @return the http request
*/
public HttpRequest httpRequest() {
return request;
}
/**
* Returns an absolute URI of the request.
*
* @return the URI
*/
public URI requestUri() {
return request.requestUri();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this))
.append(" [").append(request.toString());
if (channels().length > 0) {
builder.append(", channels=");
builder.append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
/**
* Represents a HTTP CONNECT request.
*/
public static class Connect extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Connect(URL uri) {
super("CONNECT", uri);
}
}
/**
* Represents a HTTP DELETE request.
*/
public static class Delete extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Delete(URL uri) {
super("DELETE", uri);
}
}
/**
* Represents a HTTP GET request.
*/
public static class Get extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Get(URL uri) {
super("GET", uri);
}
}
/**
* Represents a HTTP HEAD request.
*/
public static class Head extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Head(URL uri) {
super("HEAD", uri);
}
}
/**
* Represents a HTTP OPTIONS request.
*/
public static class Options extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Options(URL uri) {
super("OPTIONS", uri);
}
}
/**
* Represents a HTTP POST request.
*/
public static class Post extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Post(URL uri) {
super("POST", uri);
}
}
/**
* Represents a HTTP PUT request.
*/
public static class Put extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Put(URL uri) {
super("PUT", uri);
}
}
/**
* Represents a HTTP TRACE request.
*/
public static class Trace extends Out {
/**
* Instantiates a new request.
*
* @param uri the uri
*/
public Trace(URL uri) {
super("TRACE", uri);
}
}
}
}