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

io.quarkus.vertx.http.runtime.handlers.DevStaticHandler Maven / Gradle / Ivy

package io.quarkus.vertx.http.runtime.handlers;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Set;

import org.jboss.logging.Logger;

import io.quarkus.vertx.http.runtime.GeneratedStaticResourcesRecorder;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.impl.MimeMapping;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.StaticHandler;

/**
 * {@link StaticHandler} implementation to handle static resources using the Classpath.
 * This is meant to be used on {@link io.quarkus.runtime.LaunchMode#DEVELOPMENT} mode.
 */
public class DevStaticHandler implements Handler {

    private static final Logger LOG = Logger.getLogger(DevStaticHandler.class);
    private static final int HTTP_STATUS_OK = 200;
    private static final int HTTP_STATUS_NO_CONTENT = 204;
    private static final String ALLOW_HEADER = "Allow";
    private static final String ALLOW_HEADER_VALUE = "HEAD,GET,OPTIONS";
    private final Set generatedClasspathResources;
    private final Map generatedFilesResources;
    private final Set compressedMediaTypes;
    private final ClassLoader currentClassLoader;
    private final boolean enableCompression;
    private final String indexPage;
    private final Charset defaultEncoding;

    public DevStaticHandler(Set generatedClasspathResources, Map generatedFilesResources,
            DevClasspathStaticHandlerOptions options) {
        this.generatedClasspathResources = generatedClasspathResources;
        this.generatedFilesResources = generatedFilesResources;
        this.compressedMediaTypes = options.getCompressMediaTypes();
        this.currentClassLoader = Thread.currentThread().getContextClassLoader();
        this.enableCompression = options.isEnableCompression();
        this.indexPage = options.getIndexPage();
        this.defaultEncoding = options.getDefaultEncoding();
    }

    @Override
    public void handle(RoutingContext context) {

        String resolvedPath = resolvePath(context);
        String path = resolvedPath.endsWith("/") ? resolvedPath.concat(this.indexPage) : resolvedPath;

        if (LOG.isDebugEnabled()) {
            LOG.debugf("Handling request for path '%s'", path);
        }

        boolean containsGeneratedResource = this.generatedClasspathResources.contains(path)
                || this.generatedFilesResources.containsKey(path);

        if (!containsGeneratedResource) {
            beforeNextHandler(this.currentClassLoader, context);
            return;
        }

        if (context.request().method().equals(HttpMethod.OPTIONS)) {
            context.response().putHeader(ALLOW_HEADER, ALLOW_HEADER_VALUE).setStatusCode(HTTP_STATUS_NO_CONTENT)
                    .send();
            return;
        }

        compressIfNeeded(context, path);

        if (generatedFilesResources.containsKey(path)) {
            context.vertx().fileSystem().readFile(generatedFilesResources.get(path), r -> {
                if (r.succeeded()) {
                    handleAsyncResultSucceeded(context, r.result(), path);
                } else {
                    context.fail(r.cause());
                }
            });
            return;
        }

        context.vertx().executeBlocking(future -> {
            try {
                byte[] content = getClasspathResourceContent(path);
                future.complete(content);
            } catch (Exception e) {
                future.fail(e);
            }
        }, asyncResult -> {
            if (asyncResult.succeeded()) {
                byte[] result = (byte[]) asyncResult.result();
                handleAsyncResultSucceeded(context, result == null ? null : Buffer.buffer(result), path);
            } else {
                context.fail(asyncResult.cause());
            }
        });
    }

    private void handleAsyncResultSucceeded(RoutingContext context, Buffer result, String path) {

        if (result == null) {
            LOG.warnf("The '%s' file does not contain any content. Proceeding to the next handler if it exists", path);
            beforeNextHandler(this.currentClassLoader, context);
            return;
        }
        String contentType = MimeMapping.getMimeTypeForFilename(path);
        if (contentType != null) {
            if (contentType.startsWith("text")) {
                context.response().putHeader(HttpHeaders.CONTENT_TYPE, contentType + ";charset=" + defaultEncoding);
            } else {
                context.response().putHeader(HttpHeaders.CONTENT_TYPE, contentType);
            }
        }
        if (context.request().method().equals(HttpMethod.HEAD)) {
            handleHeadMethod(context, result);
        } else {
            context.response().send(result);
        }
    }

    private void handleHeadMethod(RoutingContext context, Buffer content) {
        context.response().putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(content.length()));
        context.response().setStatusCode(HTTP_STATUS_OK).end();
    }

    private byte[] getClasspathResourceContent(String name) {
        String resourceName = GeneratedStaticResourcesRecorder.META_INF_RESOURCES + name;
        URL resource = getClassLoader().getResource(resourceName);
        if (resource == null) {
            LOG.warnf("The resource '%s' does not exist on classpath", resourceName);
            return null;
        }
        try {
            try (InputStream inputStream = resource.openStream()) {
                return inputStream.readAllBytes();
            }
        } catch (IOException e) {
            LOG.error("Error while reading file from Classpath for path " + resourceName, e);
            return null;
        }
    }

    private static String resolvePath(RoutingContext ctx) {
        return (ctx.mountPoint() == null) ? ctx.normalizedPath()
                : ctx.normalizedPath().substring(
                        // let's be extra careful here in case Vert.x normalizes the mount points at
                        // some point
                        ctx.mountPoint().endsWith("/") ? ctx.mountPoint().length() - 1 : ctx.mountPoint().length());
    }

    private static void beforeNextHandler(ClassLoader cl, RoutingContext ctx) {
        // make sure we don't lose the correct TCCL to Vert.x...
        Thread.currentThread().setContextClassLoader(cl);
        ctx.next();
    }

    private void compressIfNeeded(RoutingContext ctx, String path) {
        if (enableCompression && isCompressed(path)) {
            // VertxHttpRecorder is adding "Content-Encoding: identity" to all requests if compression is enabled.
            // Handlers can remove the "Content-Encoding: identity" header to enable compression.
            ctx.response().headers().remove(HttpHeaders.CONTENT_ENCODING);
        }
    }

    private boolean isCompressed(String path) {
        if (this.compressedMediaTypes.isEmpty()) {
            return false;
        }
        final String resourcePath = path.endsWith("/") ? path + this.indexPage : path;
        String contentType = MimeMapping.getMimeTypeForFilename(resourcePath);
        return contentType != null && this.compressedMediaTypes.contains(contentType);
    }

    private ClassLoader getClassLoader() {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        if (cl == null) {
            cl = getClass().getClassLoader();
        }
        if (cl == null) {
            cl = Object.class.getClassLoader();
        }
        return cl;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy