public class ExcelsiorJet {

    private final JetHome jetHome;
    private final Log logger;

    private JetEdition edition;
    private OS targetOS;
    private CpuArch targetCpu;

    public ExcelsiorJet(JetHome jetHome, Log logger) throws JetHomeException {
        this.jetHome = jetHome;
        this.logger = logger;
        detectEditionAndTargetPlatform();
    }

    public ExcelsiorJet(String jetHome) throws JetHomeException {
        this(Utils.isEmpty(jetHome) ? new JetHome() : new JetHome(jetHome), Log.logger);
    }

    /**
     * Invokes the {@code jc} command line tool in the given {@code workingDirectory} with a logger specified at construction time, passing
     * {@code args} to it.
     *
     * @param workingDirectory working directory for {@code jc}. May be null, in which case the working directory of the current process is used.
     * @param args command line arguments that will be passed to {@code jc}.
     */
    public int compile(File workingDirectory, String... args) throws CmdLineToolException {
        return new JetCompiler(jetHome, args)
                .workingDirectory(workingDirectory)
                .withLog(logger)
                .execute();
    }

    /**
     * Invokes the {@code xpack} command line tool in the given {@code workingDirectory} with a logger specified at construction time, passing
     * {@code args} to it.
     *
     * @param workingDirectory working directory for {@code xpack}. May be null, in which case the working directory of the current process is used.
     * @param args command line arguments that will be passed to {@code xpack}.
     */
    public int pack(File workingDirectory, String... args) throws CmdLineToolException {
        return new JetPackager(jetHome, args)
                .workingDirectory(workingDirectory)
                .withLog(logger)
                .execute();
    }

    /**
     * Invokes the {@code xjava} command line tool in the given {@code workingDirectory} with a logger specified at construction time, passing
     * {@code args} to it.
     *
     * @param workingDirectory working directory for {@code xjava}. May be null, in which case the working directory of the current process is used.
     * @param args command line arguments that will be passed to {@code xjava}.
     */
    public int testRun(File workingDirectory, String... args) throws CmdLineToolException {
        return testRun(workingDirectory, logger, false, args);
    }

    /**
     * Invokes the {@code xjava} command line tool in the given {@code workingDirectory} with the specified {@code logger}, passing
     * {@code args} to it.
     *
     * @param workingDirectory working directory for {@code xjava}. May be null, in this case current process's working directory is used.
     * @param logger logger to which the output of {@code xjava} should be redirected.
     * @param errToOut specifies whether the stderr of xjava should be redirected with info level.
     * @param args command line arguments that will be passed to {@code xjava}.
     */
    public int testRun(File workingDirectory, Log logger, boolean errToOut, String... args) throws CmdLineToolException {
        return new XJava(jetHome, args)
                .workingDirectory(workingDirectory)
                .withLog(logger, errToOut)
                .execute();
    }

    private String obtainVersionString() throws JetHomeException {
        try {
            String[] result = {null};
            CmdLineTool jetCompiler = new JetCompiler(this.jetHome).withLog(new StdOutLog() {
                public void info(String info) {
                    if (result[0] == null) {
                        if (info.contains("Excelsior JET ")) {
                            result[0] = info;
                        }
                    }
                }

            });
            if ((jetCompiler.execute() != 0) || result[0] == null)  {
                throw new JetHomeException(Txt.s("JetHome.UnableToDetectEdition.Error"));
            }
            return result[0];
        } catch (CmdLineToolException e) {
            throw new JetHomeException(e.getMessage());
        }
    }

    private void detectEditionAndTargetPlatform() throws JetHomeException {
        if (edition == null) {
            String version = obtainVersionString();
            edition = JetEdition.retrieveEdition(version);
            if (edition == null) {
                throw new JetHomeException(Txt.s("JetHome.UnableToDetectEdition.Error"));
            }

            targetOS = Host.getOS();

            if (version.contains("64-bit")) {
                targetCpu = CpuArch.AMD64;
            } else if (version.contains("ARM")) {
                targetCpu = CpuArch.ARM32;
                //currently Excelsior JET supports only ARM Linux
                targetOS = OS.LINUX;
            } else {
                targetCpu = CpuArch.X86;
            }
        }
    }

    public JetEdition getEdition() {
        return edition;
    }

    public OS getTargetOS()  {
        return targetOS;
    }

    private boolean isX86() {
        return targetCpu == CpuArch.X86;
    }

    public boolean isGlobalOptimizerSupported() {
        return (isX86() || since12_0()) && (getEdition() != JetEdition.STANDARD);
    }

    public boolean isSlimDownSupported() {
        return isGlobalOptimizerSupported() && isX86();
    }

    public boolean isUsageListGenerationSupported() {
        return isX86() || since12_0();
    }

    public boolean isStartupProfileGenerationSupported()  {
        return getEdition() != JetEdition.STANDARD;
    }

    private boolean isFullFeaturedEdition() {
        return (edition == JetEdition.EVALUATION) || (edition == JetEdition.ENTERPRISE);
    }

    private boolean isEmbedded() {
        return (edition == JetEdition.EMBEDDED) || (edition == JetEdition.EMBEDDED_EVALUATION);
    }

    public boolean isTomcatSupported() {
        return  isFullFeaturedEdition() ||
                since11_3() && isEmbedded();
    }

    public boolean isSpringBootSupported() {
        return  since15_3() &&
                (isFullFeaturedEdition() || isEmbedded());
    }

    public boolean isPDBConfigurationSupported() {
        return since15_0();
    }

    public boolean isSmartSupported() {
        return since15_0() && !isX86();
    }
    public boolean since11_3() {
        return jetHome.getJetVersion() >= 1130;
    }

    public boolean since12_0() {
        return jetHome.getJetVersion() >= 1200;
    }

    public boolean since15_0() {
        return jetHome.getJetVersion() >= 1500;
    }

    public boolean since15_3() {
        return jetHome.getJetVersion() >= 1530;
    }

    public boolean isCrossCompilation() {
        //currently only ARM32 is supported in cross compilation mode
        return targetCpu == CpuArch.ARM32;
    }

    public boolean isTestRunSupported() {
        return !isCrossCompilation();
    }

    public boolean isExcelsiorInstallerSupported() {
        return !getTargetOS().isOSX() && !(isEmbedded());
    }

    public boolean isAdvancedExcelsiorInstallerFeaturesSupported() {
        return isExcelsiorInstallerSupported() && since11_3() && (edition != JetEdition.STANDARD);
    }

    public boolean isStartupAcceleratorSupported() {
        return !getTargetOS().isOSX() && !isCrossCompilation() && (edition != JetEdition.STANDARD);
    }

    public boolean isWindowsServicesSupported() {
        return targetOS.isWindows() && (edition != JetEdition.STANDARD);
    }

    public boolean isWindowsServicesInExcelsiorInstallerSupported() {
        return isWindowsServicesSupported() && isExcelsiorInstallerSupported() && since11_3();
    }

    public boolean isCompactProfilesSupported() {
        return since11_3() && (edition != JetEdition.STANDARD);
    }

    public boolean isMultiAppSupported() {
        return edition != JetEdition.STANDARD;
    }

    public boolean isTrialSupported() {
        return edition != JetEdition.STANDARD;
    }

    public boolean isDataProtectionSupported() {
        return edition != JetEdition.STANDARD;
    }

    public boolean isDiskFootprintReductionSupported() {
        return since11_3() && isGlobalOptimizerSupported();
    }

    public boolean isHighDiskFootprintReductionSupported() {
        return isDiskFootprintReductionSupported() && isX86();
    }

    public boolean isWindowsVersionInfoSupported() {
        return targetOS.isWindows() && (edition != JetEdition.STANDARD);
    }

    public boolean isRuntimeSupported(RuntimeFlavorType flavor) {
        switch (flavor) {
            case SERVER:
                return isFullFeaturedEdition() ||
                        (since11_3() && isEmbedded());
            case DESKTOP:
                return edition != JetEdition.STANDARD;
            case CLASSIC:
                return true;
            default:
                throw new AssertionError("Unknown runtime flavor:" + flavor);
        }
    }

    public boolean isChangeRTLocationAvailable() {
        return since11_3();
    }

    public boolean isPGOSupported() {
        return since12_0() && !isX86() && (edition != JetEdition.STANDARD) && (edition != JetEdition.PROFESSIONAL);
    }

    /**
     * @return home directory of this Excelsior JET instance
     */
    public String getJetHome() {
        return jetHome.getJetHome();
    }

} public class CmdLineTool {

    private ArrayList<String> args;
    private Log log;
    private File workDir;
    private boolean errToOut = false;
    private HashMap<String, String> env = new HashMap<>();

    public CmdLineTool(String... args) {
        this.args = new ArrayList<>(Arrays.asList(args));
    }

    public CmdLineTool withLog(Log log, boolean errToOut) {
        this.log = log;
        this.errToOut = errToOut;
        return this;
    }

    public CmdLineTool withLog(Log log) {
        return withLog(log, false);
    }

    public CmdLineTool workingDirectory(File workDir) {
        this.workDir = workDir;
        return this;
    }

    public CmdLineTool withEnvironment(String var, String val) {
        this.env.put(Host.isWindows() ? var.toUpperCase() : var, val);
        return this;
    }

    public CmdLineTool arg(String arg) {
        args.add(arg);
        return this;
    }

    private class OutputReader extends Thread {

        BufferedReader reader;
        boolean err;

        OutputReader(InputStream stream, boolean err) {
            this.reader = new BufferedReader(new InputStreamReader(stream));
            this.err = err;
        }

        public void run() {
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    if (isInterrupted()) {
                        return;
                    }
                    if (log != null) {
                        if (err && !errToOut) {
                            log.error(line);
                        } else {
                            log.info(line);
                        }
                    }
                }
            } catch (IOException ignore) {
            }
        }
    }

    public int execute() throws CmdLineToolException {
        try {
            ProcessBuilder pb = new ProcessBuilder(args).directory(workDir).redirectInput(Redirect.INHERIT);
            if (!env.isEmpty()) {
                Map<String, String> penv = pb.environment();
                if (Host.isWindows()) {
                    for (String key : penv.keySet()) {
                        String keyUpper = key.toUpperCase();
                        if (env.containsKey(keyUpper)) {
                            penv.put(key, env.get(keyUpper));
                            env.remove(keyUpper);
                        }
                    }
                }
                penv.putAll(env);
            }
            Process process = pb.start();
            OutputReader inreader = new OutputReader(process.getInputStream(),false);
            inreader.start();
            OutputReader errreader = new OutputReader(process.getErrorStream(), true);
            errreader.start();
            int exitCode = process.waitFor();
            inreader.join(0);
            errreader.join(0);
            return exitCode;
        } catch (IOException | InterruptedException e) {
            throw new CmdLineToolException(e);
        }
    }

} public class CmdLineToolException extends Exception {

    public CmdLineToolException(Exception e) {
        super(e);
    }
} public enum ApplicationType {

    /**
     * Plain Java application that runs standalone.
     */
    PLAIN,

    /**
     * Spring Boot application that runs as Spring Boot executable jar.
     */
    SPRING_BOOT,

    /**
     * Servlet-based Java application that runs within the Tomcat servlet container.
     */
    TOMCAT,

    /**
     * Dynamic library callable from a non-Java environment.
     */
    DYNAMIC_LIBRARY,

    /**
     * Windows service (Windows only).
     */
    WINDOWS_SERVICE
    ;

    public String toString() {
        return Utils.enumConstantNameToParameter(name());
    }

    public static ApplicationType validate(String appType) throws JetTaskFailureException {
        try {
            return ApplicationType.valueOf(Utils.parameterToEnumConstantName(appType));
        } catch (Exception e) {
            throw new JetTaskFailureException(s("JetApi.UnknownAppType.Failure", appType));
        }
    }
} public enum PackagingType {
    NONE,
    ZIP,
    TAR_GZ,
    EXCELSIOR_INSTALLER,
    OSX_APP_BUNDLE,
    NATIVE_BUNDLE;

    public String toString() {
        return Utils.enumConstantNameToParameter(name());
    }

    public static PackagingType validate(String packaging) throws JetTaskFailureException {
        try {
            return PackagingType.valueOf(Utils.parameterToEnumConstantName(packaging));
        } catch (Exception e) {
            throw new JetTaskFailureException(s("JetApi.UnknownPackagingMode.Failure", packaging));
        }
    }

    public static PackagingType fromString(String packaging) {
        try {
            return validate(packaging);
        } catch (JetTaskFailureException e) {
            throw new AssertionError("excelsiorJetPackaging should be valid here", e);
        }
    }

    public boolean isNativeBundle() {
        return (this == EXCELSIOR_INSTALLER) || (this == OSX_APP_BUNDLE) || (this == NATIVE_BUNDLE);
    }
} public class ExecProfilesConfig {

    private static final String PROFILE_DIR = "appToProfile";

    /**
     * The target location for application execution profiles gathered by the Test Run and Profile tasks.
     * It is recommended to commit the collected profiles ({@code .usg}, {@code .startup}, {@code .jprof})
     * to the VCS to enable the {@link JetBuildTask} to re-use them during subsequent builds without performing
     * a Test Run and/or Profile task again. Once you have committed the profiles, it is recommended to set
     * the {@link #checkExistence} parameter to an appropriate value in order to verify that the profiles
     * are available during subsequent builds.
     * <p>
     * By default, {@link JetProject#jetResourcesDir} is used.
     * </p>
     * @see TestRunTask
     * @see RunTask
     */
    public File outputDir;

    /**
     * The base file name of all execution profiles. By default, {@link JetProject#projectName} is used.
     */
    public String outputName;

    /**
     * <p>
     * Whether to run the Profile task on the same machine or prepare an image for deployment
     * to a reference system.
     * </p>
     * <p>
     * It may be necessary to profile the natively compiled application on a computer other than the one
     * conducting the build, e.g. because profiling requires a specially configured environment.
     * Setting this parameter to {@code false} forces the plugin to create a special <em>profiling image</em>
     * that you can then deploy to such an environment to collect an application execution profile.
     * </p>
     * <p>
     * You can also set the {@code jet.create.profiling.image} system property to force the Profile task to create
     * such an image instead of running the generated binary locally.
     * </p>
     * <p>
     * Note that this parameter is always set to {@code false} for the cross-compiling flavors of Excelsior JET,
     * e.g. those targeting Linux/ARM.
     * </p>
     *
     * @see #profilingImageDir
     */
    public Boolean profileLocally;

    /**
     * <p>
     * Directory where the special "profiling" image of the natively compiled application has to be placed.
     * </p>
     * <p>
     * By default, points to the {@code "appToProfile"} subdirectory of {@link JetProject#jetOutputDir}.
     * </p>
     * <p>
     * To facilitate deployment of the profiling image to a reference system when {@link #profileLocally}
     * is set to {@code false}, the plugin also creates a zip archive that contains a copy of that image,
     * gives it the same base name and places it next to this directory.
     * </p>
     */
    public File profilingImageDir;

    /**
     * <p>
     * Profile validity threshold in days.
     * </p>
     * <p>
     * It is recommended to re-collect all profiles periodically as your code base evolves.
     * The plugins issue a warning upon detecting an outdated profile during a build.
     * With this parameter, you can adjust the respective threshold, measured in days,
     * or set it to {@code 0} to disable the warning. The default value is 30.
     * </p>
     */
    public int daysToWarnAboutOutdatedProfiles = 30;

    /**
     * Check that all or certain application profiles are available before starting a build.
     * Valid values are: "all" , "test-run", "profile", "none" (default):
     * <p>
     * {@code test-run} - profiles collected by the Test Run task (".usg", ".startup").
     * </p>
     * <p>
     * {@code profile} - application executiion profile collected by the Profile task (".jprof").
     * </p>
     * <p>
     * {@code all} - all profiles (".usg", ".startup", and ".jprof").
     * </p>
     */
    public String checkExistence = ExecProfilesExistenceType.NONE.toString();

    /**
     * Timeout for the test run task in seconds.
     *
     * Set the parameter, if your app does not terminate automatically and you would like to automate collection of
     * the profiles gathered during {@link TestRunTask}.
     */
    public int testRunTimeout;

    /**
     * Timeout for the profile run task in seconds.
     *
     * Set the parameter, if your app does not terminate automatically and you would like to automate collection of
     * the execution profile gathered during jet:profile task.
     */
    public int profileRunTimeout;

    public void fillDefaults(JetProject jetProject, ExcelsiorJet excelsiorJet) throws JetTaskFailureException {
        if (outputDir == null) {
            outputDir = jetProject.jetResourcesDir();
        }
        if (Utils.isEmpty(outputName)) {
            outputName = jetProject.projectName();
        }

        if (excelsiorJet.isCrossCompilation())  {
            if ((profileLocally != null) && profileLocally) {
                logger.warn(s("JetApi.CannotProfileLocallyForCrossCompilation.Warning"));
            }
            profileLocally = false;
        } else if (System.getProperty("jet.create.profiling.image") != null) {
            profileLocally = false;
        } else if (profileLocally == null) {
            profileLocally = true;
        }

        if (profilingImageDir == null) {
            profilingImageDir = new File(jetProject.jetOutputDir(), PROFILE_DIR);
        }

        ExecProfilesExistenceType existenceType = ExecProfilesExistenceType.validate(checkExistence);
        if ((existenceType == ALL) || (existenceType == TEST_RUN)) {
            if (excelsiorJet.isUsageListGenerationSupported() && !getUsg().exists()) {
                throw new JetTaskFailureException(s("JetApi.NoTestRunProfile.Failure", getUsg().getAbsolutePath()));
            }
            if (excelsiorJet.isStartupProfileGenerationSupported() && !getStartup().exists()) {
                throw new JetTaskFailureException(s("JetApi.NoTestRunProfile.Failure", getStartup().getAbsolutePath()));
            }
        }
        if ((existenceType == ALL) || (existenceType == PROFILE)) {
            if (excelsiorJet.isPGOSupported() && !getJProfile().exists()) {
                throw new JetTaskFailureException(s("JetApi.NoJProfile.Failure", getJProfile().getAbsolutePath()));
            }
        }
    }


    public File getUsg() {
        return new File(outputDir, outputName + ".usg");
    }

    public File getStartup() {
        return new File(outputDir, outputName + ".startup");
    }

    public File getJProfile() {
        return new File(outputDir, outputName + ".jprof");
    }

} public class Txt {

    private static Messages messages = new Messages("Strings");
    private static Messages altMessages;
    public static Log log;

    public static void setAdditionalMessages(ResourceBundle rb) {
        altMessages = new Messages(rb);
    }

    public static String s(String id, Object... params) {
        String str = null;
        if (altMessages != null) {
            str = altMessages.format(id, params);
        }
        if (str == null) {
            str = messages.format(id, params);
        }
        if (str != null) {
            return str;
        } else {
            if (log != null) {
                log.error("JET message file broken: key = " + id);
            } else {
                throw new IllegalStateException("No log to issue error. JET message file broken: key = " + id);
            }
            return id;
        }
    }
} public class Utils {

    public static void cleanDirectory(File f) throws IOException {
        Files.walkFileTree(f.toPath(), new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                return FileVisitResult.CONTINUE;
            }

            private void deleteFile(File f) throws IOException {
                if (!f.delete()) {
                    if (f.exists()) {
                        throw new IOException(Txt.s("JetApi.UnableToDelete.Error", f.getAbsolutePath()));
                    }
                }
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                deleteFile(file.toFile());
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                if (file.toFile().exists()) {
                    throw new IOException(Txt.s("JetApi.UnableToDelete.Error", f.getAbsolutePath()));
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                deleteFile(dir.toFile());
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public static void cleanDirectorySilently(File f) {
        try {
            cleanDirectory(f);
        } catch (IOException ignore) {
        }
    }

    public static void copyFile(Path source, Path target) throws IOException {
        if (!target.toFile().exists()) {
            Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES);
        } else if (source.toFile().lastModified() != target.toFile().lastModified()) {
            //copy only files that were changed
            Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
        }

    }

    public static void copyDirectory(Path source, Path target) throws IOException {
        Files.walkFileTree(source, new FileVisitor<Path>() {

            @Override
            public FileVisitResult preVisitDirectory(Path subfolder, BasicFileAttributes attrs) throws IOException {
                Files.createDirectories(target.resolve(source.relativize(subfolder)));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException {
                Path targetFile = target.resolve(source.relativize(sourceFile));
                copyFile(sourceFile, targetFile);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path sourceFile, IOException e) throws IOException {
                throw new IOException(Txt.s("Utils.CannotCopyFile.Error", sourceFile.toString(), e.getMessage()), e);
            }

            @Override
            public FileVisitResult postVisitDirectory(Path source, IOException ioe) throws IOException {
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public static boolean isEmpty(String s) {
        return (s == null) || s.isEmpty();
    }

    public static boolean isEmpty(String[] strings) {
        return (strings == null) || (strings.length == 0);
    }

    public static String randomAlphanumeric(int count) {
        char[] chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < count; i++) {
            res.append(chars[(int) (chars.length * Math.random())]);
        }
        return res.toString();
    }

    public static void copy(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024 * 1024];
        int len;
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }
    }

    public static void mkdir(File dir) throws JetTaskFailureException {
        if (!dir.exists() && !dir.mkdirs()) {
            if (!dir.exists()) {
                throw new JetTaskFailureException(s("JetApi.DirCreate.Error", dir.getAbsolutePath()));
            }
            logger.warn(s("JetApi.DirCreate.Warning", dir.getAbsolutePath()));
        }
    }

    @FunctionalInterface
    private interface CreateArchiveEntry {
        ArchiveEntry createEntry(String name, long size, int mode);
    }

    private static ArchiveEntry createZipEntry(String name, long size, int mode) {
        ZipArchiveEntry entry = new ZipArchiveEntry(name);
        if (Host.isUnix()) {
            entry.setUnixMode(mode);
        }
        return entry;
    }

    private static ArchiveEntry createTarEntry(String name, long size, int mode) {
        TarArchiveEntry entry = new TarArchiveEntry(name);
        entry.setSize(size);
        if (Host.isUnix()) {
            entry.setMode(mode);
        }
        return entry;
    }

    private static void compressDirectoryToArchive(String rootDir, String sourceDir, ArchiveOutputStream out, CreateArchiveEntry cae) throws IOException {
        File[] files = new File(sourceDir).listFiles();
        assert files != null;
        for (File file : files) {
            if (file.isDirectory()) {
                compressDirectoryToArchive(rootDir, sourceDir + File.separator + file.getName(), out, cae);
            } else {
                ArchiveEntry entry = cae.createEntry(
                        file.getAbsolutePath().substring(rootDir.length() + 1),
                        file.length(),
                        file.canExecute() ? /*-rwxr-xr-x*/ 0100755 : /*-rw-r--r--*/ 0100644
                );
                out.putArchiveEntry(entry);
                try (InputStream in = new BufferedInputStream(new FileInputStream(sourceDir + File.separator + file.getName()))) {
                    copy(in, out);
                }
                out.closeArchiveEntry();
            }
        }
    }

    public static void compressToZipFile(File sourceDir, File outputFile) throws IOException {
        try (ZipArchiveOutputStream zipFile = new ZipArchiveOutputStream(
                new BufferedOutputStream(new FileOutputStream(outputFile)))) {
            compressDirectoryToArchive(sourceDir.getAbsolutePath(), sourceDir.getAbsolutePath(), zipFile,
                    Utils::createZipEntry);
        }
    }

    public static void compressToTarGzFile(File sourceDir, File outputFile) throws IOException {
        try (TarArchiveOutputStream tarFile = new TarArchiveOutputStream(new GzipCompressorOutputStream(
                new BufferedOutputStream(new FileOutputStream(outputFile))))) {
            compressDirectoryToArchive(sourceDir.getAbsolutePath(), sourceDir.getAbsolutePath(), tarFile,
                    Utils::createTarEntry);
        }
    }

    public static void copyQuietly(Path source, Path target) {
        // We could just use Maven FileUtils.copyDirectory method but it copies a directory as a whole
        // while here we copy only those files that were changed from previous build.
        try {
            copyDirectory(source, target);
        } catch (IOException e) {
            logger.warn(s("TestRunTask.ErrorWhileCopying.Warning", source.toString(), target.toString(), e.getMessage()), e);
        }
    }

    public static String parameterToEnumConstantName(String parameter) {
        return parameter.toUpperCase().replace('-', '_');
    }

    public static String enumConstantNameToParameter(String constantName) {
        return constantName.toLowerCase().replace('_', '-');
    }

    /**
     * Encloses string in double quotes (") if it contains space.
     */
    public static String quoteCmdLineArgument(String arg) {
        return arg.contains(" ") ? '"' + arg + '"' : arg;
    }

    /**
     * Splits a string containing value for {@link com.excelsiorjet.api.tasks.JetProject#runArgs},
     * where arguments are separated by commas and commas within an argument are escaped with '\'.
     * <p>
     * For example, "value1,value2.1\, value2.2"
     * is parsed into 2 arguments: ["value1", "value2.1, value2.2"]
     */
    public static String[] parseRunArgs(String runArgs) {
        List<String> res = new ArrayList<>();
        StringBuilder buff = new StringBuilder();
        for (int i = 0; i < runArgs.length(); i++) {
            char c = runArgs.charAt(i);
            if (c == ',') {
                if (i > 0 && runArgs.charAt(i - 1) == '\\') {
                    // replace "\," with ","
                    buff.setCharAt(buff.length() - 1, c);
                } else {
                    // split args
                    res.add(buff.toString());
                    buff = new StringBuilder();
                }
            } else {
                buff.append(c);
            }
        }
        res.add(buff.toString());
        return res.toArray(new String[res.size()]);
    }

    public static String[] prepend(String firstElement, String[] remaining) {
        if (remaining == null) {
            return new String[]{firstElement};
        }
        ArrayList<String> res = new ArrayList<>();
        res.add(firstElement);
        res.addAll(Arrays.asList(remaining));
        return res.toArray(new String[remaining.length + 1]);
    }

    public static String getCanonicalPath(File path) {
        try {
            return path.getCanonicalPath();
        } catch (IOException e) {
            // getCanonicalPath throws IOException,
            // so just return absolute path in a very rare case of IOException as there is no other
            // appropriate way to handle this situation.
            return path.getAbsolutePath();
        }
    }

    /**
     * @return String in format "([groupId],[artifactId],[version], [path])" (all fields are optional)
     */
    public static String idStr(String groupId, String artifactId, String version, File path) {
        return Stream.of(groupId, artifactId, version, path == null? null: getCanonicalPath(path)).
                filter(item -> item != null).
                collect(Collectors.joining(":", "(", ")"));
    }

    public static void linesToFile(List<String> lines, File file) throws FileNotFoundException {
        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream(file)))) {
            lines.forEach(out::println);
        }
    }

    public static String deriveFourDigitVersion(String version) {
        String[] versions = version.split("\\.");
        String[] finalVersions = new String[]{"0", "0", "0", "0"};
        for (int i = 0; i < Math.min(versions.length, 4); ++i) {
            try {
                finalVersions[i] = Integer.decode(versions[i]).toString();
            } catch (NumberFormatException e) {
                int minusPos = versions[i].indexOf('-');
                if (minusPos > 0) {
                    String v = versions[i].substring(0, minusPos);
                    try {
                        finalVersions[i] = Integer.decode(v).toString();
                    } catch (NumberFormatException ignore) {
                    }
                }
            }
        }
        return String.join(".", (CharSequence[]) finalVersions);
    }

    public static File checkFileWithDefault(File file, File defaultFile, String notExistKey, String notExistParam) throws JetTaskFailureException {
        if (file == null) {
            if (defaultFile.exists()) {
                return defaultFile;
            }
        } else if (!file.exists()) {
            throw new JetTaskFailureException(s(notExistKey, file.getAbsolutePath(), notExistParam));
        }
        return file;
    }
} public static Log logger; public static String s(String id, Object... params) {
    String str = null;
    if (altMessages != null) {
        str = altMessages.format(id, params);
    }
    if (str == null) {
        str = messages.format(id, params);
    }
    if (str != null) {
        return str;
    } else {
        if (log != null) {
            log.error("JET message file broken: key = " + id);
        } else {
            throw new IllegalStateException("No log to issue error. JET message file broken: key = " + id);
        }
        return id;
    }
}
import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import static com.excelsiorjet.api.log.Log.logger;
import static com.excelsiorjet.api.util.Txt.s;
import com.excelsiorjet.api.ExcelsiorJet;
import com.excelsiorjet.api.cmd.CmdLineTool;
import com.excelsiorjet.api.cmd.CmdLineToolException;
import com.excelsiorjet.api.tasks.config.ApplicationType;
import com.excelsiorjet.api.tasks.config.PackagingType;
import com.excelsiorjet.api.tasks.config.compiler.ExecProfilesConfig;
import com.excelsiorjet.api.util.Txt;
import com.excelsiorjet.api.util.Utils;
        File rspFile = new File(buildDir, project.outputName() + suffix + ".xpack");
        ArrayList<XPackOption> xpackOptions = packagerArgsGenerator.getCommonXPackOptions(targetDir, exeDir);
        return getXPackArgs(xpackOptions, rspFile);
    }

    /**
     * Packages the generated executable and required Excelsior JET runtime files
     * as a self-contained directory
     */
    private void createAppOrProfileDir(File buildDir, File appOrProfileDir) throws CmdLineToolException, JetTaskFailureException {
        ArrayList<String> xpackArgs = getCommonXPackArgs(appOrProfileDir.getAbsolutePath(), buildDir, ".SFD", "/");
        if (useXPackZipping()) {
            //since 11.3 Excelsior JET supports zipping self-contained directories itself
            xpackArgs.add("-backend");
            xpackArgs.add("self-contained-directory"); //setting backend is needed for ARM 32 due to JET-8882 bug
            xpackArgs.add("-zip");
        }
        if (excelsiorJet.pack(buildDir, xpackArgs.toArray(new String[xpackArgs.size()])) != 0) {
            throw new JetTaskFailureException(s("JetBuildTask.Package.Failure"));
        }
        if (project.appType() == ApplicationType.WINDOWS_SERVICE) {
            try {
                createWinServiceInstallScripts(appOrProfileDir);
            } catch (IOException e) {
                throw new JetTaskFailureException(s("JetBuildTask.WinServiceScriptsCreation.Failure", e.toString()), e);
            }
        }
    }

    private void createWinServiceInstallScripts(File appDir) throws IOException {
        //copy isrv to app dir
        Utils.copyFile(new File(excelsiorJet.getJetHome() + File.separator + "bin", "isrv.exe").toPath(),
                new File(appDir, "isrv.exe").toPath());

        WindowsServiceScriptsGenerator scriptsGenerator = new WindowsServiceScriptsGenerator(project, excelsiorJet);
        String rspFile = project.outputName() + ".rsp";
        Utils.linesToFile(scriptsGenerator.isrvArgs(), new File(appDir, rspFile));
        Utils.linesToFile(scriptsGenerator.installBatFileContent(rspFile), new File(appDir, "install.bat"));
        Utils.linesToFile(scriptsGenerator.uninstallBatFileContent(), new File(appDir, "uninstall.bat"));
    }

    private ArrayList<String> getExcelsiorInstallerXPackArgs(File target, File buildDir) throws JetTaskFailureException {
        File rspFile = new File(buildDir, project.outputName() + ".EI.xpack");
        ArrayList<XPackOption> xpackOptions = packagerArgsGenerator.getExcelsiorInstallerXPackOptions(target);
        return getXPackArgs(xpackOptions, rspFile);
    }

    /**
     * Packages the generated executable and required Excelsior JET runtime files
     * as a excelsior installer file.
     */
    private void packWithEI(File buildDir) throws CmdLineToolException, JetTaskFailureException, IOException {
        File target = new File(project.jetOutputDir(), excelsiorJet.getTargetOS().mangleExeName(project.artifactName()));
        ArrayList<String> xpackArgs = getExcelsiorInstallerXPackArgs(target, buildDir);
        if (excelsiorJet.pack(buildDir, xpackArgs.toArray(new String[xpackArgs.size()])) != 0) {
            throw new JetTaskFailureException(s("JetBuildTask.Package.Failure"));
        }
        logger.info(s("JetBuildTask.Build.Success"));
        logger.info(s("JetBuildTask.GetEI.Info", target.getAbsolutePath()));
    }

    private void createOSXAppBundle(File buildDir) throws JetTaskFailureException, CmdLineToolException, IOException {
        File appBundle = new File(project.jetOutputDir(), project.osxBundleConfiguration().fileName + ".app");
        Utils.mkdir(appBundle);
        try {
            Utils.cleanDirectory(appBundle);
        } catch (IOException e) {
            throw new JetTaskFailureException(e.getMessage(), e);
        }
        File contents = new File(appBundle, "Contents");
        Utils.mkdir(contents);

        ArrayList<String> xpackArgs = getCommonXPackArgs(contents.getAbsolutePath(), buildDir, ".OSXBundle", "MacOS");
        if (excelsiorJet.pack(buildDir, xpackArgs.toArray(new String[xpackArgs.size()])) != 0) {
            throw new JetTaskFailureException(s("JetBuildTask.Package.Failure"));
        }

        File contentsResources = new File(contents, "Resources");
        Utils.mkdir(contentsResources);

        try (PrintWriter out = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream(new File(contents, "Info.plist")), "UTF-8"))) {
            out.print(
                    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                            "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
                            "<plist version=\"1.0\">\n" +
                            "<dict>\n" +
                            "  <key>CFBundlePackageType</key>\n" +
                            "  <string>APPL</string>\n" +
                            "  <key>CFBundleExecutable</key>\n" +
                            "  <string>" + project.outputName() + "</string>\n" +
                            "  <key>CFBundleName</key>\n" +
                            "  <string>" + project.osxBundleConfiguration().bundleName + "</string>\n" +
                            "  <key>CFBundleIdentifier</key>\n" +
                            "  <string>" + project.osxBundleConfiguration().identifier + "</string>\n" +
                            "  <key>CFBundleVersionString</key>\n" +
                            "  <string>" + project.osxBundleConfiguration().version + "</string>\n" +
                            "  <key>CFBundleShortVersionString</key>\n" +
                            "  <string>" + project.osxBundleConfiguration().shortVersion + "</string>\n" +
                            (project.osxBundleConfiguration().icon != null ?
                                    "  <key>CFBundleIconFile</key>\n" +
                                            "  <string>" + project.osxBundleConfiguration().icon.getName() + "</string>\n" : "") +
                            (project.osxBundleConfiguration().highResolutionCapable ?
                                    "  <key>NSHighResolutionCapable</key>\n" +
                                            "  <true/>" : "") +
                            "</dict>\n" +
                            "</plist>\n");
        }

        if (project.osxBundleConfiguration().icon != null) {
            Files.copy(project.osxBundleConfiguration().icon.toPath(),
                    new File(contentsResources, project.osxBundleConfiguration().icon.getName()).toPath());
        }

        File appPkg = null;
        if (project.osxBundleConfiguration().developerId != null) {
            logger.info(s("JetBuildTask.SigningOSXBundle.Info"));

            signExecutablesInRT(new File(contents, "rt"));

            if (new CmdLineTool("codesign", "--verbose", "--force", "--deep", "-o", "runtime", "--sign",