com.github.robozonky.installer.RoboZonkyInstallerListener.java Source code

Java tutorial

Introduction

Here is the source code for com.github.robozonky.installer.RoboZonkyInstallerListener.java

Source

/*
 * Copyright 2019 The RoboZonky Project
 *
 * 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 com.github.robozonky.installer;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
import java.util.stream.Stream;

import com.github.robozonky.cli.Feature;
import com.github.robozonky.cli.GoogleCredentialsFeature;
import com.github.robozonky.cli.SetupFailedException;
import com.github.robozonky.cli.ZonkoidPasswordFeature;
import com.github.robozonky.cli.ZonkyPasswordFeature;
import com.github.robozonky.installer.scripts.RunScriptGenerator;
import com.github.robozonky.installer.scripts.ServiceGenerator;
import com.github.robozonky.internal.api.Settings;
import com.izforge.izpack.api.data.InstallData;
import com.izforge.izpack.api.data.Pack;
import com.izforge.izpack.api.event.AbstractInstallerListener;
import com.izforge.izpack.api.event.ProgressListener;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import static com.github.robozonky.integrations.stonky.Properties.GOOGLE_CALLBACK_HOST;
import static com.github.robozonky.integrations.stonky.Properties.GOOGLE_CALLBACK_PORT;
import static com.github.robozonky.integrations.stonky.Properties.GOOGLE_LOCAL_FOLDER;

public final class RoboZonkyInstallerListener extends AbstractInstallerListener {

    static final char[] KEYSTORE_PASSWORD = UUID.randomUUID().toString().toCharArray();
    private static final Logger LOGGER = LogManager.getLogger(RoboZonkyInstallerListener.class);
    static File INSTALL_PATH, DIST_PATH, KEYSTORE_FILE, JMX_PROPERTIES_FILE, EMAIL_CONFIG_FILE, SETTINGS_FILE,
            CLI_CONFIG_FILE, LOG4J2_CONFIG_FILE;
    private static InstallData DATA;
    private RoboZonkyInstallerListener.OS operatingSystem = RoboZonkyInstallerListener.OS.OTHER;

    public RoboZonkyInstallerListener() {
        if (SystemUtils.IS_OS_LINUX) {
            operatingSystem = RoboZonkyInstallerListener.OS.LINUX;
        } else if (SystemUtils.IS_OS_WINDOWS) {
            operatingSystem = RoboZonkyInstallerListener.OS.WINDOWS;
        }
    }

    /**
     * Testing OS-specific behavior was proving very difficult, this constructor takes all of that pain away.
     * @param os Fake operating system used for testing.
     */
    RoboZonkyInstallerListener(final RoboZonkyInstallerListener.OS os) {
        operatingSystem = os;
    }

    /**
     * This is a dirty ugly hack to workaround a bug in IZPack's Picocontainer. If we had the proper constructor to
     * accept {@link InstallData}, Picocontainer would have thrown some weird exception.
     * <p>
     * Therefore we share the data this way - through the last panel before the actual installation starts.
     * <p>
     * See more at https://izpack.atlassian.net/browse/IZPACK-1403.
     * @param data Installer data to store.
     */
    static void setInstallData(final InstallData data) {
        RoboZonkyInstallerListener.DATA = data;
        INSTALL_PATH = new File(Variables.INSTALL_PATH.getValue(DATA));
        DIST_PATH = new File(INSTALL_PATH, "Dist/");
        KEYSTORE_FILE = new File(INSTALL_PATH, "robozonky.keystore");
        JMX_PROPERTIES_FILE = new File(INSTALL_PATH, "management.properties");
        EMAIL_CONFIG_FILE = new File(INSTALL_PATH, "robozonky-notifications.cfg");
        SETTINGS_FILE = new File(INSTALL_PATH, "robozonky.properties");
        CLI_CONFIG_FILE = new File(INSTALL_PATH, "robozonky.cli");
        LOG4J2_CONFIG_FILE = new File(INSTALL_PATH, "log4j2.xml");
    }

    static void resetInstallData() {
        RoboZonkyInstallerListener.DATA = null;
        INSTALL_PATH = null;
        DIST_PATH = null;
        KEYSTORE_FILE = null;
        JMX_PROPERTIES_FILE = null;
        EMAIL_CONFIG_FILE = null;
        SETTINGS_FILE = null;
        CLI_CONFIG_FILE = null;
        LOG4J2_CONFIG_FILE = null;
    }

    private static void primeKeyStore(final char... keystorePassword) throws SetupFailedException, IOException {
        final String username = Variables.ZONKY_USERNAME.getValue(DATA);
        final char[] password = Variables.ZONKY_PASSWORD.getValue(DATA).toCharArray();
        Files.deleteIfExists(KEYSTORE_FILE.toPath()); // re-install into the same directory otherwise fails
        final Feature f = new ZonkyPasswordFeature(KEYSTORE_FILE, keystorePassword, username, password);
        f.setup();
    }

    private static void prepareZonkoid(final char... keystorePassword) throws SetupFailedException {
        final Feature f = new ZonkoidPasswordFeature(KEYSTORE_FILE, keystorePassword,
                Variables.ZONKOID_TOKEN.getValue(DATA).toCharArray());
        f.setup();
    }

    private static CommandLinePart prepareCore(final char... keystorePassword)
            throws SetupFailedException, IOException {
        final String zonkoidId = "zonkoid";
        final CommandLinePart cli = new CommandLinePart().setOption("-p", String.valueOf(keystorePassword));
        cli.setOption("-g", KEYSTORE_FILE.getAbsolutePath());
        if (Boolean.valueOf(Variables.IS_DRY_RUN.getValue(DATA))) {
            cli.setOption("-d");
            cli.setJvmArgument("Xmx128m"); // more memory for the JFR recording
            cli.setJvmArgument("XX:StartFlightRecording=disk=true,dumponexit=true,maxage=1d,path-to-gc-roots=true");
        } else {
            cli.setJvmArgument("Xmx64m");
        }
        primeKeyStore(keystorePassword);
        final boolean isZonkoidEnabled = Boolean.parseBoolean(Variables.IS_ZONKOID_ENABLED.getValue(DATA));
        if (isZonkoidEnabled) {
            prepareZonkoid(keystorePassword);
            cli.setOption("-x", zonkoidId);
        }
        return cli;
    }

    private static File assembleCliFile(final CommandLinePart... source) throws IOException {
        // assemble the CLI
        final CommandLinePart cli = new CommandLinePart();
        Stream.of(source).forEach(c -> Util.copyOptions(c, cli));
        // store it to a file
        cli.storeOptions(CLI_CONFIG_FILE);
        return CLI_CONFIG_FILE.getAbsoluteFile();
    }

    static CommandLinePart prepareStrategy() {
        final String content = Variables.STRATEGY_SOURCE.getValue(DATA);
        if (Objects.equals(Variables.STRATEGY_TYPE.getValue(DATA), "file")) {
            final File strategyFile = new File(INSTALL_PATH, "robozonky-strategy.cfg");
            try {
                Util.copyFile(new File(content), strategyFile);
                return new CommandLinePart().setOption("-s", strategyFile.getAbsolutePath());
            } catch (final IOException ex) {
                throw new IllegalStateException("Failed copying strategy file.", ex);
            }
        } else {
            try {
                return new CommandLinePart().setOption("-s", new URL(content).toExternalForm());
            } catch (final MalformedURLException ex) {
                throw new IllegalStateException("Wrong strategy URL.", ex);
            }
        }
    }

    private static URL getEmailConfiguration() throws IOException {
        final String type = Variables.EMAIL_CONFIGURATION_TYPE.getValue(DATA);
        LOGGER.debug("Configuring notifications: {}", type);
        switch (type) {
        case "file":
            final File f = new File(Variables.EMAIL_CONFIGURATION_SOURCE.getValue(DATA));
            Util.copyFile(f, EMAIL_CONFIG_FILE);
            return EMAIL_CONFIG_FILE.toURI().toURL();
        case "url":
            return new URL(Variables.EMAIL_CONFIGURATION_SOURCE.getValue(DATA));
        default:
            final Properties props = Util.configureEmailNotifications(DATA);
            Util.writeOutProperties(props, EMAIL_CONFIG_FILE);
            return EMAIL_CONFIG_FILE.toURI().toURL();
        }
    }

    static CommandLinePart prepareEmailConfiguration() {
        if (!Boolean.valueOf(Variables.IS_EMAIL_ENABLED.getValue(DATA))) {
            return new CommandLinePart();
        }
        try {
            final URL url = getEmailConfiguration();
            return new CommandLinePart().setOption("-i", url.toExternalForm());
        } catch (final Exception ex) {
            throw new IllegalStateException("Failed writing e-mail configuration.", ex);
        }
    }

    static CommandLinePart prepareJmx() {
        final boolean isJmxEnabled = Boolean.parseBoolean(Variables.IS_JMX_ENABLED.getValue(DATA));
        final CommandLinePart clp = new CommandLinePart()
                .setProperty("com.sun.management.jmxremote", isJmxEnabled ? "true" : "false")
                // the buffer is effectively a memory leak; we'll reduce its size from 1000 to 10
                .setProperty("jmx.remote.x.notification.buffer.size", "10");
        if (!isJmxEnabled) { // ignore JMX
            return clp;
        }
        // write JMX properties file
        final Properties props = new Properties();
        props.setProperty("com.sun.management.jmxremote.authenticate",
                Variables.IS_JMX_SECURITY_ENABLED.getValue(DATA));
        props.setProperty("com.sun.management.jmxremote.ssl", "false");
        final String port = Variables.JMX_PORT.getValue(DATA);
        props.setProperty("com.sun.management.jmxremote.rmi.port", port);
        props.setProperty("com.sun.management.jmxremote.port", port);
        try {
            Util.writeOutProperties(props, JMX_PROPERTIES_FILE);
        } catch (final Exception ex) {
            throw new IllegalStateException("Failed writing JMX configuration.", ex);
        }
        // configure JMX to read the props file
        return clp.setProperty("com.sun.management.config.file", JMX_PROPERTIES_FILE.getAbsolutePath())
                .setProperty("java.rmi.server.hostname", Variables.JMX_HOSTNAME.getValue(DATA));
    }

    static CommandLinePart prepareCore() throws SetupFailedException, IOException {
        return prepareCore(KEYSTORE_PASSWORD);
    }

    private static CommandLinePart prepareCommandLine(final CommandLinePart strategy,
            final CommandLinePart emailConfig, final CommandLinePart stonky, final CommandLinePart jmxConfig,
            final CommandLinePart core, final CommandLinePart logging) {
        try {
            final File cliConfigFile = assembleCliFile(core, strategy, emailConfig);
            // have the CLI file loaded during RoboZonky startup
            final CommandLinePart commandLine = new CommandLinePart()
                    .setOption("@" + cliConfigFile.getAbsolutePath())
                    .setProperty(Settings.FILE_LOCATION_PROPERTY, SETTINGS_FILE.getAbsolutePath())
                    .setEnvironmentVariable("JAVA_HOME", "");
            // now proceed to set all system properties and settings
            final Properties settings = new Properties();
            Util.processCommandLine(commandLine, settings, strategy, stonky, jmxConfig, core, logging);
            // write settings to a file
            Util.writeOutProperties(settings, SETTINGS_FILE);
            return commandLine;
        } catch (final IOException ex) {
            throw new IllegalStateException("Failed writing CLI.", ex);
        }
    }

    private static void prepareLinuxServices(final File runScript) {
        for (final ServiceGenerator serviceGenerator : ServiceGenerator.values()) {
            final File result = serviceGenerator.apply(runScript);
            LOGGER.info("Generated {} as a {} service.", result, serviceGenerator);
        }
    }

    private static CommandLinePart prepareLogging() {
        try {
            final InputStream log4j2config = RoboZonkyInstallerListener.class.getResourceAsStream("/log4j2.xml");
            FileUtils.copyInputStreamToFile(log4j2config, LOG4J2_CONFIG_FILE);
            return new CommandLinePart().setProperty("log4j.configurationFile",
                    LOG4J2_CONFIG_FILE.getAbsolutePath());
        } catch (final IOException ex) {
            throw new IllegalStateException("Failed copying Log4j configuration file.", ex);
        }
    }

    private static CommandLinePart prepareStonky() {
        try {
            final String host = Variables.GOOGLE_CALLBACK_HOST.getValue(DATA);
            final String port = Variables.GOOGLE_CALLBACK_PORT.getValue(DATA);
            final CommandLinePart cli = new CommandLinePart();
            if (Boolean.valueOf(Variables.IS_STONKY_ENABLED.getValue(DATA))) {
                cli.setProperty(GOOGLE_CALLBACK_HOST.getKey(), host);
                cli.setProperty(GOOGLE_CALLBACK_PORT.getKey(), port);
                // reuse the same code for this as we do in CLI
                LOGGER.debug("Preparing Google credentials.");
                final String username = Variables.ZONKY_USERNAME.getValue(DATA);
                final GoogleCredentialsFeature google = new GoogleCredentialsFeature(username);
                google.setHost(host);
                google.setPort(Integer.parseInt(port));
                google.runGoogleCredentialCheck();
                LOGGER.debug("Credential check over.");
                // copy credentials to the correct directory
                final File source = GOOGLE_LOCAL_FOLDER.getValue().map(File::new).filter(File::isDirectory)
                        .orElseThrow(() -> new IllegalStateException("Google credentials folder is not proper."));
                final File target = new File(INSTALL_PATH, source.getName());
                LOGGER.debug("Will copy {} to {}.", source, target);
                FileUtils.moveDirectory(source, target);
            }
            return cli;
        } catch (final Exception ex) {
            throw new IllegalStateException("Failed configuring Google account.", ex);
        }
    }

    RoboZonkyInstallerListener.OS getOperatingSystem() {
        return operatingSystem;
    }

    private void prepareRunScript(final CommandLinePart commandLine) {
        final RunScriptGenerator generator = operatingSystem == RoboZonkyInstallerListener.OS.WINDOWS
                ? RunScriptGenerator.forWindows(DIST_PATH, CLI_CONFIG_FILE)
                : RunScriptGenerator.forUnix(DIST_PATH, CLI_CONFIG_FILE);
        final File runScript = generator.apply(commandLine);
        final File distRunScript = generator.getChildRunScript();
        Stream.of(runScript, distRunScript).forEach(script -> {
            final boolean success = script.setExecutable(true);
            LOGGER.info("Made '{}' executable: {}.", script, success);
        });
        if (operatingSystem == RoboZonkyInstallerListener.OS.LINUX) {
            prepareLinuxServices(runScript);
        }
    }

    @Override
    public void afterPacks(final List<Pack> packs, final ProgressListener progressListener) {
        try {
            progressListener.startAction("Konfigurace RoboZonky", 8);
            progressListener.nextStep("Pprava strategie.", 1, 1);
            final CommandLinePart strategyConfig = prepareStrategy();
            progressListener.nextStep("Pprava nastaven e-mailu.", 2, 1);
            final CommandLinePart emailConfig = prepareEmailConfiguration();
            progressListener.nextStep("Pprava Google ?tu.", 3, 1);
            final CommandLinePart stonky = prepareStonky();
            progressListener.nextStep("Pprava nastaven JMX.", 4, 1);
            final CommandLinePart jmx = prepareJmx();
            progressListener.nextStep("Pprava nastaven Zonky.", 5, 1);
            final CommandLinePart core = prepareCore();
            progressListener.nextStep("Pprava nastaven logovn.", 6, 1);
            final CommandLinePart logging = prepareLogging();
            progressListener.nextStep("Generovn parametr pkazov dky.", 7, 1);
            final CommandLinePart result = prepareCommandLine(strategyConfig, emailConfig, stonky, jmx, core,
                    logging);
            progressListener.nextStep("Generovn spustitelnho souboru.", 8, 1);
            prepareRunScript(result);
            progressListener.stopAction();
        } catch (final Exception ex) {
            LOGGER.error("Uncaught exception.", ex);
            throw new IllegalStateException("Uncaught exception.", ex);
        }
    }

    enum OS {
        WINDOWS, LINUX, OTHER
    }
}