/* * ****************************************************************************** * Copyright (C) 2015-2020 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * ***************************************************************************** */ package io.github.dsheirer.jmbe.creator; import io.github.dsheirer.jmbe.creator.github.GitHub; import io.github.dsheirer.jmbe.creator.github.Release; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.StringJoiner; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** * Creator utility that downloads the JMBE source code, compiles it, and generates the JMBE library jar. */ public class Creator { private final static Logger mLog = LoggerFactory.getLogger(GitHub.class); private final static String GITHUB_JMBE_RELEASES_URL = "https://api.github.com/repos/dsheirer/jmbe/releases"; /** * Exit code to indicate that the process completed successfully */ public static final int EXIT_CODE_SUCCESS = 0; /** * Exit code to indicate an unknown error */ public static final int EXIT_CODE_UNKNOWN_ERROR = 1; /** * Exit code to indicate an error when reading or writing files to the local storage system */ public static final int EXIT_CODE_IO_ERROR = 2; /** * Exit code to indicate an error when attempting to download network resources */ public static final int EXIT_CODE_NETWORK_ERROR = 3; /** * Exit code to indicate that the library path argument is required */ public static final int EXIT_CODE_LIBRARY_PATH_REQUIRED = 4; /** * Creates an instance */ public Creator() { } /** * Process a downloaded source code file * * @param downloadFile containing a GitHub JMBE release artifact * @throws IOException if there is an error */ public static void process(Path downloadFile) throws IOException { Path downloadDirectory = downloadFile.getParent(); List<Path> toCompile = new ArrayList<>(); System.out.println("Unzipping: Source Code"); try(ZipFile zf = new ZipFile(downloadFile.toFile())) { Enumeration<? extends ZipEntry> zipEntries = zf.entries(); zipEntries.asIterator().forEachRemaining(entry -> { try { if(entry.isDirectory()) { Path directoryToCreate = downloadDirectory.resolve(entry.getName()); if(!Files.exists(directoryToCreate)) { Files.createDirectory(directoryToCreate); } } else { Path fileToCreate = downloadDirectory.resolve(entry.getName()); Files.copy(zf.getInputStream(entry), fileToCreate, StandardCopyOption.REPLACE_EXISTING); if(isCompilable(entry)) { toCompile.add(fileToCreate); } } } catch(IOException ioe) { System.out.println("Failed: I/O Error While Unzipping Source Code Files"); ioe.printStackTrace(); System.exit(EXIT_CODE_IO_ERROR); } }); } catch(Exception e) { System.out.println("Failed: Error unzipping source code file - " + e.getLocalizedMessage()); } if(!toCompile.isEmpty()) { compile(toCompile, getOptions(downloadDirectory)); System.out.println("Deleting: Compiled Interfaces"); deleteInterfaceClasses(getOutputDirectory(downloadDirectory)); } } /** * Creates JAR metadata directory and manifest file * * @param outputDirectory for writing files * @param version string for the library * @throws IOException if there is an error */ public static void createJarMetadata(Path outputDirectory, String version) throws IOException { Path metaDirectory = outputDirectory.resolve("META-INF"); if(!Files.exists(metaDirectory)) { Files.createDirectory(metaDirectory); } StringBuilder sb = new StringBuilder(); sb.append("Manifest-Version: 1.0\r\n"); sb.append("Implementation-Title: jmbe\r\n"); sb.append("Version: ").append(version).append("\r\n"); sb.append("Site: https://github.com/DSheirer/jmbe\r\n"); Path manifest = metaDirectory.resolve("MANIFEST.MF"); Files.writeString(manifest, sb.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } /** * Discovers the LICENSE file from the source code tree and copies it to the output directory * * @param downloadDirectory where source code exists * @throws IOException if there is an error */ public static void copyLicenseFile(Path downloadDirectory) throws IOException { String fileName = "LICENSE"; Path license = getFile(downloadDirectory, fileName); if(license != null) { Path toLicense = getOutputDirectory(downloadDirectory).resolve(fileName); Files.copy(license, toLicense, StandardCopyOption.REPLACE_EXISTING); } } /** * Recursively finds the specified filename in the specified directory * * @param downloadDirectory to search * @param fileName to discover * @return discovered file path or null */ public static Path getFile(Path downloadDirectory, String fileName) { try { DirectoryStream<Path> stream = Files.newDirectoryStream(downloadDirectory); Iterator<Path> it = stream.iterator(); while(it.hasNext()) { Path path = it.next(); if(Files.isDirectory(path) && !path.equals(getOutputDirectory(downloadDirectory))) { Path subPath = getFile(path, fileName); if(subPath != null && subPath.endsWith(fileName)) { return subPath; } } else if(path.endsWith(fileName)) { return path; } } } catch(IOException ioe) { System.out.println("Error while searching for [" + fileName + "]"); System.exit(EXIT_CODE_IO_ERROR); } return null; } /** * Output directory for storing compiled classes and JAR artifacts * * @param downloadDirectory where source code was downloaded * @return output directory */ public static Path getOutputDirectory(Path downloadDirectory) { return downloadDirectory.resolve("output"); } /** * Creates compiler options for classpath and output directory * * @param downloadDirectory where source code is located * @return options */ public static List<String> getOptions(Path downloadDirectory) { Path libs = downloadDirectory.resolve("libs"); Path output = getOutputDirectory(downloadDirectory); String classpath = getLibraryClassPath(); List<String> options = new ArrayList<>(); options.add("-cp"); options.add(classpath); options.add("-d"); options.add(output.toString()); return options; } /** * Deletes the compiled interface classes from the output directory * * @param output * @throws IOException */ public static void deleteInterfaceClasses(Path output) throws IOException { Path iface = output.resolve("jmbe").resolve("iface"); if(Files.exists(iface) && Files.isDirectory(iface)) { DirectoryStream<Path> stream = Files.newDirectoryStream(iface); stream.forEach(file -> { try { Files.delete(file); } catch(IOException ioe) { System.out.println("Error deleteing interface class file: " + file.toString()); ioe.printStackTrace(); System.exit(EXIT_CODE_IO_ERROR); } }); Files.delete(iface); } } /** * Indicates if the zip entry is a compilable file for the codec or interface java classes * * @param zipEntry to inspect * @return true if the file is part of the interfaces or codec package and is a java file. */ public static boolean isCompilable(ZipEntry zipEntry) { String name = zipEntry.getName(); return name.endsWith(".java") && (name.contains("iface") || name.contains("codec")); } /** * Creates a command line classpath of the dependent jar libraries * * @return concatenated string suitable for -d command line option */ public static String getLibraryClassPath() { URL currentURL = Creator.class.getProtectionDomain().getCodeSource().getLocation(); System.out.println("Current URL:" + currentURL.toString()); Path currentPath = null; try { currentPath = new File(currentURL.toURI()).toPath(); } catch(Exception e) { mLog.error("Error discovering current execution path to lookup compile dependencies", e); currentPath = null; } if(currentPath != null && Files.exists(currentPath)) { System.out.println("Discovering: Current Location [" + currentPath.toString() + "]"); Path parent = currentPath.getParent(); System.out.println("Discovering: Compile Dependencies [" + parent.toString() + "]"); StringJoiner joiner = new StringJoiner(String.valueOf(File.pathSeparatorChar)); try { DirectoryStream<Path> stream = Files.newDirectoryStream(parent); stream.forEach(path -> { if(!Files.isDirectory(path) && path.toString().endsWith("jar")) { joiner.add(path.toString()); } }); } catch(IOException ioe) { System.out.println("Failed: Error creating classpath for compile-time libraries - " + ioe.getLocalizedMessage()); ioe.printStackTrace(); System.exit(EXIT_CODE_IO_ERROR); } return joiner.toString(); } return ""; } /** * Compiles the list of java source code files using the specified compile time options * * @param paths of source code files * @param options for compilation (classpath & output directory) */ public static void compile(List<Path> paths, List<String> options) { System.out.println("Compiling: Source Code"); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromPaths(paths); compiler.getTask(null, fileManager, null, options, null, compilationUnits).call(); } /** * Creates a jar from the specified source directory, storing the jar at the specified output file name * * @param source directory containing compiled classes and other jar artifacts * @param output library file path and name * @throws IOException if there is an error */ public static void createJar(Path source, Path output) throws IOException { if(!Files.exists(output.getParent())) { Files.createDirectory(output.getParent()); } ZipUtility zipUtility = new ZipUtility(); List<File> files = new ArrayList<>(); DirectoryStream<Path> stream = Files.newDirectoryStream(source); stream.forEach(path -> { files.add(path.toFile()); }); zipUtility.zip(files, output.toString()); } /** * Creates a jar name for the specified release version * * @param version of the GitHub JMBE release * @return JMBE library jar name */ public static String getJarName(String version) { version = version.replace("v", ""); return "jmbe-" + version + ".jar"; } /** * Creates a JMBE library jar file from the lastest available source code. * * @param libraryPath for the final library * @return an exit code to indicate success or failure */ public static int createLibrary(String libraryPath) { try { System.out.println("Starting: JMBE Library Creator"); Path temporaryDirectory = Files.createTempDirectory("jmbe-creator"); System.out.println("Created: Temporary Directory [" + temporaryDirectory.toString() + "]"); Release latest = GitHub.getLatestRelease(GITHUB_JMBE_RELEASES_URL); if(latest != null) { Path library = null; if(libraryPath == null) { library = Path.of(System.getProperty("user.dir")).getParent().resolve(getJarName(latest.getVersion().toString())); System.out.println("Generated: Library Path [" + library.toString() + "]"); } else { library = Path.of(libraryPath); System.out.println("Specified: Library Path [" + library.toString() + "]"); } System.out.println("Downloading: Source Code [Version " + latest.getVersion().toString() + "]"); Path download = GitHub.downloadReleaseSourceCode(latest, temporaryDirectory); if(download != null) { process(download); System.out.println("Creating: JAR Metadata"); createJarMetadata(getOutputDirectory(temporaryDirectory), latest.getVersion().toString()); System.out.println("Creating: JAR License File"); copyLicenseFile(temporaryDirectory); Path sourceFiles = getOutputDirectory(temporaryDirectory); Path zip = temporaryDirectory.resolve(getJarName(latest.getVersion().toString())); System.out.println("Creating: JMBE Library [" + library.toString() + "]"); createJar(sourceFiles, library); System.out.println("Deleting: Temporary Directory [" + temporaryDirectory.toString() + "]"); try { FileUtils.deleteDirectory(temporaryDirectory.toFile()); } catch(IOException ioe) { System.out.println("Delete: Temporary Directory Failed [" + temporaryDirectory.toString() + "]"); } System.out.println("----------------------------------------------------------------------"); System.out.println("Success: JMBE Library Created At: " + library.toString()); System.out.println("----------------------------------------------------------------------\n"); return EXIT_CODE_SUCCESS; } else { System.out.println("Failed: Couldn't download source code from GitHub. Exiting."); return EXIT_CODE_NETWORK_ERROR; } } else { System.out.println("Failed: Unable to determine the latest JMBE release version from GitHub."); return EXIT_CODE_NETWORK_ERROR; } } catch(IOException ioe) { System.out.println("Failed: Unknown I/O Error " + ioe.getLocalizedMessage()); ioe.printStackTrace(); return EXIT_CODE_IO_ERROR; } catch(Exception e) { System.out.println("Failed: Unknown (General) Error " + e.getLocalizedMessage()); e.printStackTrace(); return EXIT_CODE_UNKNOWN_ERROR; } } public static void main(String[] args) { int status; if(args.length == 0) { status = createLibrary(null); } else if(args.length == 1) { status = createLibrary(args[0]); } else { status = EXIT_CODE_LIBRARY_PATH_REQUIRED; System.out.println("Usage: Creator (optional full path and library name)"); } System.exit(status); } }