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

com.github.mcollovati.vertx.vaadin.VertxVaadin Maven / Gradle / Ivy

There is a newer version: 24.5.0-alpha1
Show newest version
/*
 * The MIT License
 * Copyright © 2016-2020 Marco Collovati ([email protected])
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.mcollovati.vertx.vaadin;

import javax.servlet.ServletContext;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.github.mcollovati.vertx.Sync;
import com.github.mcollovati.vertx.http.HttpReverseProxy;
import com.github.mcollovati.vertx.support.StartupContext;
import com.github.mcollovati.vertx.vaadin.sockjs.communication.SockJSPushConnection;
import com.github.mcollovati.vertx.vaadin.sockjs.communication.SockJSPushHandler;
import com.github.mcollovati.vertx.web.sstore.ExtendedLocalSessionStore;
import com.github.mcollovati.vertx.web.sstore.ExtendedSessionStore;
import com.github.mcollovati.vertx.web.sstore.NearCacheSessionStore;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.server.DevModeHandler;
import com.vaadin.flow.server.ServiceException;
import com.vaadin.flow.server.WrappedSession;
import com.vaadin.flow.shared.ApplicationConstants;
import com.vaadin.flow.shared.Registration;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.VertxException;
import io.vertx.core.eventbus.Message;
import io.vertx.core.eventbus.MessageConsumer;
import io.vertx.core.eventbus.MessageProducer;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.ext.web.handler.CookieHandler;
import io.vertx.ext.web.handler.SessionHandler;
import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
import org.slf4j.LoggerFactory;

public class VertxVaadin {

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(VertxVaadin.class);
    private static final String VAADIN_SESSION_EXPIRED_ADDRESS = "vaadin.session.expired";
    private static final String VERSION;

    private final VertxVaadinService service;
    private final StartupContext startupContext;
    private final VaadinOptions config;
    private final Vertx vertx;
    private final Router router;
    private final ExtendedSessionStore sessionStore;
    WebJars webJars;

    static {
        String version = "0.0.0";
        Properties properties = new Properties();
        try {
            properties.load(VertxVaadin.class.getResourceAsStream("version.properties"));
            version = properties.getProperty("vertx-vaadin.version");
        } catch (Exception e) {
            logger.warn("Unable to determine VertxVaadin version");
        }
        VERSION = version;
    }


    private VertxVaadin(Vertx vertx, Optional sessionStore, StartupContext startupContext) {
        this.vertx = Objects.requireNonNull(vertx);
        this.startupContext = Objects.requireNonNull(startupContext);
        this.config = startupContext.vaadinOptions();

        this.service = createVaadinService();

        if (config.supportsSockJS()) {
            logger.trace("Configuring SockJS Push connection");
            this.service.addUIInitListener(event ->
                event.getUI().getInternals().setPushConnection(new SockJSPushConnection(event.getUI()))
            );
        }

        logger.trace("Setup WebJar server");
        this.webJars = new WebJars(service.getDeploymentConfiguration());
        try {
            service.init();
        } catch (Exception ex) {
            throw new VertxException("Cannot initialize Vaadin service", ex);
        }

        this.sessionStore = withSessionExpirationHandler(
            this.service, sessionStore.orElseGet(this::createSessionStore)
        );
        configureSessionStore();
        this.router = initRouter();
    }

    protected VertxVaadin(Vertx vertx, ExtendedSessionStore sessionStore, StartupContext startupContext) {
        this(vertx, Optional.of(sessionStore), startupContext);
    }

    protected VertxVaadin(Vertx vertx, StartupContext startupContext) {
        this(vertx, Optional.empty(), startupContext);
    }

    private void configureSessionStore() {
        final Registration sessionInitListenerReg = this.service.addSessionInitListener(event -> {
            MessageConsumer consumer = sessionExpiredHandler(vertx, msg ->
                Optional.of(event.getSession().getSession())
                    .filter(session -> msg.body().equals(session.getId()))
                    .ifPresent(WrappedSession::invalidate));
            AtomicReference sessionDestroyListenerUnregister = new AtomicReference<>();
            sessionDestroyListenerUnregister.set(
                event.getService().addSessionDestroyListener(ev2 -> {
                    consumer.unregister();
                    sessionDestroyListenerUnregister.get().remove();
                })
            );

        });
        this.service.addServiceDestroyListener(event -> sessionInitListenerReg.remove());
    }

    public Router router() {
        return router;
    }

    public final Vertx vertx() {
        return vertx;
    }

    public final VertxVaadinService vaadinService() {
        return service;
    }

    public String serviceName() {
        return config.serviceName().orElseGet(() -> getClass().getName() + ".service");
    }


    protected final VaadinOptions config() {
        return config;
    }

    ServletContext servletContext() {
        return startupContext.servletContext();
    }

    protected void serviceInitialized(Router router) {
        // empty by default
    }

    protected VertxVaadinService createVaadinService() {
        VertxVaadinService service = new VertxVaadinService(this, createDeploymentConfiguration());
        return service;
    }

    protected ExtendedSessionStore createSessionStore() {
        if (vertx.isClustered()) {
            return NearCacheSessionStore.create(vertx);
        }
        return ExtendedLocalSessionStore.create(vertx);
    }

    private Router initRouter() {
        logger.debug("Initializing router");
        String sessionCookieName = sessionCookieName();
        SessionHandler sessionHandler = SessionHandler.create(sessionStore)
            .setSessionTimeout(config().sessionTimeout())
            .setSessionCookieName(sessionCookieName)
            .setNagHttps(false)
            .setCookieHttpOnlyFlag(true);

        Router vaadinRouter = Router.router(vertx);
        // Redirect mountPoint to mountPoint/
        vaadinRouter.routeWithRegex("^$").handler(ctx -> ctx.response()
            .putHeader(HttpHeaders.LOCATION, ctx.request().uri() + "/")
            .setStatusCode(302).end()
        );

        vaadinRouter.route().handler(BodyHandler.create());

        // Disable SessionHandler for /VAADIN/ static resources
        vaadinRouter.routeWithRegex("^(?!/(VAADIN(?!/dynamic)|frontend|frontend-es6|webjars|webroot)/).*$").handler(sessionHandler);

        // Forward vaadinPush javascript to sockjs implementation
        vaadinRouter.routeWithRegex("/VAADIN/static/push/vaadinPush(?-min)?\\.js(?\\.gz)?")
            .handler(ctx -> ctx.reroute(
                String.format("%s/VAADIN/static/push/vaadinPushSockJS%s.js%s", ctx.mountPoint(),
                    Objects.toString(ctx.request().getParam("min"), ""),
                    Objects.toString(ctx.request().getParam("compressed"), "")
                )
            ));


        if (DevModeHandler.getDevModeHandler() != null) {
            logger.info("Starting DevModeHandler proxy");
            HttpReverseProxy proxy = HttpReverseProxy.create(vertx, DevModeHandler.getDevModeHandler().getPort());
            vaadinRouter.routeWithRegex(".+\\.js$").blockingHandler(proxy::forward);
        }

        //
        //vaadinRouter.route("/VAADIN/dynamic/*").handler(this::handleVaadinRequest);
        vaadinRouter.route("/VAADIN/static/client/*")
            //.handler(StaticHandler.create("META-INF/resources/VAADIN/static/client", getClass().getClassLoader()));
            .handler(StaticHandler.create("META-INF/resources/VAADIN/static/client"));
        vaadinRouter.route("/VAADIN/build/*").handler(StaticHandler.create("META-INF/VAADIN/build"));
        vaadinRouter.route("/VAADIN/static/*").handler(StaticHandler.create("VAADIN/static"));
        vaadinRouter.route("/VAADIN/static/*").handler(StaticHandler.create("META-INF/resources/VAADIN/static"));
        vaadinRouter.routeWithRegex("/VAADIN(?!/dynamic)/.*").handler(StaticHandler.create("VAADIN"));
        vaadinRouter.route("/webroot/*").handler(StaticHandler.create("webroot"));
        vaadinRouter.route("/webjars/*").handler(StaticHandler.create("webroot"));
        vaadinRouter.route("/webjars/*").handler(StaticHandler.create("META-INF/resources/webjars"));
        vaadinRouter.routeWithRegex("/frontend/bower_components/(?.*)").handler(ctx -> {
                logger.trace("Rerouting bower component to {}/webjars/{}",
                    ctx.mountPoint(), Objects.toString(ctx.request().getParam("webjar"), "")
                );
                ctx.reroute(String.format("%s/webjars/%s",
                    ctx.mountPoint(), Objects.toString(ctx.request().getParam("webjar"), "")
                ));
            }
        );

        logger.trace("Setup fronted routes");
        vaadinRouter.route("/frontend/*").handler(StaticHandler.create("frontend"));
        vaadinRouter.route("/frontend/*").handler(StaticHandler.create("webroot"));
        vaadinRouter.route("/frontend/*").handler(StaticHandler.create("META-INF/resources/frontend"));
        vaadinRouter.route("/frontend-es6/*").handler(StaticHandler.create("frontend-es6"));
        vaadinRouter.route("/frontend-es6/*").handler(StaticHandler.create("META-INF/resources/frontend-es6"));

        initSockJS(vaadinRouter, sessionHandler);

        vaadinRouter.route("/*").handler(StaticHandler.create("META-INF/resources"));
        vaadinRouter.route("/*").handler(this::handleVaadinRequest);

        serviceInitialized(vaadinRouter);
        return vaadinRouter;
    }

    private void handleVaadinRequest(RoutingContext routingContext) {
        VertxVaadinRequest request = new VertxVaadinRequest(service, routingContext);
        VertxVaadinResponse response = new VertxVaadinResponse(service, routingContext);

        try {
            logger.trace("Handling Vaadin request: {}", routingContext.request().uri());
            service.handleRequest(request, response);
            response.end();
        } catch (ServiceException ex) {
            logger.error("Error processing request {}" + routingContext.request().uri(), ex);
            routingContext.fail(ex);
        }
    }

    private void initSockJS(Router vaadinRouter, SessionHandler sessionHandler) {
        if (this.config.supportsSockJS()) {
            SockJSHandlerOptions options = new SockJSHandlerOptions()
                .setSessionTimeout(config().sessionTimeout())
                .setHeartbeatInterval(service.getDeploymentConfiguration().getHeartbeatInterval() * 1000);
            SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);

            SockJSPushHandler pushHandler = new SockJSPushHandler(service, sessionHandler, sockJSHandler);

            String pushPath = config.pushURL().replaceFirst("/$", "") + "/*";
            logger.debug("Setup PUSH communication on {}", pushPath);

            vaadinRouter.route(pushPath).handler(rc -> {
                if (ApplicationConstants.REQUEST_TYPE_PUSH.equals(rc.request().getParam(ApplicationConstants.REQUEST_TYPE_PARAMETER))) {
                    pushHandler.handle(rc);
                } else {
                    rc.next();
                }
            });
        } else {
            logger.info("PUSH communication over SockJS is disabled");
        }
    }


    private String sessionCookieName() {
        return config().sessionCookieName();
    }

    private DeploymentConfiguration createDeploymentConfiguration() {
        return DeploymentConfigurationFactory.createDeploymentConfiguration(getClass(), config());
    }


    public static VertxVaadin create(Vertx vertx, ExtendedSessionStore sessionStore, StartupContext startupContext) {
        return new VertxVaadin(vertx, sessionStore, startupContext);
    }

    public static VertxVaadin create(Vertx vertx, JsonObject config) {
        StartupContext startupContext = Sync.await(completer -> StartupContext.of(vertx, new VaadinOptions(config)).setHandler(completer));
        return create(vertx, startupContext);
    }

    public static VertxVaadin create(Vertx vertx, StartupContext config) {
        return new VertxVaadin(vertx, config);
    }

    private static ExtendedSessionStore withSessionExpirationHandler(
        VertxVaadinService service, ExtendedSessionStore store
    ) {
        MessageProducer sessionExpiredProducer = sessionExpiredProducer(service);
        store.expirationHandler(res -> {
            if (res.succeeded()) {
                sessionExpiredProducer.write(res.result());
            } else {
                res.cause().printStackTrace();
            }
        });
        return store;
    }

    private static MessageProducer sessionExpiredProducer(VertxVaadinService vaadinService) {
        return vaadinService.getVertx().eventBus().sender(VAADIN_SESSION_EXPIRED_ADDRESS);
    }

    public static MessageConsumer sessionExpiredHandler(Vertx vertx, Handler> handler) {
        return vertx.eventBus().consumer(VAADIN_SESSION_EXPIRED_ADDRESS, handler);
    }

    public static String getVersion() {
        return VERSION;
    }

    static final class WebJars {

        static final String WEBJARS_RESOURCES_PREFIX = "META-INF/resources/webjars/";
        private final String webjarsLocation;
        private final Pattern urlPattern;

        private WebJars(DeploymentConfiguration deploymentConfiguration) {
            String frontendPrefix = deploymentConfiguration
                .getDevelopmentFrontendPrefix();
            if (!frontendPrefix.endsWith("/")) {
                throw new IllegalArgumentException(
                    "Frontend prefix must end with a /. Got \"" + frontendPrefix
                        + "\"");
            }
            if (!frontendPrefix
                .startsWith(ApplicationConstants.CONTEXT_PROTOCOL_PREFIX)) {
                throw new IllegalArgumentException(
                    "Cannot host WebJars for a fronted prefix that isn't relative to 'context://'. Current frontend prefix: "
                        + frontendPrefix);
            }

            webjarsLocation = "/"
                + frontendPrefix.substring(
                ApplicationConstants.CONTEXT_PROTOCOL_PREFIX.length());


            urlPattern = Pattern.compile("^((/\\.)?(/\\.\\.)*)" + webjarsLocation + "(bower_components/)?(?.*)");
        }


        /**
         * Gets web jar resource path if it exists.
         *
         * @param filePathInContext servlet context path for file
         * @return an optional web jar resource path, or an empty optional if the
         * resource is not web jar resource
         */
        public Optional getWebJarResourcePath(String filePathInContext) {
            String webJarPath = null;

            Matcher matcher = urlPattern.matcher(filePathInContext);
            // If we don't find anything then we don't have the prefix at all.
            if (matcher.find()) {
                webJarPath = WEBJARS_RESOURCES_PREFIX + matcher.group("webjar");
            }
            return Optional.ofNullable(webJarPath);
        }


    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy