content: /*
   * Copyright (C) 2020 ActiveJ 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.activej.http;
  
  import io.activej.async.function.AsyncSupplier;
  import io.activej.bytebuf.ByteBuf;
  import io.activej.common.Checks;
  import io.activej.common.builder.AbstractBuilder;
  import io.activej.http.loader.IStaticLoader;
  import io.activej.http.loader.ResourceIsADirectoryException;
  import io.activej.http.loader.ResourceNotFoundException;
  import io.activej.promise.Promise;
  import io.activej.promise.Promises;
  import io.activej.reactor.AbstractReactive;
  import io.activej.reactor.Reactor;
  import org.jetbrains.annotations.Nullable;
  
  import java.nio.charset.Charset;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Path;
  import java.util.LinkedHashSet;
  import java.util.List;
  import java.util.Set;
  import java.util.concurrent.Executor;
  import java.util.function.Function;
  import java.util.function.Supplier;
  
  import static io.activej.http.HttpHeaderValue.ofContentType;
  import static io.activej.http.HttpHeaders.CONTENT_TYPE;
  import static io.activej.reactor.Reactive.checkInReactorThread;
  
  /**
   * This servlet allows return HTTP responses by HTTP paths from some predefined storage, mainly the filesystem.
   */
  public final class StaticServlet extends AbstractReactive
  	implements AsyncServlet {
  	private static final boolean CHECKS = Checks.isEnabled(StaticServlet.class);
  
  	public static final Charset DEFAULT_TXT_ENCODING = StandardCharsets.UTF_8;
  
  	private final IStaticLoader resourceLoader;
  	private Function<String, ContentType> contentTypeResolver = StaticServlet::getContentType;
  	private Function<HttpRequest, @Nullable String> pathMapper = HttpRequest::getRelativePath;
  	private Supplier<HttpResponse.Builder> responseBuilderSupplier = HttpResponse::ok200;
  	private final Set<String> indexResources = new LinkedHashSet<>();
  
  	private @Nullable String defaultResource;
  
  	private StaticServlet(Reactor reactor, IStaticLoader resourceLoader) {
  		super(reactor);
  		this.resourceLoader = resourceLoader;
  	}
  
  	public static StaticServlet create(Reactor reactor, IStaticLoader resourceLoader) {
  		return builder(reactor, resourceLoader).build();
  	}
  
  	public static StaticServlet create(Reactor reactor, IStaticLoader resourceLoader, String page) {
  		return builder(reactor, resourceLoader).withMappingTo(page).build();
  	}
  
  	public static StaticServlet ofClassPath(Reactor reactor, Executor executor, String path) {
  		return builder(reactor, IStaticLoader.ofClassPath(reactor, executor, path)).build();
  	}
  
  	public static StaticServlet ofPath(Reactor reactor, Executor executor, Path path) {
  		return builder(reactor, IStaticLoader.ofPath(reactor, executor, path)).build();
  	}
  
  	public static Builder builder(Reactor reactor, IStaticLoader resourceLoader) {
  		return new StaticServlet(reactor, resourceLoader).new Builder();
  	}
  
  	public final class Builder extends AbstractBuilder<Builder, StaticServlet> {
  		private Builder() {}
  
  		@SuppressWarnings("UnusedReturnValue")
  		public Builder withContentType(ContentType contentType) {
  			checkNotBuilt(this);
  			return withContentTypeResolver($ -> contentType);
  		}
  
  		public Builder withContentTypeResolver(Function<String, ContentType> contentTypeResolver) {
  			checkNotBuilt(this);
  			StaticServlet.this.contentTypeResolver = contentTypeResolver;
  			return this;
  		}
  
  		public Builder withMapping(Function<HttpRequest, String> fn) {
  			checkNotBuilt(this);
  			pathMapper = fn;
  			return this;
  		}
  
  		public Builder withMappingTo(String path) {
  			checkNotBuilt(this);
  			//noinspection RedundantCast - it does not compile without the cast
  			if (StaticServlet.this.contentTypeResolver == (Function<String, ContentType>) StaticServlet::getContentType) {
  				withContentType(getContentType(path));
  			}
  			return withMapping($ -> path);
  		}
  
  		public Builder withMappingNotFoundTo(String defaultResource) {
  			checkNotBuilt(this);
  			StaticServlet.this.defaultResource = defaultResource;
  			return this;
  		}
  
  		public Builder withIndexResources(String... indexResources) {
  			checkNotBuilt(this);
  			StaticServlet.this.indexResources.addAll(List.of(indexResources));
  			return this;
  		}
  
  		public Builder withIndexHtml() {
  			checkNotBuilt(this);
  			StaticServlet.this.indexResources.add("index.html");
  			return this;
  		}
  
  		public Builder withResponse(Supplier<HttpResponse.Builder> responseBuilderSupplier) {
  			checkNotBuilt(this);
  			StaticServlet.this.responseBuilderSupplier = responseBuilderSupplier;
  			return this;
  		}
  
  		@Override
  		protected StaticServlet doBuild() {
  			return StaticServlet.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 Promise<HttpResponse> createHttpResponse(ByteBuf buf, ContentType contentType) {
  		return responseBuilderSupplier.get()
  			.withBody(buf)
  			.withHeader(CONTENT_TYPE, ofContentType(contentType))
  			.toPromise();
  	}
  
  	@Override
  	public Promise<HttpResponse> serve(HttpRequest request) {
  		if (CHECKS) checkInReactorThread(this);
  		String mappedPath = pathMapper.apply(request);
  		if (mappedPath == null) return Promise.ofException(HttpError.notFound404());
  		ContentType contentType = contentTypeResolver.apply(mappedPath);
  		return Promise.complete()
  			.then(() -> (mappedPath.endsWith("/") || mappedPath.isEmpty()) ?
  				tryLoadIndexResource(mappedPath) :
  				resourceLoader.load(mappedPath)
  					.then(byteBuf -> createHttpResponse(byteBuf, contentType))
  					.then((value, e) -> {
  						if (e instanceof ResourceIsADirectoryException) {
  							return tryLoadIndexResource(mappedPath);
  						} else {
  							return Promise.of(value, e);
  						}
  					}))
  			.then(Promise::of,
  				e -> e instanceof ResourceNotFoundException ?
  					tryLoadDefaultResource() :
  					Promise.ofException(HttpError.ofCode(400, e)));
  	}
  
  	private Promise<HttpResponse> tryLoadIndexResource(String mappedPath) {
  		String dirPath = mappedPath.endsWith("/") || mappedPath.isEmpty() ? mappedPath : (mappedPath + '/');
  		return Promises.first(
  				indexResources.stream()
  					.map(indexResource -> (AsyncSupplier<HttpResponse>) () ->
  						resourceLoader.load(dirPath + indexResource)
  							.then(byteBuf -> createHttpResponse(byteBuf, contentTypeResolver.apply(indexResource)))))
  			.mapException(e -> new ResourceNotFoundException("Could not find '" + mappedPath + '\'', e));
  	}
  
  	private Promise<HttpResponse> tryLoadDefaultResource() {
  		return defaultResource != null ?
  			resourceLoader.load(defaultResource)
  				.then(buf -> createHttpResponse(buf, contentTypeResolver.apply(defaultResource))) :
  			Promise.ofException(HttpError.notFound404());
  	}
  }
  