001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.tools;
017
018import static java.util.Optional.ofNullable;
019import static java.util.concurrent.TimeUnit.MINUTES;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Scanner;
024import java.util.concurrent.CountDownLatch;
025import java.util.concurrent.atomic.AtomicReference;
026import java.util.function.Consumer;
027import java.util.stream.Stream;
028
029import org.apache.catalina.core.StandardServer;
030import org.apache.catalina.webresources.StandardRoot;
031import org.apache.meecrowave.Meecrowave;
032import org.apache.meecrowave.runner.Cli;
033
034public class WebServer implements Runnable {
035
036    private final Collection<String> serverArguments;
037
038    private final Integer port;
039
040    private final String componentGav;
041
042    private final Log log;
043
044    private final Collection<Consumer<Meecrowave.Builder>> onOpen = new ArrayList<>();
045
046    public WebServer(final Collection<String> serverArguments, final Integer port, final Object log, final String gav) {
047        this.serverArguments = serverArguments;
048        this.port = port;
049        try {
050            this.log = Log.class.isInstance(log) ? Log.class.cast(log) : new ReflectiveLog(log);
051        } catch (final NoSuchMethodException e) {
052            throw new IllegalArgumentException(e);
053        }
054        this.componentGav = gav;
055    }
056
057    public WebServer onOpen(final Consumer<Meecrowave.Builder> task) {
058        onOpen.add(task);
059        return this;
060    }
061
062    public WebServer openBrowserWhenReady() {
063        return onOpen(builder -> Browser.open("http://localhost:" + builder.getHttpPort(), log));
064    }
065
066    @Override
067    public void run() {
068        final String originalCompSystProp =
069                setSystemProperty("talend.component.server.component.coordinates", componentGav);
070        final String skipClasspathSystProp = setSystemProperty("component.manager.classpath.skip", "true");
071        final String skipCallersSystProp = setSystemProperty("component.manager.callers.skip", "true");
072        final AtomicReference<Meecrowave> ref = new AtomicReference<>();
073        try {
074            final CountDownLatch latch = new CountDownLatch(1);
075            new Thread(() -> {
076                try (final Meecrowave meecrowave = new Meecrowave(Cli.create(buildArgs()))) {
077                    meecrowave.start().deployClasspath(new Meecrowave.DeploymentMeta("", null, stdCtx -> {
078                        stdCtx.setResources(new StandardRoot() {
079
080                            @Override
081                            protected void registerURLStreamHandlerFactory() {
082                                // no-op - gradle supports to reuse the same JVM so it would be broken
083                            }
084                        });
085                    }, null));
086
087                    ref.set(meecrowave);
088                    latch.countDown();
089                    onOpen.forEach(it -> it.accept(meecrowave.getConfiguration()));
090                    meecrowave.getTomcat().getServer().await();
091                } catch (final RuntimeException re) {
092                    latch.countDown();
093                    log.error(re.getMessage());
094                    throw re;
095                }
096            }, getClass().getName() + '_' + findPort()).start();
097            try {
098                latch.await(2, MINUTES);
099            } catch (final InterruptedException e) {
100                Thread.currentThread().interrupt();
101                return;
102            }
103            final boolean batch = Boolean.parseBoolean(System.getProperty("talend.web.batch", "false"));
104            final int timeout = Integer.parseInt(System.getProperty("talend.web.batch.timeout", "2"));
105            if (!batch) {
106                log.info("\n\n  You can now access the UI at http://localhost:" + port + "\n\n");
107                final Scanner scanner = new Scanner(System.in);
108                do {
109                    log.info("Enter 'exit' to quit");
110                } while (!shouldQuit(scanner.nextLine()));
111            } else {
112                log.info(String.format(
113                        "Server running at http://localhost:%d in non-interactive mode. Will shutdown in %d minutes.",
114                        port, timeout));
115                try {
116                    Thread.currentThread().sleep(MINUTES.toMillis(timeout));
117                    log.info("Shutting down.");
118                } catch (final InterruptedException e) {
119                    Thread.currentThread().interrupt();
120                    return;
121                }
122            }
123        } finally {
124            reset("talend.component.server.component.coordinates", originalCompSystProp);
125            reset("component.manager.classpath.skip", skipClasspathSystProp);
126            reset("component.manager.callers.skip", skipCallersSystProp);
127            ofNullable(ref.get()).ifPresent(mw -> StandardServer.class.cast(mw.getTomcat().getServer()).stopAwait());
128        }
129    }
130
131    private String setSystemProperty(final String key, final String value) {
132        final String old = System.getProperty(key);
133        System.setProperty(key, value);
134        return old;
135    }
136
137    private void reset(final String key, final String value) {
138        if (value == null) {
139            System.clearProperty(key);
140        } else {
141            System.setProperty(key, value);
142        }
143    }
144
145    private boolean shouldQuit(final String value) {
146        return Stream.of("exit", "quit", "X").anyMatch(v -> v.equalsIgnoreCase(value));
147    }
148
149    private String[] buildArgs() {
150        final Collection<String> args = new ArrayList<>();
151        if (serverArguments != null) {
152            args.addAll(serverArguments);
153        }
154        if (serverArguments != null && serverArguments.contains("--http")) {
155            if (port != null) {
156                log.info("port configuration ignored since serverArguments already defines it");
157            }
158        } else {
159            args.add("--http");
160            args.add(findPort());
161        }
162        if (!args.contains("--scanning-exclude")) { // nicer default logging
163            args.add("--scanning-exclude");
164            args
165                    .add("animal-sniffer-annotations,checker-qual,component-form,component-server-model,"
166                            + "error_prone_annotations,failureaccess,freemarker,j2objc-annotations,jib-core,"
167                            + "jsr305,listenablefuture,talend-component-maven-plugin,"
168                            + "avro,beam,paranamer,xz,component-api,component-spi,component-runtime-impl,"
169                            + "component-runtime-manager,component-runtime-design-extension,container-core,"
170                            + "component-runtime-beam");
171        }
172        if (!args.contains("--use-shutdown-hook")) {
173            args.add("--use-shutdown-hook");
174            args.add("false");
175        }
176        return args.toArray(new String[0]);
177    }
178
179    private String findPort() {
180        return port == null ? "8080" : Integer.toString(port);
181    }
182}