io.github.stylesmile.server.Response Maven / Gradle / Ivy
package io.github.stylesmile.server;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;
import static io.github.stylesmile.server.JdkHTTPServer.*;
/**
* The {@code Response} class encapsulates a single HTTP response.
*/
public class Response implements Closeable {
protected OutputStream out; // the underlying output stream
protected OutputStream encodedOut; // chained encoder streams
protected Headers headers;
protected boolean discardBody;
protected int state; // nothing sent, headers sent, or closed
protected Request req; // request used in determining client capabilities
/**
* Constructs a Response whose output is written to the given stream.
*
* @param out the stream to which the response is written
*/
public Response(OutputStream out) {
this.out = out;
this.headers = new Headers();
}
/**
* Sets whether this response's body is discarded or sent.
*
* @param discardBody specifies whether the body is discarded or not
*/
public void setDiscardBody(boolean discardBody) {
this.discardBody = discardBody;
}
/**
* Sets the request which is used in determining the capabilities
* supported by the client (e.g. compression, encoding, etc.)
*
* @param req the request
*/
public void setClientCapabilities(Request req) {
this.req = req;
}
/**
* Returns the request headers collection.
*
* @return the request headers collection
*/
public Headers getHeaders() {
return headers;
}
/**
* set header
* @param key key
* @param value value
*/
public void setHeader(String key, String value) {
this.headers.add(key, value);
}
/**
* Returns the underlying output stream to which the response is written.
* Except for special cases, you should use {@link #getBody()} instead.
*
* @return the underlying output stream to which the response is written
*/
public OutputStream getOutputStream() {
return out;
}
/**
* Returns whether the response headers were already sent.
*
* @return whether the response headers were already sent
*/
public boolean headersSent() {
return state == 1;
}
/**
* Returns an output stream into which the response body can be written.
* The stream applies encodings (e.g. compression) according to the sent headers.
* This method must be called after response headers have been sent
* that indicate there is a body. Normally, the content should be
* prepared (not sent) even before the headers are sent, so that any
* errors during processing can be caught and a proper error response returned -
* after the headers are sent, it's too late to change the status into an error.
*
* @return an output stream into which the response body can be written,
* or null if the body should not be written (e.g. it is discarded)
* @throws IOException if an error occurs
*/
public OutputStream getBody() throws IOException {
if (encodedOut != null || discardBody)
return encodedOut; // return the existing stream (or null)
// set up chain of encoding streams according to headers
List te = Arrays.asList(splitElements(headers.get("Transfer-Encoding"), true));
List ce = Arrays.asList(splitElements(headers.get("Content-Encoding"), true));
encodedOut = new ResponseOutputStream(out); // leaves underlying stream open when closed
if (te.contains("chunked"))
encodedOut = new JdkHTTPServer.ChunkedOutputStream(encodedOut);
if (ce.contains("gzip") || te.contains("gzip"))
encodedOut = new GZIPOutputStream(encodedOut, 4096);
else if (ce.contains("deflate") || te.contains("deflate"))
encodedOut = new DeflaterOutputStream(encodedOut);
return encodedOut; // return the outer-most stream
}
/**
* Closes this response and flushes all output.
*
* @throws IOException if an error occurs
*/
public void close() throws IOException {
state = -1; // closed
if (encodedOut != null)
encodedOut.close(); // close all chained streams (except the underlying one)
out.flush(); // always flush underlying stream (even if getBody was never called)
}
/**
* Sends the response headers with the given response status.
* A Date header is added if it does not already exist.
* If the response has a body, the Content-Length/Transfer-Encoding
* and Content-Type headers must be set before sending the headers.
*
* @param status the response status
* @throws IOException if an error occurs or headers were already sent
* @see #sendHeaders(int, long, long, String, String, long[])
*/
public void sendHeaders(int status) throws IOException {
if (headersSent())
throw new IOException("headers were already sent");
if (!headers.contains("Date"))
headers.add("Date", formatDate(System.currentTimeMillis()));
headers.add("Server", "JLHTTP/2.6");
out.write(getBytes("HTTP/1.1 ", Integer.toString(status), " ", statuses[status]));
out.write(CRLF);
headers.writeTo(out);
state = 1; // headers sent
}
/**
* Sends the response headers, including the given response status
* and description, and all response headers. If they do not already
* exist, the following headers are added as necessary:
* Content-Range, Content-Type, Transfer-Encoding, Content-Encoding,
* Content-Length, Last-Modified, ETag, Connection and Date. Ranges are
* properly calculated as well, with a 200 status changed to a 206 status.
*
* @param status the response status
* @param length the response body length, or zero if there is no body,
* or negative if there is a body but its length is not yet known
* @param lastModified the last modified date of the response resource,
* or non-positive if unknown. A time in the future will be
* replaced with the current system time.
* @param etag the ETag of the response resource, or null if unknown
* (see RFC2616#3.11)
* @param contentType the content type of the response resource, or null
* if unknown (in which case "application/octet-stream" will be sent)
* @param range the content range that will be sent, or null if the
* entire resource will be sent
* @throws IOException if an error occurs
*/
public void sendHeaders(int status, long length, long lastModified,
String etag, String contentType, long[] range) throws IOException {
if (range != null) {
headers.add("Content-Range", "bytes " + range[0] + "-" +
range[1] + "/" + (length >= 0 ? length : "*"));
length = range[1] - range[0] + 1;
if (status == 200)
status = 206;
}
String ct = headers.get("Content-Type");
if (ct == null) {
ct = contentType != null ? contentType : "application/octet-stream";
headers.add("Content-Type", ct);
} else {
if (contentType != null) { //noear,20181220
ct = contentType;
headers.replace("Content-Type", ct);
}
}
if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding")) {
// RFC2616#3.6: transfer encodings are case-insensitive and must not be sent to an HTTP/1.0 client
boolean modern = req != null && req.getVersion().endsWith("1.1");
String accepted = req == null ? null : req.getHeaders().get("Accept-Encoding");
List encodings = Arrays.asList(splitElements(accepted, true));
String compression = encodings.contains("gzip") ? "gzip" :
encodings.contains("deflate") ? "deflate" : null;
if (compression != null && (length < 0 || length > 300) && isCompressible(ct) && modern) {
//todo: by noear 20220316; add() -> replace()
headers.replace("Transfer-Encoding", "chunked"); // compressed data is always unknown length
headers.replace("Content-Encoding", compression);
} else if (length < 0 && modern) {
headers.replace("Transfer-Encoding", "chunked"); // unknown length
} else if (length >= 0) {
headers.replace("Content-Length", Long.toString(length)); // known length
}
}
if (!headers.contains("Vary")) // RFC7231#7.1.4: Vary field should include headers
headers.add("Vary", "Accept-Encoding"); // that are used in selecting representation
if (lastModified > 0 && !headers.contains("Last-Modified")) // RFC2616#14.29
headers.add("Last-Modified", formatDate(Math.min(lastModified, System.currentTimeMillis())));
if (etag != null && !headers.contains("ETag"))
headers.add("ETag", etag);
if (req != null && "close".equalsIgnoreCase(req.getHeaders().get("Connection"))
&& !headers.contains("Connection"))
headers.add("Connection", "close"); // #RFC7230#6.6: should reply to close with close
sendHeaders(status);
}
/**
* Sends the full response with the given status, and the given string
* as the body. The text is sent in the UTF-8 charset. If a
* Content-Type header was not explicitly set, it will be set to
* text/html, and so the text must contain valid (and properly
* {@link JdkHTTPServer#escapeHTML escaped}) HTML.
*
* @param status the response status
* @param text the text body (sent as text/html)
* @throws IOException if an error occurs
*/
public void send(int status, String text) throws IOException {
byte[] content = text.getBytes(StandardCharsets.UTF_8);
sendHeaders(status, content.length, -1,
"W/\"" + Integer.toHexString(text.hashCode()) + "\"",
// "text/html; charset=utf-8", null);
"application/json; charset=utf-8", null);
headers.add("Content-Type", "application/json; charset=utf-8");
OutputStream out = getBody();
if (out != null)
out.write(content);
}
public void sendHtml(int status, byte[] content) throws IOException {
sendHeaders(status, content.length, -1,
"W/\"" + Integer.toHexString(content.hashCode()) + "\"",
"text/html; charset=utf-8", null);
// "application/json; charset=utf-8", null);
headers.add("Content-Type", "application/json; charset=utf-8");
OutputStream out = getBody();;
if (out != null){
out.write(content);
}
}
public void send2(int status, String text) throws IOException {
byte[] content = text.getBytes(StandardCharsets.UTF_8);
sendHeaders(status, content.length, -1,
"W" + Integer.toHexString(text.hashCode()) + "",
// "text/html; charset=utf-8", null);
"application/json; charset=utf-8", null);
OutputStream out = getBody();
if (out != null)
out.write(content);
}
public void send(int status) throws IOException {
}
/**
* Sends an error response with the given status and detailed message.
* An HTML body is created containing the status and its description,
* as well as the message, which is escaped using the
* {@link JdkHTTPServer#escapeHTML escape} method.
*
* @param status the response status
* @param text the text body (sent as text/html)
* @throws IOException if an error occurs
*/
public void sendError(int status, String text) throws IOException {
send(status, String.format(
"%n%n%d %s %n" +
"%d %s
%n%s
%n",
status, statuses[status], status, statuses[status], escapeHTML(text)));
}
/**
* Sends an error response with the given status and default body.
*
* @param status the response status
* @throws IOException if an error occurs
*/
public void sendError(int status) throws IOException {
String text = status < 400 ? ":)" : "sorry it didn't work out :(";
sendError(status, text);
}
/**
* Sends the response body. This method must be called only after the
* response headers have been sent (and indicate that there is a body).
*
* @param body a stream containing the response body
* @param length the full length of the response body, or -1 for the whole stream
* @param range the sub-range within the response body that should be
* sent, or null if the entire body should be sent
* @throws IOException if an error occurs
*/
public void sendBody(InputStream body, long length, long[] range) throws IOException {
OutputStream out = getBody();
if (out != null) {
if (range != null) {
long offset = range[0];
length = range[1] - range[0] + 1;
while (offset > 0) {
long skip = body.skip(offset);
if (skip == 0)
throw new IOException("can't skip to " + range[0]);
offset -= skip;
}
}
transfer(body, out, length);
}
}
/**
* Sends a 301 or 302 response, redirecting the client to the given URL.
*
* @param url the absolute URL to which the client is redirected
* @param permanent specifies whether a permanent (301) or
* temporary (302) redirect status is sent
* @throws IOException if an IO error occurs or url is malformed
*/
public void redirect(String url, boolean permanent) throws IOException {
try {
url = new URI(url).toASCIIString();
} catch (URISyntaxException e) {
throw new IOException("malformed URL: " + url);
}
headers.add("Location", url);
// some user-agents expect a body, so we send it
if (permanent)
sendError(301, "Permanently moved to " + url);
else
sendError(302, "Temporarily moved to " + url);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy