OceanusLauncher.java

/* *****************************************************************************
 * Oceanus: Java Utilities
 * Copyright 2012,2025 Tony Washer
 *
 * 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 net.sourceforge.joceanus.oceanus.jar;

import net.sourceforge.joceanus.oceanus.exc.OceanusDataException;
import net.sourceforge.joceanus.oceanus.base.OceanusException;
import net.sourceforge.joceanus.oceanus.logger.OceanusLogManager;
import net.sourceforge.joceanus.oceanus.logger.OceanusLogger;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Launcher utilities.
 */
public final class OceanusLauncher {
    /**
     * Create Logger.
     */
    private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(OceanusLauncher.class);

    /**
     * Are we windows?
     */
    private static final boolean OS_WINDOWS = System.getProperty("os.name").startsWith("Windows");

    /**
     * NewLine character.
     */
    private static final String NEWLINE = "\n";

    /**
     * Resources directory.
     */
    private static final String RESOURCES = "../resources";

    /**
     * Private constructor.
     */
    private OceanusLauncher() {
    }

    /**
     * Main entry point.
     *
     * @param pArgs the program arguments
     */
    public static void main(final String[] pArgs) {
        /* Protect against exceptions */
        try {
            /* Access the Backup location */
            final String myBackup = "dist";
            OceanusLauncher.processJarFiles(new File(myBackup + "/bin"));

            /* Handle exceptions */
        } catch (OceanusException e) {
            LOGGER.fatal("Failed to create launch scripts", e);
        }
    }

    /**
     * Create launchers for jars in directory.
     * @param pDirectory the directory.
     * @throws OceanusException on error
     */
    public static void processJarFiles(final File pDirectory) throws OceanusException {
        /* Loop through the jar files in the directory */
        for (File myJar: Objects.requireNonNull(pDirectory.listFiles(f -> f.getName().endsWith(".jar")))) {
            /* Process jar file */
            final Manifest myManifest = loadManifest(myJar);
            writeLauncher(myJar, myManifest.getMainAttributes());
        }
    }

    /**
     * Write launcher.
     * @param pJar the Jar file.
     * @param pAttrs the attributes
     * @throws OceanusException on error
     */
    private static void writeLauncher(final File pJar,
                                      final Attributes pAttrs) throws OceanusException {
        /* Access details */
        final String myPreLoader = pAttrs.getValue("JavaFX-Preloader-Class");
        final String myMainClass = pAttrs.getValue("Main-Class");
        final String myClassPath = pAttrs.getValue("Class-Path");
        final String mySplash = pAttrs.getValue("SplashScreen-Image");
        final String myModule = pAttrs.getValue("Automatic-Module-Name");

        /* If there is no mainClass, then just return */
        if (myMainClass == null) {
            return;
        }

        /* Create the StringBuilder */
        final StringBuilder myBuilder = new StringBuilder();
        final String myName = pJar.getName();

        /* Output the header */
        myBuilder.append(getBatchHeader())
                .append(getComment("make sure that we are in the same directory as the jar file"))
                .append(setDirectory());

        /* Report details */
        myBuilder.append(getComment("set up details of the jarFile"))
                .append(setVariable("JARFILE")).append(myName).append(NEWLINE)
                .append(setVariable("MODULE")).append(myModule).append(NEWLINE)
                .append(setVariable("MAIN")).append(myMainClass).append(NEWLINE);
        if (myPreLoader != null) {
            myBuilder.append(setVariable("PRELOADER")).append(myPreLoader).append(NEWLINE);
        }
        if (mySplash != null) {
            myBuilder.append(setVariable("SPLASH")).append(RESOURCES).append("/").append(mySplash).append(NEWLINE);
            extractSplash(pJar, mySplash);
        }
        myBuilder.append(NEWLINE);

        /* Obtain and process the classPath */
        if (myClassPath != null) {
            final String[] myClasses = myClassPath.split(" ");
            myBuilder.append(getComment("build the modulePath from the classPath"));
            myBuilder.append(setVariable("JARS")).append(myClasses[0]).append(NEWLINE);
            for (int i = 1; i < myClasses.length; i++) {
                myBuilder.append(setVariable("JARS")).append(getValue("JARS")).append(File.pathSeparator).append(myClasses[i]).append(NEWLINE);
            }
            myBuilder.append(NEWLINE);
        }

        /* Output the commandLine */
        myBuilder.append(getComment("run the jar"));
        myBuilder.append("java ");
        if (myPreLoader != null) {
            myBuilder.append("-Djavafx.preloader=").append(getValue("PRELOADER")).append(" ");
        }
        if (mySplash != null) {
            myBuilder.append("-splash:").append(getValue("SPLASH")).append(" ");
        }
        myBuilder.append("-p ").append(getValue("JARFILE"));
        if (myClassPath != null) {
            myBuilder.append(File.pathSeparator).append(getValue("JARS"));
        }
        myBuilder.append(" -m ").append(getValue("MODULE")).append("/").append(getValue("MAIN")).append(" &");
        myBuilder.append(getBatchTrailer());

        /* determine the launch file name */
        final String mySuffix = "-" + pAttrs.getValue("Implementation-Version") + ".jar";
        final String myFileName = myName.substring(0, myName.length() - mySuffix.length()) + getBatchSuffix();
        final File myOutFile = new File(pJar.getParent(), myFileName);
        writeBatchFile(myOutFile, myBuilder.toString());
    }

    /**
     * Load Manifest.
     * @param pJar the Jar file.
     * @return the manifest
     * @throws OceanusException on error
     */
    private static Manifest loadManifest(final File pJar) throws OceanusException {
        try (FileInputStream myInStream = new FileInputStream(pJar);
             BufferedInputStream myInBuffer = new BufferedInputStream(myInStream);
             ZipInputStream myZipStream = new ZipInputStream(myInBuffer)) {
            /* Loop through the Zip file entries */
            for (;;) {
                /* Read next entry */
                final ZipEntry myEntry = myZipStream.getNextEntry();

                /* If this is EOF we did not find the manifest */
                if (myEntry == null) {
                    throw new OceanusDataException("Manifest not found");
                }

                /* Process manifest file if found */
                if ("META-INF/MANIFEST.MF".equals(myEntry.getName())) {
                    return new Manifest(myZipStream);
                }
            }

            /* Handle exceptions */
        } catch (IOException e) {
            throw new OceanusDataException("Exception accessing Zip file", e);
        }
    }

    /**
     * Extract splash file.
     * @param pJar the Jar file.
     * @param pSplash the path to the splash file
     * @throws OceanusException on error
     */
    private static void extractSplash(final File pJar,
                                      final String pSplash) throws OceanusException {
        try (FileInputStream myInStream = new FileInputStream(pJar);
             BufferedInputStream myInBuffer = new BufferedInputStream(myInStream);
             ZipInputStream myZipStream = new ZipInputStream(myInBuffer)) {
            /* Loop through the Zip file entries */
            for (;;) {
                /* Read next entry */
                final ZipEntry myEntry = myZipStream.getNextEntry();

                /* If this is EOF we did not find the manifest */
                if (myEntry == null) {
                    throw new OceanusDataException("Splash not found");
                }

                /* Process manifest file if found */
                if (pSplash.equals(myEntry.getName())) {
                    /* Determine location for splashFile */
                    final File myBase = new File(pJar.getParent(), RESOURCES);
                    final File myTarget = new File(myBase, pSplash);
                    final File myDir = new File(myTarget.getParent());

                    /* If we created the directory OK */
                    if (myDir.mkdirs()) {
                        /* Copy the splashScreen */
                        try (FileOutputStream myStream = new FileOutputStream(myTarget)) {
                            myZipStream.transferTo(myStream);
                        }
                    }

                    /* Return */
                    return;
                }
            }

            /* Handle exceptions */
        } catch (IOException e) {
            throw new OceanusDataException("Exception copying Splash file", e);
        }
    }

    /**
     * Write batchFile.
     * @param pTarget the target batchFile.
     * @param pText the contents of the batch file
     * @throws OceanusException on error
     */
    private static void writeBatchFile(final File pTarget,
                                       final String pText) throws OceanusException {
        try (FileOutputStream myOutput = new FileOutputStream(pTarget);
             BufferedOutputStream myBuffer = new BufferedOutputStream(myOutput);
             OutputStreamWriter myWriter = new OutputStreamWriter(myBuffer, StandardCharsets.ISO_8859_1)) {
            /* Write the text to the file */
            myWriter.write(pText);

            /* Handle exceptions */
        } catch (IOException e) {
            throw new OceanusDataException("Exception writing batch file", e);
        }

        /* Try to make file executable */
        if (!OS_WINDOWS) {
            pTarget.setExecutable(true);
        }
    }

    /**
     * Obtain the batch file header.
     * @return the header.
     */
    private static String getBatchHeader() {
        return OS_WINDOWS ? "@echo off\nsetlocal\n\n" : "#!/usr/bin/ksh\n\n";
    }

    /**
     * Obtain the batch setDirectory command.
     * @return the command.
     */
    private static String setDirectory() {
        return OS_WINDOWS ? "cd %0\\..\n\n" : "cd $(dirname %0)\n\n";
    }

    /**
     * Obtain the batch file trailer.
     * @return the trailer.
     */
    private static String getBatchTrailer() {
        return OS_WINDOWS ? "\n\nendlocal\n" : "\n\n";
    }

    /**
     * Obtain the batch file comment.
     * @param pComment the comment
     * @return the comment.
     */
    private static String getComment(final String pComment) {
        return (OS_WINDOWS ? "rem " : "# ") + pComment + NEWLINE;
    }

    /**
     * Set a variable's value.
     * @param pVar the variable
     * @return the set clause.
     */
    private static String setVariable(final String pVar) {
        return (OS_WINDOWS ? "set " : "") + pVar + '=';
    }

    /**
     * Obtain a variable's value.
     * @param pVar the variable
     * @return the value.
     */
    private static String getValue(final String pVar) {
        return OS_WINDOWS ? "%" + pVar + "%" : "$" + pVar;
    }

    /**
     * Obtain the batch suffix.
     * @return the suffix.
     */
    private static String getBatchSuffix() {
        return OS_WINDOWS ? ".bat" : ".ksh";
    }
}