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

org.citrusframework.remote.CitrusRemoteApplication Maven / Gradle / Ivy

/*
 * Copyright 2006-2018 the original author or authors.
 *
 * 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 org.citrusframework.remote;

import io.netty.handler.codec.http.HttpResponseStatus;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.RequestBody;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.BodyHandler;
import org.citrusframework.Citrus;
import org.citrusframework.CitrusInstanceManager;
import org.citrusframework.CitrusInstanceStrategy;
import org.citrusframework.TestClass;
import org.citrusframework.main.CitrusAppConfiguration;
import org.citrusframework.main.TestRunConfiguration;
import org.citrusframework.remote.job.RunJob;
import org.citrusframework.remote.model.RemoteResult;
import org.citrusframework.remote.listener.RemoteTestListener;
import org.citrusframework.remote.transformer.JsonRequestTransformer;
import org.citrusframework.remote.transformer.JsonResponseTransformer;
import org.citrusframework.report.JUnitReporter;
import org.citrusframework.report.LoggingReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Stream;

/**
 * Remote application creates routes for this web application.
 *
 * @author Christoph Deppisch
 * @since 2..4
 */
public class CitrusRemoteApplication extends AbstractVerticle {

    /** Logger */
    private static final Logger logger = LoggerFactory.getLogger(CitrusRemoteApplication.class);

    /** Global url encoding */
    private static final String ENCODING = "UTF-8";
    /** Content types */
    private static final String APPLICATION_JSON = "application/json";
    private static final String APPLICATION_XML = "application/xml";
    private static final String APPLICATION_OCTET_STREAM = "application/octet-stream";

    /** Application configuration */
    private final CitrusRemoteConfiguration configuration;

    /** Single thread job scheduler */
    private Future> remoteResultFuture;

    /** Latest test reports */
    private final RemoteTestListener remoteTestListener =
            new RemoteTestListener();

    /** Router customizations */
    private final List> routerCustomizations;

    private final ExecutorService executorService = Executors.newCachedThreadPool();

    private final JsonRequestTransformer requestTransformer = new JsonRequestTransformer();
    private final JsonResponseTransformer responseTransformer = new JsonResponseTransformer();

    /**
     * Constructor with given application configuration and route customizations.
     * @param configuration
     * @param routerCustomizations
     */
    public CitrusRemoteApplication(
            CitrusRemoteConfiguration configuration,
            List> routerCustomizations) {
        this.configuration = configuration;
        this.routerCustomizations = Optional.ofNullable(routerCustomizations)
                .orElse(Collections.emptyList());
    }

    @Override
    public void start() {
        CitrusInstanceManager.mode(CitrusInstanceStrategy.SINGLETON);
        CitrusInstanceManager
                .addInstanceProcessor(citrus -> citrus.addTestListener(remoteTestListener));

        Router router = Router.router(getVertx());
        router.route().handler(BodyHandler.create());
        router.route().handler(ctx -> {
            logger.info("{} {}", ctx.request().method(), ctx.request().uri());
            ctx.next();
        });
        addHealthEndpoint(router);
        addFilesEndpoint(router);
        addResultsEndpoints(router);
        addRunEndpoints(router);
        addConfigEndpoints(router);
        routerCustomizations.forEach(customization -> customization.accept(router));

        getVertx().createHttpServer()
                .requestHandler(router)
                .listen(configuration.getPort())
                .onSuccess(unused ->
                        logger.info("Server started on port {}", configuration.getPort()));
    }

    private static void addHealthEndpoint(Router router) {
        router.get("/health")
                .handler(wrapThrowingHandler(ctx ->
                        ctx.response()
                                .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                                .end("{ \"status\": \"UP\" }")));
    }

    private static void addFilesEndpoint(Router router) {
        router.get("/files/:name")
                .handler(wrapThrowingHandler(ctx -> {
                    HttpServerResponse response = ctx.response();
                    String fileName = ctx.pathParam("name");
                    Path file = Path.of(fileName);
                    if (Files.isRegularFile(file)) {
                        response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_OCTET_STREAM)
                                .putHeader(
                                        HttpHeaders.CONTENT_DISPOSITION,
                                        "attachment; filename=\"" + file.getFileName() + "\"")
                                .sendFile(fileName);
                    } else {
                        response.setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end();
                    }
                }));
    }

    private void addResultsEndpoints(Router router) {
        router.get("/results")
                .produces(APPLICATION_JSON)
                .handler(wrapThrowingHandler(ctx -> {
                    long timeout = Optional.ofNullable(ctx.request().params().get("timeout"))
                            .map(Long::valueOf)
                            .orElse(10000L);

                    HttpServerResponse response = ctx.response();
                    if (remoteResultFuture != null) {
                        response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON);
                        remoteResultFuture.timeout(timeout, TimeUnit.MILLISECONDS)
                                .onSuccess(results ->
                                        response.end(responseTransformer.render(results)))
                                .onFailure(throwable -> response
                                        .setStatusCode(HttpResponseStatus.PARTIAL_CONTENT.code())
                                        .end(responseTransformer
                                                .render(remoteTestListener.toRemoteResults())));
                    } else {
                        final List results = remoteTestListener.toRemoteResults();
                        logger.info("results = {}", results.size());
                        response.end(responseTransformer.render(results));
                    }
                }));
        router.get("/results")
                .handler(ctx -> ctx.response()
                        .end(responseTransformer.render(remoteTestListener.generateTestReport())));
        router.get("/results/files")
                .handler(wrapThrowingHandler(ctx -> {
                    File junitReportsFolder = new File(getJUnitReportsFolder());

                    List result = Collections.emptyList();
                    if (junitReportsFolder.exists()) {
                        result = Optional.ofNullable(junitReportsFolder.list())
                                .stream()
                                .flatMap(Stream::of)
                                .toList();
                    }
                    ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                            .end(responseTransformer.render(result));
                }));
        router.get("/results/file/:name")
                .handler(wrapThrowingHandler(ctx -> {
                    HttpServerResponse response = ctx.response();
                    response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_XML);
                    String fileName = ctx.pathParam("name");
                    String reportsFolder = getJUnitReportsFolder();
                    Path testResultFile = Path.of(reportsFolder).resolve(fileName);

                    if (Files.exists(testResultFile)) {
                        response.sendFile(testResultFile.toString());
                    } else {
                        response.setStatusCode(HttpResponseStatus.NOT_FOUND.code())
                                .end("Failed to find test result file: %s".formatted(fileName));
                    }
                }));
        router.get("/results/suite")
                .handler(wrapThrowingHandler(ctx -> {
                    HttpServerResponse response = ctx.response();
                    response.putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON);
                    JUnitReporter jUnitReporter = new JUnitReporter();
                    Path suiteResultFile = Path.of(jUnitReporter.getReportDirectory())
                            .resolve(String.format(
                                    jUnitReporter.getReportFileNamePattern(),
                                    jUnitReporter.getSuiteName()));
                    if (Files.exists(suiteResultFile)) {
                        response.sendFile(suiteResultFile.toString());
                    } else {
                        response.setStatusCode(HttpResponseStatus.NOT_FOUND.code())
                                .end("Failed to find suite result file: %s"
                                        .formatted(suiteResultFile));
                    }
                }));
    }

    private void addRunEndpoints(Router router) {
        router.get("/run")
                .handler(wrapThrowingHandler(ctx ->
                        runTestsAsync(constructRunConfig(ctx.request().params()), ctx.response())));
        router.post("/run")
                .handler(wrapThrowingHandler(ctx ->
                        runTestsAsync(constructRunConfig(ctx.body()), ctx.response())));
        router.put("/run")
                .handler(wrapThrowingHandler(ctx -> {
                    remoteTestListener.reset();
                    remoteResultFuture = startTestsAsync(constructRunConfig(ctx.body()));
                    ctx.response().end("");
                }));
    }

    public static Handler wrapThrowingHandler(
            ThrowingHandler handler) {
        return ctx -> {
            try {
                handler.handle(ctx);
            } catch (Exception e) {
                ctx.response()
                        .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
                        .end(e.getMessage());
            }
        };
    }

    private TestRunConfiguration constructRunConfig(MultiMap queryParams)
            throws UnsupportedEncodingException {
        TestRunConfiguration runConfiguration = new TestRunConfiguration();
        if (queryParams.contains("engine")) {
            String engine = queryParams.get("engine");
            runConfiguration.setEngine(URLDecoder.decode(engine, ENCODING));
        } else {
            runConfiguration.setEngine(configuration.getEngine());
        }

        if (queryParams.contains("includes")) {
            String value = queryParams.get("includes");
            runConfiguration.setIncludes(URLDecoder.decode(value, ENCODING)
                    .split(","));
        }

        if (queryParams.contains("package")) {
            String value = queryParams.get("package");
            runConfiguration.setPackages(Collections.singletonList(
                    URLDecoder.decode(value, ENCODING)));
        }

        if (queryParams.contains("class")) {
            String value = queryParams.get("class");
            runConfiguration.setTestSources(Collections.singletonList(
                    TestClass.fromString(URLDecoder.decode(value, ENCODING))));
        }
        return runConfiguration;
    }

    private TestRunConfiguration constructRunConfig(RequestBody body) {
        return requestTransformer.read(body.asString(), TestRunConfiguration.class);
    }

    private void runTestsAsync(
            TestRunConfiguration runConfiguration,
            HttpServerResponse response) {
        Future
                .fromCompletionStage(CompletableFuture.supplyAsync(
                        new RunJob(configuration, runConfiguration, remoteTestListener),
                        executorService))
                .onSuccess(results ->
                        response.end(responseTransformer.render(results)))
                .onFailure(error -> response
                        .setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code())
                        .end(error.getMessage()));
    }

    private Future> startTestsAsync(TestRunConfiguration testRunConfiguration) {
        return Future.fromCompletionStage(CompletableFuture.supplyAsync(
                new RunJob(configuration, testRunConfiguration, remoteTestListener),
                executorService));
    }

    private void addConfigEndpoints(Router router) {
        router.get("/configuration")
                .handler(wrapThrowingHandler(ctx ->
                        ctx.response()
                                .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
                                .end(responseTransformer.render(configuration))));
        router.put("/configuration")
                .handler(wrapThrowingHandler(ctx ->
                        configuration.apply(requestTransformer.read(
                                ctx.body().asString(),
                                CitrusAppConfiguration.class))));
    }

    /**
     * Find reports folder based in unit testing framework present on classpath.
     * @return
     */
    private String getJUnitReportsFolder() {
        if (isPresent("org.testng.annotations.Test")) {
            return "test-output" + File.separator + "junitreports";
        } else if (isPresent("org.junit.Test")) {
            JUnitReporter jUnitReporter = new JUnitReporter();
            return jUnitReporter.getReportDirectory() +
                    File.separator +
                    jUnitReporter.getOutputDirectory();
        } else {
            return new LoggingReporter().getReportDirectory();
        }
    }

    @Override
    public void stop() {
        Optional citrus = CitrusInstanceManager.get();
        if (citrus.isPresent()) {
            logger.info("Closing Citrus and its application context");
            citrus.get().close();
        }
        getVertx().close();
    }

    // TODO: Check if this is equivalent to
    // https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/util/ClassUtils.java
    private boolean isPresent(String className) {
        try {
            Class.forName(className);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy