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

org.jgrapes.http.StaticContentDispatcher Maven / Gradle / Ivy

The newest version!
/*
 * JGrapes Event Driven Framework
 * Copyright (C) 2016-2018 Michael N. Lipp
 * 
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Affero General Public License as published by 
 * the Free Software Foundation; either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
 * for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along 
 * with this program; if not, see .
 */

package org.jgrapes.http;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.util.Arrays;
import java.util.Optional;
import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
import org.jdrupes.httpcodec.protocols.http.HttpField;
import org.jdrupes.httpcodec.protocols.http.HttpResponse;
import org.jdrupes.httpcodec.types.Converters;
import org.jdrupes.httpcodec.types.MediaType;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Component;
import org.jgrapes.http.ResponseCreationSupport.MaxAgeCalculator;
import org.jgrapes.http.annotation.RequestHandler;
import org.jgrapes.http.events.Request;
import org.jgrapes.http.events.Response;
import org.jgrapes.io.IOSubchannel;
import org.jgrapes.io.events.StreamFile;

/**
 * A dispatcher for requests for static content, usually files.
 */
public class StaticContentDispatcher extends Component {

    private ResourcePattern resourcePattern;
    private URI contentRoot;
    private Path contentDirectory;
    private MaxAgeCalculator maxAgeCalculator
        = (request, mediaType) -> 365 * 24 * 3600;

    /**
     * Creates new dispatcher that tries to fulfill requests matching 
     * the given resource pattern from the given content root.
     * 
     * An attempt is made to convert the content root to a {@link Path}
     * in a {@link FileSystem}. If this fails, the content root is
     * used as a URL against which requests are resolved and data
     * is obtained by open an input stream from the resulting URL.
     * In the latter case modification times aren't available. 
     * 
     * @param componentChannel this component's channel
     * @param resourcePattern the pattern that requests must match 
     * in order to be handled by this component 
     * (see {@link ResourcePattern})
     * @param contentRoot the location with content to serve 
     * @see Component#Component(Channel)
     */
    public StaticContentDispatcher(Channel componentChannel,
            String resourcePattern, URI contentRoot) {
        super(componentChannel);
        try {
            this.resourcePattern = new ResourcePattern(resourcePattern);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        }
        try {
            this.contentDirectory = Paths.get(contentRoot);
        } catch (FileSystemNotFoundException e) {
            this.contentRoot = contentRoot;
        }
        RequestHandler.Evaluator.add(this, "onGet", resourcePattern);
    }

    /**
     * Creates a new component base with its channel set to
     * itself.
     * 
     * @param resourcePattern the pattern that requests must match with to 
     * be handled by this component 
     * (see {@link ResourcePattern#matches(String, java.net.URI)})
     * @param contentRoot the location with content to serve 
     * @see Component#Component()
     */
    public StaticContentDispatcher(String resourcePattern, URI contentRoot) {
        this(Channel.SELF, resourcePattern, contentRoot);
    }

    /**
     * @return the maxAgeCalculator
     */
    public MaxAgeCalculator maxAgeCalculator() {
        return maxAgeCalculator;
    }

    /**
     * Sets the {@link MaxAgeCalculator} for generating the `Cache-Control` 
     * (`max-age`) header of the response. The default max age calculator 
     * used simply returns a max age of one year, since this component
     * is intended to serve static content.
     * 
     * @param maxAgeCalculator the maxAgeCalculator to set
     */
    public void setMaxAgeCalculator(MaxAgeCalculator maxAgeCalculator) {
        this.maxAgeCalculator = maxAgeCalculator;
    }

    /**
     * Handles a `GET` request.
     *
     * @param event the event
     * @param channel the channel
     * @throws ParseException the parse exception
     * @throws IOException Signals that an I/O exception has occurred.
     */
    @RequestHandler(dynamic = true)
    public void onGet(Request.In.Get event, IOSubchannel channel)
            throws ParseException, IOException {
        if (event.fulfilled()) {
            return;
        }
        int prefixSegs = resourcePattern.matches(event.requestUri());
        if (prefixSegs < 0) {
            return;
        }
        if (contentDirectory == null) {
            getFromUri(event, channel, prefixSegs);
        } else {
            getFromFileSystem(event, channel, prefixSegs);
        }
    }

    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
    private boolean getFromFileSystem(Request.In.Get event,
            IOSubchannel channel, int prefixSegs)
            throws IOException, ParseException {
        // Final wrapper for usage in closure
        final Path[] assembly = { contentDirectory };
        Arrays.stream(event.requestUri().getPath().split("/"))
            .skip(prefixSegs + 1)
            .forEach(seg -> assembly[0] = assembly[0].resolve(seg));
        Path resourcePath = assembly[0];
        if (Files.isDirectory(resourcePath)) {
            Path indexPath = resourcePath.resolve("index.html");
            if (Files.isReadable(indexPath)) {
                resourcePath = indexPath;
            } else {
                return false;
            }
        }
        if (!Files.isReadable(resourcePath)) {
            return false;
        }

        // Get content type
        HttpResponse response = event.httpRequest().response().get();
        MediaType mediaType = HttpResponse.contentType(resourcePath.toUri());

        // Derive max-age
        ResponseCreationSupport.setMaxAge(
            response, maxAgeCalculator.maxAge(event.httpRequest(), mediaType));

        // Check if sending is really required.
        Instant lastModified = Files.getLastModifiedTime(resourcePath)
            .toInstant().with(ChronoField.NANO_OF_SECOND, 0);
        Optional modifiedSince = event.httpRequest()
            .findValue(HttpField.IF_MODIFIED_SINCE, Converters.DATE_TIME);
        event.setResult(true);
        event.stop();
        if (modifiedSince.isPresent()
            && !lastModified.isAfter(modifiedSince.get())) {
            response.setStatus(HttpStatus.NOT_MODIFIED);
            response.setField(HttpField.LAST_MODIFIED, lastModified);
            channel.respond(new Response(response));
        } else {
            response.setContentType(mediaType);
            response.setStatus(HttpStatus.OK);
            response.setField(HttpField.LAST_MODIFIED, lastModified);
            channel.respond(new Response(response));
            fire(new StreamFile(resourcePath, StandardOpenOption.READ),
                channel);
        }
        return true;
    }

    private boolean getFromUri(Request.In.Get event, IOSubchannel channel,
            int prefixSegs) throws ParseException {
        return ResponseCreationSupport.sendStaticContent(
            event, channel, path -> {
                try {
                    return contentRoot.resolve(
                        ResourcePattern.removeSegments(
                            path, prefixSegs + 1))
                        .toURL();
                } catch (MalformedURLException e) {
                    throw new IllegalArgumentException(e);
                }
            }, maxAgeCalculator);
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy