All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.http.HttpResponse Maven / Gradle / Ivy

/*
 * Copyright (C) 2015-2019 SoftIndex LLC.
 *
 * 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.datakernel.http;

import io.datakernel.async.Promise;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.codec.StructuredEncoder;
import io.datakernel.codec.json.JsonUtils;
import io.datakernel.csp.ChannelSupplier;
import io.datakernel.http.HttpHeaderValue.HttpHeaderValueOfSetCookies;
import io.datakernel.util.Initializable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static io.datakernel.bytebuf.ByteBufStrings.encodeAscii;
import static io.datakernel.bytebuf.ByteBufStrings.putPositiveInt;
import static io.datakernel.http.ContentTypes.*;
import static io.datakernel.http.HttpHeaderValue.ofContentType;
import static io.datakernel.http.HttpHeaders.*;
import static io.datakernel.http.MediaTypes.OCTET_STREAM;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Represents HTTP response for {@link HttpRequest}. After handling {@code HttpResponse} will be recycled so you cannot
 * usi it afterwards.
 */
public final class HttpResponse extends HttpMessage implements Initializable {
	private static final byte[] HTTP11_BYTES = encodeAscii("HTTP/1.1 ");
	private static final byte[] CODE_ERROR_BYTES = encodeAscii(" Error");
	private static final byte[] CODE_OK_BYTES = encodeAscii(" OK");
	private static final byte[] CODE_200_BYTES = encodeAscii("HTTP/1.1 200 OK");
	private static final byte[] CODE_201_BYTES = encodeAscii("HTTP/1.1 201 Created");
	private static final byte[] CODE_206_BYTES = encodeAscii("HTTP/1.1 206 Partial Content");
	private static final byte[] CODE_302_BYTES = encodeAscii("HTTP/1.1 302 Found");
	private static final byte[] CODE_400_BYTES = encodeAscii("HTTP/1.1 400 Bad Request");
	private static final byte[] CODE_403_BYTES = encodeAscii("HTTP/1.1 403 Forbidden");
	private static final byte[] CODE_404_BYTES = encodeAscii("HTTP/1.1 404 Not Found");
	private static final byte[] CODE_500_BYTES = encodeAscii("HTTP/1.1 500 Internal Server Error");
	private static final byte[] CODE_502_BYTES = encodeAscii("HTTP/1.1 502 Bad Gateway");
	private static final byte[] CODE_503_BYTES = encodeAscii("HTTP/1.1 503 Service Unavailable");
	private static final int LONGEST_FIRST_LINE_SIZE = CODE_500_BYTES.length;

	private final int code;

	@Nullable
	private Map parsedCookies;

	// region creators
	HttpResponse(int code) {
		this.code = code;
	}

	@NotNull
	public static HttpResponse ofCode(int code) {
		assert code >= 100 && code < 600;
		return new HttpResponse(code);
	}

	@NotNull
	public static HttpResponse ok200() {
		return new HttpResponse(200);
	}

	@NotNull
	public static HttpResponse ok201() {
		return new HttpResponse(201);
	}

	@NotNull
	public static HttpResponse ok206() {
		return new HttpResponse(206);
	}

	@NotNull
	public static HttpResponse redirect302(@NotNull String url) {
		HttpResponse response = HttpResponse.ofCode(302);
		// RFC-7231, section 6.4.3 (https://tools.ietf.org/html/rfc7231#section-6.4.3)
		response.addHeader(LOCATION, url);
		return response;
	}

	@NotNull
	public static HttpResponse unauthorized401(@NotNull String challenge) {
		HttpResponse response = new HttpResponse(401);
		// RFC-7235, section 3.1 (https://tools.ietf.org/html/rfc7235#section-3.1)
		response.addHeader(WWW_AUTHENTICATE, challenge);
		return response;
	}

	@NotNull
	public static HttpResponse notFound404() {
		return new HttpResponse(404);
	}
	// endregion

	// region common builder methods
	@NotNull
	public HttpResponse withHeader(@NotNull HttpHeader header, @NotNull String value) {
		addHeader(header, value);
		return this;
	}

	@NotNull
	public HttpResponse withHeader(@NotNull HttpHeader header, @NotNull byte[] bytes) {
		addHeader(header, bytes);
		return this;
	}

	@NotNull
	public HttpResponse withHeader(@NotNull HttpHeader header, @NotNull HttpHeaderValue value) {
		addHeader(header, value);
		return this;
	}

	@Override
	public void addCookies(@NotNull List cookies) {
		for (HttpCookie cookie : cookies) {
			addCookie(cookie);
		}
	}

	@Override
	public void addCookie(@NotNull HttpCookie cookie) {
		headers.add(SET_COOKIE, new HttpHeaderValueOfSetCookies(cookie));
	}

	@NotNull
	public HttpResponse withCookies(@NotNull List cookies) {
		addCookies(cookies);
		return this;
	}

	@NotNull
	public HttpResponse withCookies(@NotNull HttpCookie... cookies) {
		addCookies(cookies);
		return this;
	}

	@NotNull
	public HttpResponse withCookie(@NotNull HttpCookie cookie) {
		addCookie(cookie);
		return this;
	}

	@NotNull
	public HttpResponse withBodyGzipCompression() {
		setBodyGzipCompression();
		return this;
	}

	@NotNull
	public HttpResponse withBody(@NotNull ByteBuf body) {
		setBody(body);
		return this;
	}

	@NotNull
	public HttpResponse withBody(@NotNull byte[] array) {
		setBody(array);
		return this;
	}

	@NotNull
	public HttpResponse withBodyStream(@NotNull ChannelSupplier stream) {
		setBodyStream(stream);
		return this;
	}

	@NotNull
	public HttpResponse withPlainText(@NotNull String text) {
		return withHeader(CONTENT_TYPE, ofContentType(PLAIN_TEXT_UTF_8))
				.withBody(text.getBytes(UTF_8));
	}

	@NotNull
	public HttpResponse withHtml(@NotNull String text) {
		return withHeader(CONTENT_TYPE, ofContentType(HTML_UTF_8))
				.withBody(text.getBytes(UTF_8));
	}

	@NotNull
	public  HttpResponse withJson(StructuredEncoder encoder, T object) {
		return withHeader(CONTENT_TYPE, ofContentType(JSON_UTF_8))
				.withBody(JsonUtils.toJson(encoder, object).getBytes(UTF_8));
	}

	@FunctionalInterface
	public interface HttpDownloader {

		Promise> download(long offset, long limit);
	}

	public HttpResponse withFile(HttpRequest request, HttpDownloader downloader, String name, long size) throws HttpException {
		String localName = name.substring(name.lastIndexOf('/') + 1);
		String headerRange = request.getHeader(RANGE);
		if (headerRange == null) {
			return withHeader(CONTENT_TYPE, HttpHeaderValue.ofContentType(ContentType.of(OCTET_STREAM)))
					.withHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + localName + "\"")
					.withHeader(ACCEPT_RANGES, "bytes")
					.withHeader(CONTENT_LENGTH, Long.toString(size))
					.withBodyStream(ChannelSupplier.ofPromise(downloader.download(0, -1)));
		}
		if (!headerRange.startsWith("bytes=")) {
			throw HttpException.ofCode(416, "Invalid range header (not in bytes)");
		}
		headerRange = headerRange.substring(6);
		if (!headerRange.matches("(\\d+)?-(\\d+)?")) {
			throw HttpException.ofCode(416, "Only single part ranges are allowed");
		}
		String[] parts = headerRange.split("-", 2);
		long offset = parts[0].isEmpty() ? 0 : Long.parseLong(parts[0]);
		long endOffset = parts[1].isEmpty() ? -1 : Long.parseLong(parts[1]);
		if (endOffset != -1 && offset > endOffset) {
			throw HttpException.ofCode(416, "Invalid range");
		}
		long length = (endOffset == -1 ? size : endOffset) - offset + 1;

		return withHeader(CONTENT_TYPE, HttpHeaderValue.ofContentType(ContentType.of(OCTET_STREAM)))
				.withHeader(CONTENT_DISPOSITION, HttpHeaderValue.of("attachment; filename=\"" + localName + "\""))
				.withHeader(ACCEPT_RANGES, "bytes")
				.withHeader(CONTENT_RANGE, offset + "-" + (offset + length) + "/" + size)
				.withHeader(CONTENT_LENGTH, "" + length)
				.withBodyStream(ChannelSupplier.ofPromise(downloader.download(offset, length)));
	}

	// endregion

	public int getCode() {
		assert !isRecycled();
		return code;
	}

	@NotNull
	public Map getCookies() {
		if (parsedCookies != null) {
			return parsedCookies;
		}
		Map cookies = new LinkedHashMap<>();
		for (HttpCookie cookie : getHeader(SET_COOKIE, HttpHeaderValue::toFullCookies)) {
			cookies.put(cookie.getName(), cookie);
		}
		return parsedCookies = cookies;
	}

	@Nullable
	public HttpCookie getCookie(@NotNull String cookie) {
		return getCookies().get(cookie);
	}

	private static void writeCodeMessageEx(@NotNull ByteBuf buf, int code) {
		buf.put(HTTP11_BYTES);
		putPositiveInt(buf, code);
		if (code >= 400) {
			buf.put(CODE_ERROR_BYTES);
		} else {
			buf.put(CODE_OK_BYTES);
		}
	}

	private static void writeCodeMessage(@NotNull ByteBuf buf, int code) {
		byte[] result;
		switch (code) {
			case 200:
				result = CODE_200_BYTES;
				break;
			case 201:
				result = CODE_201_BYTES;
				break;
			case 206:
				result = CODE_206_BYTES;
				break;
			case 302:
				result = CODE_302_BYTES;
				break;
			case 400:
				result = CODE_400_BYTES;
				break;
			case 403:
				result = CODE_403_BYTES;
				break;
			case 404:
				result = CODE_404_BYTES;
				break;
			case 500:
				result = CODE_500_BYTES;
				break;
			case 502:
				result = CODE_502_BYTES;
				break;
			case 503:
				result = CODE_503_BYTES;
				break;
			default:
				writeCodeMessageEx(buf, code);
				return;
		}
		buf.put(result);
	}

	@Override
	protected int estimateSize() {
		return estimateSize(LONGEST_FIRST_LINE_SIZE);
	}

	@Override
	protected void writeTo(@NotNull ByteBuf buf) {
		writeCodeMessage(buf, code);
		writeHeaders(buf);
	}

	@Override
	public String toString() {
		return HttpResponse.class.getSimpleName() + ": " + code;
	}
	// endregion
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy