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

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

/*
 * Copyright (C) 2015-2018 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.http.loader.StaticLoader;
import io.datakernel.promise.Promise;
import io.datakernel.promise.Promises;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.function.Supplier;

import static io.datakernel.http.HttpHeaderValue.ofContentType;
import static io.datakernel.http.HttpHeaders.CONTENT_TYPE;

/**
 * This servlet allows return HTTP responses by HTTP paths from some predefined storage, mainly the filesystem.
 */
public final class StaticServlet implements AsyncServlet {
	public static final Charset DEFAULT_TXT_ENCODING = StandardCharsets.UTF_8;

	private final StaticLoader resourceLoader;
	private Function contentTypeResolver = StaticServlet::getContentType;
	private Function mapper = HttpRequest::getRelativePath;
	private Supplier responseSupplier = HttpResponse::ok200;
	private final Set indexResources = new LinkedHashSet<>();

	@Nullable
	private String defaultResource;

	private StaticServlet(StaticLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

	public static StaticServlet create(StaticLoader resourceLoader) {
		return new StaticServlet(resourceLoader);
	}

	public static StaticServlet create(StaticLoader resourceLoader, String page) {
		return create(resourceLoader).withMappingTo(page);
	}

	public static StaticServlet ofClassPath(Executor executor, String path) {
		return new StaticServlet(StaticLoader.ofClassPath(executor, path));
	}

	public static StaticServlet ofPath(Executor executor, Path path) {
		return new StaticServlet(StaticLoader.ofPath(executor, path));
	}

	public StaticServlet withContentType(ContentType contentType) {
		return withContentTypeResolver($ -> contentType);
	}

	public StaticServlet withContentTypeResolver(Function contentTypeResolver) {
		this.contentTypeResolver = contentTypeResolver;
		return this;
	}

	public StaticServlet withMapping(Function fn) {
		mapper = fn;
		return this;
	}

	public StaticServlet withMappingTo(String path) {
		//noinspection RedundantCast - it does not compile without the cast
		if (this.contentTypeResolver == (Function) StaticServlet::getContentType) {
			withContentType(getContentType(path));
		}
		return withMapping($ -> path);
	}

	public StaticServlet withMappingNotFoundTo(String defaultResource) {
		this.defaultResource = defaultResource;
		return this;
	}

	public StaticServlet withIndexResources(String... indexResources) {
		this.indexResources.addAll(Arrays.asList(indexResources));
		return this;
	}

	public StaticServlet withIndexHtml() {
		this.indexResources.add("index.html");
		return this;
	}

	public StaticServlet withResponse(Supplier responseSupplier) {
		this.responseSupplier = responseSupplier;
		return this;
	}

	public static ContentType getContentType(String path) {
		int pos = path.lastIndexOf(".");
		if (pos == -1) {
			return ContentType.of(MediaTypes.OCTET_STREAM);
		}

		String ext = path.substring(pos + 1);

		MediaType mime = MediaTypes.getByExtension(ext);
		if (mime == null) {
			mime = MediaTypes.OCTET_STREAM;
		}

		ContentType type;
		if (mime.isTextType()) {
			type = ContentType.of(mime, DEFAULT_TXT_ENCODING);
		} else {
			type = ContentType.of(mime);
		}

		return type;
	}

	private HttpResponse createHttpResponse(ByteBuf buf, ContentType contentType) {
		return responseSupplier.get()
				.withBody(buf)
				.withHeader(CONTENT_TYPE, ofContentType(contentType));
	}

	@NotNull
	@Override
	public final Promise serve(@NotNull HttpRequest request) {
		String mappedPath = mapper.apply(request);
		if (mappedPath == null) return Promise.ofException(HttpException.notFound404());
		ContentType contentType = contentTypeResolver.apply(mappedPath);
		return Promise.complete()
				.then($ -> (mappedPath.endsWith("/") || mappedPath.isEmpty()) ?
						tryLoadIndexResource(mappedPath) :
						resourceLoader.load(mappedPath)
								.map(byteBuf -> createHttpResponse(byteBuf, contentType))
								.thenEx((value, e) -> {
									if (e == StaticLoader.IS_A_DIRECTORY) {
										return tryLoadIndexResource(mappedPath);
									} else {
										return Promise.of(value, e);
									}
								}))
				.thenEx((response, e) -> {
					if (e == null) {
						return Promise.of(response);
					} else if (e == StaticLoader.NOT_FOUND_EXCEPTION) {
						return tryLoadDefaultResource();
					} else {
						return Promise.ofException(HttpException.ofCode(400, e));
					}
				});
	}

	@NotNull
	private Promise tryLoadIndexResource(String mappedPath) {
		String dirPath = mappedPath.endsWith("/") || mappedPath.isEmpty() ? mappedPath : (mappedPath + '/');
		return Promises.firstSuccessful(indexResources.stream()
				.map(indexResource -> () -> resourceLoader.load(dirPath + indexResource)
						.map(byteBuf -> createHttpResponse(byteBuf, contentTypeResolver.apply(indexResource)))))
				.thenEx(((response, e) -> e == null ? Promise.of(response) : Promise.ofException(StaticLoader.NOT_FOUND_EXCEPTION)));
	}

	@NotNull
	private Promise tryLoadDefaultResource() {
		return defaultResource != null ?
				resourceLoader.load(defaultResource)
						.map(buf -> createHttpResponse(buf, contentTypeResolver.apply(defaultResource))) :
				Promise.ofException(HttpException.notFound404());
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy