package com.vtence.molecule; import com.vtence.molecule.helpers.Headers; import com.vtence.molecule.helpers.Streams; import com.vtence.molecule.http.ContentType; import com.vtence.molecule.http.Host; import com.vtence.molecule.http.HttpMethod; import com.vtence.molecule.http.Scheme; import com.vtence.molecule.lib.EmptyInputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.vtence.molecule.http.HeaderNames.CONTENT_LENGTH; import static com.vtence.molecule.http.HeaderNames.HOST; import static java.lang.Long.parseLong; /** * Holds client HTTP request information and maintains attributes during the request lifecycle. * * Information includes among other things the body, headers, parameters, cookies and locales. */ public class Request { private final Headers headers = new Headers(); private final Map<String, List<String>> parameters = new LinkedHashMap<>(); private final Map<Object, Object> attributes = new HashMap<>(); private final List<BodyPart> parts = new ArrayList<>(); private String uri; private String path; private String query; private String scheme; private String serverHost; private int serverPort; private String remoteHost; private String remoteIp; private int remotePort; private String protocol; private InputStream input = new EmptyInputStream(); private HttpMethod method; private boolean secure; private long timestamp; /** * Reads the URI of this request. It can be a relative URI or the full URL if the client included * the host in the request. The URI will include the query string. * * @return the request URI */ public String uri() { return uri; } /** * Changes the URI of this request. * * @param uri the new URI */ public Request uri(String uri) { this.uri = uri; return this; } /** * Reads the path of this request. This is the normalized path. * * @return the request path */ public String path() { return path; } /** * Changes the path of this request. * * @param path the new path */ public Request path(String path) { this.path = path; return this; } /** * Reads the query part of this request's URI. The query string does not include the leading <code>?</code>. * * @return the query associated with this request's URI, which might be an empty string */ public String query() { return query; } /** * Changes the query part of this request. The query string should not include the leading <code>?</code>. * * @param query the new query part */ public Request query(String query) { this.query = query; return this; } /** * Gets the hostname of the server. It might be the server name or server address. * * @return the server host or ip */ public String serverHost() { return serverHost; } /** * Changes the hostname of the server. * * @return the new server name or ip */ public Request serverHost(String host) { this.serverHost = host; return this; } /** * Gets the port of the server. * * @return the server port */ public int serverPort() { return serverPort; } /** * Changes the port of the server. * * @return the new server port */ public Request serverPort(int port) { this.serverPort = port; return this; } /** * Reads the ip of the remote client of this request. * * @return the remote client ip */ public String remoteIp() { return remoteIp; } /** * Changes the ip of the remote client of this request. * * @param ip the new ip */ public Request remoteIp(String ip) { this.remoteIp = ip; return this; } /** * Reads the hostname of the remote client of this request. * * @return the remote client hostname */ public String remoteHost() { return remoteHost; } /** * Changes the hostname of the remote client of this request. * * @param hostName the new hostname */ public Request remoteHost(String hostName) { this.remoteHost = hostName; return this; } /** * Reads the port of the remote client of this request. * * @return the remote client port */ public int remotePort() { return remotePort; } /** * Changes the port of the remote client of this request. * * @param port the new port */ public Request remotePort(int port) { this.remotePort = port; return this; } /** * Gets this request URI scheme. It will be one of {@code http} or {@code https}. * * @return the request URI scheme */ public String scheme() { return scheme; } /** * Changes the scheme of this request URI. It should be one of {@code http} or {@code https}. * * @return the new scheme */ public Request scheme(String scheme) { this.scheme = scheme; return this; } /** * Gets the host name of the server to which this request was sent. * It is taken from the Host header value, if any, otherwise the server hostname is used. * * @return the server host name */ public String hostname() { return hasHeader(HOST) ? Host.parseName(header(HOST)) : serverHost; } /** * Reads the port of the server to which this request was sent. * It is taken from the Host header value, if any, otherwise the server port is used. * * @return the server port */ public int port() { int port = hasHeader(HOST) ? Host.parsePort(header(HOST)) : serverPort; if (port == -1) { port = Scheme.of(this).defaultPort(); } if (port == -1) { port = serverPort; } return port; } /** * Reconstructs the URL the client used to make this request or reads the URI if it is absolute. * * @return the absolute URI or the reconstructed URL */ public String url() { if (URI.create(uri).isAbsolute()) { return uri; } Host host = new Host(hostname(), port() == Scheme.of(this).defaultPort() ? -1 : port()); return scheme + "://" + host + uri; } /** * Reads the protocol of this request. * * @return the request protocol */ public String protocol() { return protocol; } /** * Changes the protocol of this request. * * @param protocol the new protocol */ public Request protocol(String protocol) { this.protocol = protocol; return this; } /** * Indicates if this request was done over a secure connection, such as SSL. * * @return true if the request was transferred securely */ public boolean secure() { return secure; } /** * Changes the secure state of this request, which indicates if the request was done over a secure channel. * * @param secure the new state */ public Request secure(boolean secure) { this.secure = secure; return this; } /** * Indicates the time in milliseconds when this request was received. * * @return the time the request arrived at */ public long timestamp() { return timestamp; } /** * Changes the timestamp of this request. * * @param time the new timestamp */ public Request timestamp(long time) { timestamp = time; return this; } /** * Indicates the HTTP method with which this request was made (e.g. GET, POST, PUT, etc.). * * @return the request HTTP method */ public HttpMethod method() { return method; } /** * Changes the HTTP method of this request by name. Method name is case-insensitive and must refer to * one of the {@link com.vtence.molecule.http.HttpMethod}s. * * @param methodName the new method name */ public Request method(String methodName) { return method(HttpMethod.valueOf(methodName)); } /** * Changes the HTTP method of this request. * * @param method the new method */ public Request method(HttpMethod method) { this.method = method; return this; } /** * Consumes the body of this request and returns it as text. The text is decoded using the charset of the request. * * @see Request#charset() * @return the text that makes up the body */ public String body() throws IOException { return new String(bodyContent(), charset()); } /** * Consumes the body of this request and returns it as an array of bytes. * * @return the bytes content of the body */ public byte[] bodyContent() throws IOException { return Streams.consume(bodyStream()); } /** * Provides the body of this request as an {@link java.io.InputStream}. * * @return the stream of bytes that make up the body */ public InputStream bodyStream() { return input; } /** * Changes the body of this request. The body is encoded using the charset of the request. * * Note that this will not affect the list of parameters that might have been sent with the original * body as POST parameters. * * @see Request#charset() * @param body the new body as a string */ public Request body(String body) { return body(body.getBytes(charset())); } /** * Changes the body of this request. * * Note that this will not affect the list of parameters that might have been sent with the original * body as POST parameters. * * @param content the new body content as an array of bytes */ public Request body(byte[] content) { this.input = new ByteArrayInputStream(content); return this; } /** * Changes the body of this request. * * Note that this will not affect the list of parameters that might have been sent with the original * body as POST parameters. * * @param input the new body content as a stream of bytes */ public Request body(InputStream input) { this.input = input; return this; } /** * Ads a new body part to this request. * * <p> * Note that this will not change the body of the request, which will still contain the original multipart * encoded body. * </p> * * @param part the additional body part */ public void addPart(BodyPart part) { parts.add(part); } /** * Acquires the list of <code>BodyPart</code>s with from this request, * provided that the request is of type <code>multipart/form-data</code>. * <p> * This is typically used in case of file uploads or multipart <code>POST</code> requests. * </p> * <p> * Note that the list is a copy, modifications to the returned list will not change the request. * </p> * * @return the (possibly empty) list of body parts */ public List<BodyPart> parts() { return new ArrayList<>(parts); } /** * Acquires the <code>BodyPart</code> with the specified name from this request. * This is typically used in case of file uploads or multipart <code>POST</code> requests. * * @param name the name of the body part to acquire * * @return the named part or null if the part does not exist or the request * is not of type <code>multipart/form-data</code> */ public BodyPart part(String name) { for (BodyPart part : parts) { if (part.name().equals(name)) return part; } return null; } /** * Removes the body part with the specified name from this request. * * <p> * In case there are multiple body parts with that name, they are all removed. * </p> * * @param name the name of the part(s) to remove */ public Request removePart(String name) { BodyPart[] copy = parts.toArray(new BodyPart[parts.size()]); for (BodyPart part : copy) { if (part.name().equals(name)) removePart(part); } return this; } /** * Removes the given body part from this request. * * @param part the body part to remove */ public Request removePart(BodyPart part) { parts.remove(part); return this; } /** * Provides the charset used in the body of this request. * The charset is read from the <code>Content-Type</code> header. * * @return the charset of this request or null if <code>Content-Type</code> is missing. */ public Charset charset() { ContentType contentType = ContentType.of(this); if (contentType == null || contentType.charset() == null) { return StandardCharsets.ISO_8859_1; } return contentType.charset(); } /** * Checks for the presence of a specific HTTP message header in this request. * * @param name the name of the header to check * @return true if the header was set */ public boolean hasHeader(String name) { return headers.has(name); } /** * Gets the value of the specified header sent with this request. The name is case insensitive. * * <p> * In case there are multiple headers with that name, a comma separated list of values is returned. * </p> * * This method returns a null value if the request does not include any header of the specified name. * * @param name the name of the header to retrieve * @return the value of the header */ public String header(String name) { return headers.get(name); } /** * Gets all the header names sent with this request. If the request has no header, the set will be empty. * <p> * Note that the names are provided as originally set on the request. * Modifications to the provided set are safe and will not affect the request. * </p> * * @return a set containing all the header names sent, which might be empty */ public Set<String> headerNames() { return headers.names(); } /** * Gets the list of all the values sent with this request under the specified header. * The name is case insensitive. * * <p> * Some headers can be sent by clients as several headers - each with a different value - rather than sending * the header as a comma separated list. * </p> * * If the request does not include any header of the specified name, the list is empty. * Modifications to the provided list are safe and will not affect the request. * * @param name the name of the header to retrieve * @return the list of values for that header */ public List<String> headers(String name) { return headers.list(name); } /** * Adds an HTTP message header to this request. The new value will be added to the list * of existing values for that header name. * * @param name the name of the header to add * @param value the additional value for that header */ public Request addHeader(String name, String value) { headers.add(name, value); return this; } /** * Sets an HTTP message header on this request. The new value will replace existing values for that header name. * * @param name the name of the header to set * @param value the value the header will have */ public Request header(String name, String value) { headers.put(name, value); return this; } /** * Removes the value of the specified header on this request. The name is case insensitive. * * <p> * In case there are multiple headers with that name, all values are removed. * </p> * * @param name the name of the header(s) to remove */ public Request removeHeader(String name) { headers.remove(name); return this; } /** * Reads the value of the <code>Content-Length</code> header sent as part of this request. * If the header is missing, the value -1 is returned. * * @return the <code>Content-Length</code> header value as a long or -1 */ public long contentLength() { String value = header(CONTENT_LENGTH); return value != null ? parseLong(value) : -1; } /** * Reads the value of the <code>Content-Type</code> header sent as part of this request. If the * header is missing, a null value is returned. * * @return the <code>Content-Type</code> header value or null */ public String contentType() { ContentType contentType = ContentType.of(this); return contentType != null ? contentType.mediaType() : null; } /** * Checks for the presence of a specific parameter in this request. * * @param name the name of the parameter * @return true if the parameter is present, false otherwise */ public boolean hasParameter(String name) { return !parameters(name).isEmpty(); } /** * Gets the value of a specific parameter of this request, or null if the parameter does not exist. * * If the parameter has more than one value, the last one is returned. * * <p>Request parameters are contained in the query string or posted form data.</p> * * <p> * Note that changing the body of the request will not cause the parameters to change. To change the request * parameters, use {@link Request#addParameter(String, String)} and {@link Request#removeParameter(String)}. * </p> * * @param name the name of the parameter * @return the parameter value or null */ public String parameter(String name) { List<String> values = parameters(name); return values.isEmpty() ? null : values.get(values.size() - 1); } /** * Gets the list of values of a specific parameter of this request. If the parameter does not exist, the list will * be empty. The returned list is safe for modification and will not affect the request. * * <p>Request parameters are contained in the query string or posted form data.</p> * * <p> * Note that changing the body of the request will not cause the parameters to change. To change the request * parameters, use {@link Request#addParameter(String, String)} and {@link Request#removeParameter(String)}. * </p> * * @param name the name of the parameter * @return the list of that parameter's values */ public List<String> parameters(String name) { return parameters.containsKey(name) ? new ArrayList<>(parameters.get(name)) : new ArrayList<>(); } /** * Returns the names of all the parameters contained in this request. * If the request has no parameter, the method returns an empty <code>Set</code>. The returned * set is safe for modification and will not affect the request. * * <p> * Parameters are taken from the query or HTTP form posting. * </p> * * <p> * Note that changing the body of the request will not cause the parameters to change. To change the request * parameters, use {@link Request#addParameter(String, String)} and {@link Request#removeParameter(String)}. * </p> * * @return the set of parameter names */ public Set<String> parameterNames() { return new LinkedHashSet<>(parameters.keySet()); } /** * Returns a <code>Map</code> of all the parameters contained in this request. If the request has no parameter, * the map will be empty. The map is not modifiable. * * <p> * Parameters are taken from the query or HTTP form posting. * </p> * * <p> * Note that changing the body of the request will not cause the parameters to change. To change the request * parameters, use {@link Request#addParameter(String, String)} and {@link Request#removeParameter(String)}. * </p> * * @return a map containing all the request parameters */ public Map<String, List<String>> allParameters() { return Collections.unmodifiableMap(parameters); } /** * Adds a parameter value to this request. If a parameter with the same name already exists, * the new value is appended to this list of values for that parameter. * * @param name the parameter name * @param value the additional parameter value */ public Request addParameter(String name, String value) { if (!parameters.containsKey(name)) { parameters.put(name, new ArrayList<>()); } parameters.get(name).add(value); return this; } /** * Removes a parameter from this request. This removes all values for that parameter from the request. * * @param name the name of the parameter to remove */ public Request removeParameter(String name) { parameters.remove(name); return this; } /** * Gets the value of a keyed attribute of this request, or null if no attribute with the given key exists. * * <p> * This method will attempt to cast the attribute value to type T, * which can result in a <code>ClassCastException</code>. * </p> * * @see Request#attribute(Object, Object) * @param key the key of the attribute to retrieve * @return the value of the attribute, or null if the attribute does not exist * @throws java.lang.ClassCastException if the attribute value is not an instance of the type parameter */ @SuppressWarnings("unchecked") public <T> T attribute(Object key) { return (T) attributes.get(key); } /** * Sets an attribute on this request. Attributes make available custom information about the request. * * <p> * Attribute keys are unique. If an attribute exists under the same key, its value will be replaced by the new value. * </p> * * @param key the key of the attribute to set * @param value the new attribute value */ public Request attribute(Object key, Object value) { attributes.put(key, value); return this; } /** * Gets the set of all attribute keys on this request. If no attribute exist, the set will be empty. * * <p> * Note that the set is a copy, it can be modified without changing the request. * </p> * * @return the set of attribute keys */ public Set<Object> attributeKeys() { return new HashSet<>(attributes.keySet()); } /** * Removes the attribute with the given key from this request. If no attribute exists, the method does nothing. * * @param key the key of the attribute to remove */ public Request removeAttribute(Object key) { attributes.remove(key); return this; } /** * Gets the map of all attributes of this request. If no attribute exists, the map will be empty. * * <p> * Note that the map is not modifiable. * </p> * * @return the map of request attributes */ public Map<Object, Object> attributes() { return Collections.unmodifiableMap(attributes); } }