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.bytebuf.ByteBuf;
import io.datakernel.codec.StructuredEncoder;
import io.datakernel.codec.json.JsonUtils;
import io.datakernel.common.Initializable;
import io.datakernel.csp.ChannelSupplier;
import io.datakernel.http.HttpHeaderValue.HttpHeaderValueOfSetCookies;
import io.datakernel.promise.Async;
import io.datakernel.promise.Promise;
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 Async, 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 = new HttpResponse(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);
	}

	@NotNull
	public static Promise file(FileSliceSupplier downloader, String name, long size, @Nullable String rangeHeader) {
		HttpResponse response = new HttpResponse(rangeHeader == null ? 200 : 206);

		String localName = name.substring(name.lastIndexOf('/') + 1);
		MediaType mediaType = MediaTypes.getByExtension(localName.substring(localName.lastIndexOf('.') + 1));
		if (mediaType == null) {
			mediaType = OCTET_STREAM;
		}

		response.addHeader(CONTENT_TYPE, HttpHeaderValue.ofContentType(ContentType.of(mediaType)));
		response.addHeader(ACCEPT_RANGES, "bytes");
		response.addHeader(CONTENT_DISPOSITION, "attachment; filename=\"" + localName + "\"");

		long contentLength, offset;
		if (rangeHeader != null) {
			if (!rangeHeader.startsWith("bytes=")) {
				return Promise.ofException(HttpException.ofCode(416, "Invalid range header (not in bytes)"));
			}
			rangeHeader = rangeHeader.substring(6);
			if (!rangeHeader.matches("(?:\\d+)?-(?:\\d+)?")) {
				return Promise.ofException(HttpException.ofCode(416, "Only single part ranges are allowed"));
			}
			String[] parts = rangeHeader.split("-", 2);
			long endOffset;
			if (parts[0].isEmpty()) {
				if (parts[1].isEmpty()) {
					return Promise.ofException(HttpException.ofCode(416, "Invalid range"));
				}
				offset = size - Long.parseLong(parts[1]);
				endOffset = size;
			} else {
				if (parts[1].isEmpty()) {
					offset = Long.parseLong(parts[0]);
					endOffset = size - 1;
				} else {
					offset = Long.parseLong(parts[0]);
					endOffset = Long.parseLong(parts[1]);
				}
			}
			if (endOffset != -1 && offset > endOffset) {
				return Promise.ofException(HttpException.ofCode(416, "Invalid range"));
			}
			contentLength = endOffset - offset + 1;
			response.addHeader(CONTENT_RANGE, "bytes " + offset + "-" + endOffset + "/" + size);
		} else {
			contentLength = size;
			offset = 0;
		}
		response.addHeader(CONTENT_LENGTH, Long.toString(contentLength));
		response.setBodyStream(ChannelSupplier.ofPromise(downloader.getFileSlice(offset, contentLength)));
		return Promise.of(response);
	}

	@NotNull
	public static Promise file(FileSliceSupplier downloader, String name, long size) {
		return file(downloader, name, size, null);
	}
	// 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) {
		addHeader(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));
	}

	@Override
	public Promise get() {
		return Promise.of(this);
	}

	@FunctionalInterface
	public interface FileSliceSupplier {

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

	// 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