All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.zenjava.javafx.maven.plugin.MacAppBundlerWithAdditionalResources Maven / Gradle / Ivy

Go to download

The JavaFX Maven Plugin provides a way to to assemble distributable bundles for JavaFX applications from within Maven. It provides a wrapper around the JavaFX packaging tools which are provided as part of the JavaFX installation.

There is a newer version: 8.8.3
Show newest version
/*
 * Copyright 2012 Daniel Zwolenski.
 *
 * 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.zenjava.javafx.maven.plugin;

import com.oracle.tools.packager.BundlerParamInfo;
import com.oracle.tools.packager.IOUtils;
import com.oracle.tools.packager.Log;
import com.oracle.tools.packager.StandardBundlerParam;
import static com.oracle.tools.packager.StandardBundlerParam.APP_NAME;
import static com.oracle.tools.packager.StandardBundlerParam.BUILD_ROOT;
import static com.oracle.tools.packager.StandardBundlerParam.VERBOSE;
import com.oracle.tools.packager.mac.MacAppBundler;
import com.oracle.tools.packager.mac.MacBaseInstallerBundler;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This bundler is the workaround for mac bundlers not having the option to provide additional files
 * being used while bundling.
 *
 * If there should be any legal issue about this, please dear lawyers, contant me first.
 * I am expecting this to be allowed 'cause of the classpath-exception of the used GPLv2!
 *
 * @author Danny Althoff
 */
public class MacAppBundlerWithAdditionalResources extends MacAppBundler {

    private static final ResourceBundle I18N = ResourceBundle.getBundle(MacAppBundler.class.getName());
    private static final String LIBRARY_NAME = "libpackager.dylib";
    private static final Class ORIGINAL_MAC_APP_BUNDLER_CLASS = MacAppBundler.class;
    public static final BundlerParamInfo ADDITIONAL_BUNDLER_RESOURCES = new StandardBundlerParam<>(
            "additional bundler resources",
            "Field for providing additional resources that will be put into generation-folder.",
            "mac.app.additionalBundlerResources",
            File.class,
            params -> null,
            (s, p) -> new File(s));

    /**
     * Code mostly used from the existing bundler, but not the same ;)
     *
     * @param p
     * @param outputDirectory
     * @param dependentTask
     *
     * @return
     */
    @Override
    public File doBundle(Map p, File outputDirectory, boolean dependentTask) {
        // use original stuff as much as possible
        File additionalBundlerResources = ADDITIONAL_BUNDLER_RESOURCES.fetchFrom(p);
        if( additionalBundlerResources == null ){
            return super.doBundle(p, outputDirectory, dependentTask);
        }

        Log.info("Using special javafx-maven-plugin MacAppBundler");

        // if special additional bundler resources are provided, we need to do magic here !
        Map originalParams = new HashMap<>(p);
        doOutputFolderChecks(outputDirectory);

        File rootDirectory = null;
        try{
            final File predefinedImage = MacBaseInstallerBundler.getPredefinedImage(p);
            if( predefinedImage != null ){
                return predefinedImage;
            }

            BUILD_ROOT.fetchFrom(p);
            prepareConfigFiles(p);
            rootDirectory = new File(outputDirectory, APP_NAME.fetchFrom(p) + ".app");
            // this is the root of evil, because we can't just "pre-copy" all additional files needed
            IOUtils.deleteRecursive(rootDirectory);
            // recreate
            rootDirectory.mkdirs();

            // this mac app bundler gets called by other mac installer bundlers
            if( !dependentTask ){
                Log.info(MessageFormat.format(I18N.getString("message.creating-app-bundle"), rootDirectory.getAbsolutePath()));
            }

            File contentsDirectory = new File(rootDirectory, "Contents");
            contentsDirectory.mkdirs();
            File macOSDirectory = new File(contentsDirectory, "MacOS");
            macOSDirectory.mkdirs();
            File javaDirectory = new File(contentsDirectory, "Java");
            javaDirectory.mkdirs();
            File plugInsDirectory = new File(contentsDirectory, "PlugIns");
            File resourcesDirectory = new File(contentsDirectory, "Resources");
            resourcesDirectory.mkdirs();

            File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
            pkgInfoFile.createNewFile();
            writePkgInfo(pkgInfoFile);

            File executableFile = new File(macOSDirectory, getLauncherName(p));
            IOUtils.copyFromURL(RAW_EXECUTABLE_URL.fetchFrom(p), executableFile);

            if( JavaDetectionTools.IS_JAVA_8 && JavaDetectionTools.isAtLeastOracleJavaUpdateVersion(40) ){
                // use FQN for not having incompatible import
                IOUtils.copyFromURL(com.oracle.tools.packager.mac.MacResources.class.getResource(LIBRARY_NAME), new File(macOSDirectory, LIBRARY_NAME));

                if( JavaDetectionTools.isAtLeastOracleJavaUpdateVersion(60) ){
                    if( !MAC_CONFIGURE_LAUNCHER_IN_PLIST.fetchFrom(p) ){
                        if( LAUNCHER_CFG_FORMAT.fetchFrom(p).equals(CFG_FORMAT_PROPERTIES) ){
                            writeCfgFile(p, rootDirectory);
                        } else {
                            writeCfgFile(p, new File(rootDirectory, getLauncherCfgName(p)), getRuntimeLocation(p));
                        }
                    }
                }
            }

            executableFile.setExecutable(true, false);

            copyRuntime(plugInsDirectory, p);
            copyClassPathEntries(javaDirectory, p);
            IOUtils.copyFile(getConfig_Icon(p), new File(resourcesDirectory, getConfig_Icon(p).getName()));

            if( JavaDetectionTools.IS_JAVA_8 && JavaDetectionTools.isAtLeastOracleJavaUpdateVersion(60) ){
                for( Map fa : com.oracle.tools.packager.StandardBundlerParam.FILE_ASSOCIATIONS.fetchFrom(p) ){
                    File f = com.oracle.tools.packager.StandardBundlerParam.FA_ICON.fetchFrom(fa);
                    if( f != null && f.exists() ){
                        IOUtils.copyFile(f, new File(resourcesDirectory, f.getName()));
                    }
                }
            }

            IOUtils.copyFile(getConfig_InfoPlist(p), new File(contentsDirectory, "Info.plist"));

            if( JavaDetectionTools.IS_JAVA_8 && JavaDetectionTools.isAtLeastOracleJavaUpdateVersion(60) ){
                for( Map entryPoint : StandardBundlerParam.SECONDARY_LAUNCHERS.fetchFrom(p) ){
                    Map tmp = new HashMap<>(originalParams);
                    tmp.putAll(entryPoint);
                    createLauncherForEntryPoint(tmp, rootDirectory);
                }
            }

            Log.info("Copying additional bundler resources...");

            Path sourceFolder = additionalBundlerResources.toPath();
            Path targetFolder = rootDirectory.toPath();
            AtomicReference copyingException = new AtomicReference<>(null);

            AtomicInteger copiedFiles = new AtomicInteger(0);

            Files.walkFileTree(sourceFolder, new FileVisitor() {

                @Override
                public FileVisitResult preVisitDirectory(Path subfolder, BasicFileAttributes attrs) throws IOException {
                    // do create subfolder (if needed)
                    Files.createDirectories(targetFolder.resolve(sourceFolder.relativize(subfolder)));
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException {
                    // do copy, and replace, as the resource might already be existing
                    Files.copy(sourceFile, targetFolder.resolve(sourceFolder.relativize(sourceFile)), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
                    copiedFiles.incrementAndGet();
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path source, IOException ioe) throws IOException {
                    // don't fail, just inform user
                    copyingException.set(ioe);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path source, IOException ioe) throws IOException {
                    // nothing to do here
                    return FileVisitResult.CONTINUE;
                }
            });

            if( copyingException.get() != null ){
                throw new RuntimeException("Got exception while copying additional bundler resources", copyingException.get());
            }

            Log.info("Copied additional bundler resources count: " + copiedFiles.get());

            String signingIdentity = DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(p);
            if( signingIdentity != null ){
                if( JavaDetectionTools.IS_JAVA_8 && JavaDetectionTools.isAtLeastOracleJavaUpdateVersion(40) ){
                    // update 40 seems to have made this optional ;)
                    // use FQN for not having incompatible import
                    if( Optional.ofNullable(com.oracle.tools.packager.StandardBundlerParam.SIGN_BUNDLE.fetchFrom(p)).orElse(Boolean.TRUE) ){
                        MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p));
                    }
                } else {
                    MacBaseInstallerBundler.signAppBundle(p, rootDirectory, signingIdentity, BUNDLE_ID_SIGNING_PREFIX.fetchFrom(p));
                }
            }
        } catch(IOException ex){
            Log.info(ex.toString());
            Log.verbose(ex);
            return null;
        } finally{
            if( !VERBOSE.fetchFrom(p) ){
                cleanupConfigFiles(p);
            } else {
                Log.info(MessageFormat.format(I18N.getString("message.config-save-location"), CONFIG_ROOT.fetchFrom(p).getAbsolutePath()));
            }
        }
        return rootDirectory;
    }

    private void doOutputFolderChecks(File outputDirectory) {
        if( !outputDirectory.isDirectory() && !outputDirectory.mkdirs() ){
            throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-create-output-dir"), outputDirectory.getAbsolutePath()));
        }
        if( !outputDirectory.canWrite() ){
            throw new RuntimeException(MessageFormat.format(I18N.getString("error.cannot-write-to-output-dir"), outputDirectory.getAbsolutePath()));
        }
    }

    private void prepareConfigFiles(Map params) throws IOException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("prepareConfigFiles", Map.class);
            method.setAccessible(true);
            method.invoke(this, params);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof IOException ){
                    throw (IOException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private void writeCfgFile(Map params, File rootDir) throws FileNotFoundException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("writeCfgFile", Map.class, File.class);
            method.setAccessible(true);
            method.invoke(this, params, rootDir);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof FileNotFoundException ){
                    throw (FileNotFoundException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private void writePkgInfo(File file) throws IOException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("writePkgInfo", File.class);
            method.setAccessible(true);
            method.invoke(this, file);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof IOException ){
                    throw (IOException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private void copyRuntime(File plugInsDirectory, Map params) throws IOException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("copyRuntime", File.class, Map.class);
            method.setAccessible(true);
            method.invoke(this, plugInsDirectory, params);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof IOException ){
                    throw (IOException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private void copyClassPathEntries(File javaDirectory, Map params) throws IOException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("copyClassPathEntries", File.class, Map.class);
            method.setAccessible(true);
            method.invoke(this, javaDirectory, params);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof IOException ){
                    throw (IOException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private File getConfig_Icon(Map params) {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("getConfig_Icon", Map.class);
            method.setAccessible(true);
            Object invokationResult = method.invoke(this, params);
            if( invokationResult instanceof File ){
                return (File) invokationResult;
            }
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            if( ex instanceof RuntimeException ){
                throw (RuntimeException) ex;
            }
        }
        return null;
    }

    private String getRuntimeLocation(Map params) {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("getRuntimeLocation", Map.class);
            method.setAccessible(true);
            Object invokationResult = method.invoke(this, params);
            if( invokationResult instanceof String ){
                return (String) invokationResult;
            }
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            if( ex instanceof RuntimeException ){
                throw (RuntimeException) ex;
            }
        }
        return null;
    }

    private File getConfig_InfoPlist(Map params) {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("getConfig_InfoPlist", Map.class);
            method.setAccessible(true);
            Object invokationResult = method.invoke(this, params);
            if( invokationResult instanceof File ){
                return (File) invokationResult;
            }
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            if( ex instanceof RuntimeException ){
                throw (RuntimeException) ex;
            }
        }
        return null;
    }

    private void createLauncherForEntryPoint(Map p, File rootDirectory) throws IOException {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("createLauncherForEntryPoint", Map.class, File.class);
            method.setAccessible(true);
            method.invoke(this, p, rootDirectory);
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            // if this does not work, explode
            if( ex instanceof InvocationTargetException ){
                Throwable cause = ((InvocationTargetException) ex).getCause();
                if( cause instanceof IOException ){
                    throw (IOException) cause;
                }
                if( cause instanceof RuntimeException ){
                    throw (RuntimeException) cause;
                }
            }
        }
    }

    private String getLauncherName(Map params) {
        // call using reflection, because this method is "private"
        try{
            Method method = ORIGINAL_MAC_APP_BUNDLER_CLASS.getDeclaredMethod("getLauncherName", Map.class);
            method.setAccessible(true);
            Object invokationResult = method.invoke(this, params);
            if( invokationResult instanceof String ){
                return (String) invokationResult;
            }
        } catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex){
            if( ex instanceof RuntimeException ){
                throw (RuntimeException) ex;
            }
        }
        return null;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy