AstraeusLauncher.java
/*
* Astraeus: Post-Processing
* Copyright 2012-2026. 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 io.github.tonywasher.joceanus.astraeus.jar;
import io.github.tonywasher.joceanus.astraeus.exc.AstraeusException;
import io.github.tonywasher.joceanus.oceanus.base.OceanusSystem;
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 AstraeusLauncher {
/**
* Are we windows?
*/
private static final boolean OS_WINDOWS = OceanusSystem.WINDOWS.equals(OceanusSystem.determineSystem());
/**
* NewLine character.
*/
private static final String NEWLINE = "\n";
/**
* Resources directory.
*/
private static final String RESOURCES = "../resources";
/**
* Private constructor.
*/
private AstraeusLauncher() {
}
/**
* Create launchers for jars in directory.
*
* @param pDirectory the directory.
* @throws AstraeusException on error
*/
public static void processJarFiles(final File pDirectory) throws AstraeusException {
/* 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 AstraeusException on error
*/
private static void writeLauncher(final File pJar,
final Attributes pAttrs) throws AstraeusException {
/* 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.pathSeparatorChar).append(myClasses[i]).append(NEWLINE);
}
myBuilder.append(NEWLINE);
}
/* Output the commandLine */
myBuilder.append(getComment("run the jar"));
if (OS_WINDOWS) {
myBuilder.append("start /B ");
}
myBuilder.append("..").append(File.separatorChar).append("java").append(File.separatorChar)
.append("bin").append(File.separatorChar).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.pathSeparatorChar).append(getValue("JARS"));
}
myBuilder.append(" -m ").append(getValue("MODULE")).append("/").append(getValue("MAIN"));
if (!OS_WINDOWS) {
myBuilder.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 AstraeusException on error
*/
private static Manifest loadManifest(final File pJar) throws AstraeusException {
try (FileInputStream myInStream = new FileInputStream(pJar);
BufferedInputStream myInBuffer = new BufferedInputStream(myInStream);
ZipInputStream myZipStream = new ZipInputStream(myInBuffer)) {
/* Loop through the Zip file entries */
while (true) {
/* Read next entry */
final ZipEntry myEntry = myZipStream.getNextEntry();
/* If this is EOF we did not find the manifest */
if (myEntry == null) {
throw new AstraeusException("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 AstraeusException("Exception accessing Zip file", e);
}
}
/**
* Extract splash file.
*
* @param pJar the Jar file.
* @param pSplash the path to the splash file
* @throws AstraeusException on error
*/
private static void extractSplash(final File pJar,
final String pSplash) throws AstraeusException {
try (FileInputStream myInStream = new FileInputStream(pJar);
BufferedInputStream myInBuffer = new BufferedInputStream(myInStream);
ZipInputStream myZipStream = new ZipInputStream(myInBuffer)) {
/* Loop through the Zip file entries */
while (true) {
/* Read next entry */
final ZipEntry myEntry = myZipStream.getNextEntry();
/* If this is EOF we did not find the manifest */
if (myEntry == null) {
throw new AstraeusException("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 AstraeusException("Exception copying Splash file", e);
}
}
/**
* Write batchFile.
*
* @param pTarget the target batchFile.
* @param pText the contents of the batch file
* @throws AstraeusException on error
*/
private static void writeBatchFile(final File pTarget,
final String pText) throws AstraeusException {
try (FileOutputStream myOutput = new FileOutputStream(pTarget);
BufferedOutputStream myBuffer = new BufferedOutputStream(myOutput);
OutputStreamWriter myWriter = new OutputStreamWriter(myBuffer, StandardCharsets.UTF_8)) {
/* Write the text to the file */
myWriter.write(pText);
/* Handle exceptions */
} catch (IOException e) {
throw new AstraeusException("Exception writing batch file", e);
}
/* Try to make file executable */
if (!OS_WINDOWS && !pTarget.setExecutable(true)) {
throw new AstraeusException("Failed to set executable indication");
}
}
/**
* 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";
}
}