org.codehaus.mojo.osxappbundle.CreateApplicationBundleMojo.java Source code

Java tutorial

Introduction

Here is the source code for org.codehaus.mojo.osxappbundle.CreateApplicationBundleMojo.java

Source

package org.codehaus.mojo.osxappbundle;

/*
 * Copyright 2001-2008 The Codehaus. 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.
 */

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.codehaus.mojo.osxappbundle.encoding.DefaultEncodingDetector;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.zip.ZipArchiver;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.velocity.VelocityComponent;

/**
 * Package dependencies as an Application Bundle for Mac OS X.
 * 
 * @goal bundle
 * @phase package
 * @requiresDependencyResolution runtime
 */
public class CreateApplicationBundleMojo extends AbstractMojo {

    /**
     * Default includes - everything is included.
     */
    private static final String[] DEFAULT_INCLUDES = { "**/**" };

    /**
     * The Maven Project Object
     * 
     * @parameter default-value="${project}"
     * @readonly
     */
    private MavenProject project;

    /**
     * The directory where the application bundle will be created
     * 
     * @parameter default-value="${project.build.directory}/${project.build.finalName}";
     */
    private File buildDirectory;

    /**
     * The location of the generated disk image file
     * 
     * @parameter default-value="${project.build.directory}/${project.build.finalName}.dmg"
     */
    private File diskImageFile;

    /**
     * The location of the Java Application Stub
     * 
     * @parameter default-value="/System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub";
     */
    private File javaApplicationStub;

    /**
     * The main class to execute when double-clicking the Application Bundle
     * 
     * @parameter expression="${mainClass}"
     * @required
     */
    private String mainClass;

    /**
     * The name of the Bundle. This is the name that is given to the application bundle; and it is also what will show up in the application menu, dock etc.
     * 
     * @parameter default-value="${project.name}"
     * @required
     */
    private String bundleName;

    /**
     * The bundle identifier. This is a name used by codesign to verify the bundle
     * 
     * @parameter
     */
    private String bundleIdentifier;

    /**
     * The icon file for the bundle
     * 
     * @parameter
     */
    private File iconFile;

    /**
     * The version of the project. Will be used as the value of the CFBundleVersion key.
     * 
     * @parameter default-value="${project.version}"
     */
    private String version;

    /**
     * A value for the JVMVersion key.
     * 
     * @parameter default-value="1.4+"
     */
    private String jvmVersion;

    /**
     * The location of the produced Zip file containing the bundle.
     * 
     * @parameter default-value="${project.build.directory}/${project.build.finalName}-app.zip"
     */
    private File zipFile;

    /**
     * Paths to be put on the classpath in addition to the projects dependencies. Might be useful to specifiy locations of dependencies in the provided scope
     * that are not distributed with the bundle but have a known location on the system. {@see http://jira.codehaus.org/browse/MOJO-874}
     * 
     * @parameter
     */
    private List additionalClasspath;

    /**
     * Additional resources (as a list of FileSet objects) that will be copies into the build directory and included in the .dmg and zip files alongside with
     * the application bundle.
     * 
     * @parameter
     */
    private List additionalResources;

    /**
     * Velocity Component.
     * 
     * @component
     * @readonly
     */
    private VelocityComponent velocity;

    /**
     * The location of the template for Info.plist. Classpath is checked before the file system.
     * 
     * @parameter default-value="org/codehaus/mojo/osxappbundle/Info.plist.template"
     */
    private String dictionaryFile;

    /**
     * Options to the JVM, will be used as the value of VMOptions in Info.plist.
     * 
     * @parameter
     */
    private String vmOptions;

    /**
     * The Zip archiver.
     * 
     * @component
     * @readonly
     */
    private MavenProjectHelper projectHelper;

    /**
     * The Zip archiver.
     * 
     * @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#zip}"
     * @required
     * @readonly
     */
    private ZipArchiver zipArchiver;

    /**
     * If this is set to <code>true</code>, the generated DMG file will be internet-enabled. The default is ${false}
     * 
     * @parameter default-value="false"
     */
    private boolean internetEnable;

    /**
     * Name of the keychain to use for code signing
     * 
     * @parameter
     */
    private String codesignKeychain = "";

    /**
     * Name of the identity in the selected keychain
     * 
     * @parameter
     */
    private String codesignIdentity = "";

    /**
     * Alternate identifier to sign the app, should resemble the CFBundleIdentifier from Info.plist
     * 
     * @parameter
     */
    private String codesignIdentifier = "";

    /**
     * three strings have to be the same, or codesign will fail with exitcode 1:
     * <ol>
     * <li>CFBundleExecutable in Info.plist</li>
     * <li>executable in /Contents/MacOS/</li>
     * <li>the application bundle name</li>
     * </ol>
     * this flag has to be set to false if using codesign. Defaults to true to preserve behaviour from before the fork.
     * 
     * @parameter default-value=true
     */
    private boolean keepJavaApplicationStubName;

    /**
     * The path to the SetFile tool.
     * 
     * 
     */
    private static final String SET_FILE_PATH = "/usr/bin/SetFile";

    /**
     * Bundle project as a Mac OS X application bundle.
     * 
     * @throws MojoExecutionException
     *             If an unexpected error occurs during packaging of the bundle.
     */
    public void execute() throws MojoExecutionException {

        // Set up and create directories
        buildDirectory.mkdirs();

        File bundleDir = new File(buildDirectory, bundleName + ".app");
        bundleDir.mkdirs();

        File contentsDir = new File(bundleDir, "Contents");
        contentsDir.mkdirs();

        File resourcesDir = new File(contentsDir, "Resources");
        resourcesDir.mkdirs();

        File javaDirectory = new File(resourcesDir, "Java");
        javaDirectory.mkdirs();

        File macOSDirectory = new File(contentsDir, "MacOS");
        macOSDirectory.mkdirs();

        // Copy in the native java application stub
        File stub = new File(macOSDirectory,
                keepJavaApplicationStubName ? javaApplicationStub.getName() : bundleName);
        if (!javaApplicationStub.exists()) {
            String message = "Can't find JavaApplicationStub binary. File does not exist: " + javaApplicationStub;

            if (!isOsX()) {
                message += "\nNOTICE: You are running the osxappbundle plugin on a non OS X platform. To make this work you need to copy the JavaApplicationStub binary into your source tree. Then configure it with the 'javaApplicationStub' configuration property.\nOn an OS X machine, the JavaApplicationStub is typically located under /System/Library/Frameworks/JavaVM.framework/Versions/Current/Resources/MacOS/JavaApplicationStub";
            }

            throw new MojoExecutionException(message);

        } else {
            try {
                FileUtils.copyFile(javaApplicationStub, stub);
            } catch (IOException e) {
                throw new MojoExecutionException(
                        "Could not copy file " + javaApplicationStub + " to directory " + macOSDirectory, e);
            }
        }

        // Copy icon file to the bundle if specified
        if (iconFile != null) {
            try {
                FileUtils.copyFileToDirectory(iconFile, resourcesDir);
            } catch (IOException e) {
                throw new MojoExecutionException("Error copying file " + iconFile + " to " + resourcesDir, e);
            }
        }

        // Resolve and copy in all dependecies from the pom
        List files = copyDependencies(javaDirectory);

        // Create and write the Info.plist file
        File infoPlist = new File(bundleDir, "Contents/Info.plist");
        writeInfoPlist(infoPlist, files);

        // Copy specified additional resources into the top level directory
        if (additionalResources != null && !additionalResources.isEmpty()) {
            copyResources(additionalResources);
        }

        if (isOsX()) {
            // Make the stub executable
            Commandline chmod = new Commandline();
            try {
                chmod.setExecutable("chmod");
                chmod.createArg().setValue("755");
                chmod.createArg().setValue(stub.getAbsolutePath());

                chmod.execute();
            } catch (CommandLineException e) {
                throw new MojoExecutionException("Error executing " + chmod + " ", e);
            }

            // This makes sure that the .app dir is actually registered as an application bundle
            if (new File(SET_FILE_PATH).exists()) {
                Commandline setFile = new Commandline();
                try {
                    setFile.setExecutable(SET_FILE_PATH);
                    setFile.createArg().setValue("-a B");
                    setFile.createArg().setValue(bundleDir.getAbsolutePath());

                    getLog().info("executing " + setFile.toString());
                    setFile.execute();
                } catch (CommandLineException e) {
                    throw new MojoExecutionException("Error executing " + setFile, e);
                }
            } else {
                getLog().warn("Could  not set 'Has Bundle' attribute. " + SET_FILE_PATH
                        + " not found, is Developer Tools installed?");
            }
            // sign the code (if set up)

            if (codesignIdentity.length() > 0 && !keepJavaApplicationStubName) {
                Commandline codesign = new Commandline();
                try {
                    codesign.setExecutable("codesign");
                    codesign.createArg().setValue("-s");
                    codesign.createArg().setValue(codesignIdentity);
                    if (codesignIdentifier.length() > 0) {
                        codesign.createArg().setValue("-i");
                        codesign.createArg().setValue(codesignIdentifier);
                    }
                    codesign.createArg().setValue("-f");
                    codesign.createArg().setValue("-vvvv");

                    if (codesignKeychain.length() > 0) {
                        codesign.createArg().setValue("--keychain");
                        codesign.createArg().setValue(codesignKeychain);
                    }

                    // need to escape spaces
                    codesign.createArg().setValue(bundleDir.getAbsolutePath().replaceAll(" ", "\\ "));

                    getLog().info("executing " + codesign.toString());
                    Process process = codesign.execute();

                    process.waitFor();

                    int result = process.exitValue();
                    if (result == 0) {
                        getLog().info("codesign completed successfully");
                    } else {

                        StringBuffer buffer = new StringBuffer();
                        buffer.append("codesign failed with exit code: ");
                        buffer.append(result);
                        buffer.append("\n");
                        if (getLog().isDebugEnabled()) {
                            buffer.append(
                                    "Verify that the CFBundleExecutable and other Info.plits properties are correct, also check the availability of your certificates in the keychains.\n");
                        } else {
                            buffer.append("retry with 'mvn -X' to get more info");
                        }
                        buffer.append("Error message: ");
                        buffer.append(IOUtils.toString(process.getErrorStream()));
                        if (getLog().isDebugEnabled()) {
                            Commandline debug = new Commandline();
                            debug.setExecutable("security");
                            debug.createArg().setValue("list-keychains");

                            Process process2 = debug.execute();
                            process.waitFor();
                            buffer.append("\nSearched keychains:\n");
                            buffer.append(IOUtils.toString(process2.getInputStream()));
                        }

                        getLog().warn(buffer.toString());

                    }
                } catch (CommandLineException e) {
                    throw new MojoExecutionException("Error signing the application " + bundleDir.getAbsolutePath()
                            + " with keychain/identity " + codesignKeychain + "/" + codesignIdentity, e);
                } catch (IOException e) {
                    throw new MojoExecutionException("blah", e);
                } catch (InterruptedException e) {
                    getLog().warn("codesign failed, process interrupted", e);
                }

            }

            // Create a .dmg file of the app
            Commandline dmg = new Commandline();
            try {
                dmg.setExecutable("hdiutil");
                dmg.createArg().setValue("create");
                dmg.createArg().setValue("-srcfolder");
                dmg.createArg().setValue(buildDirectory.getAbsolutePath());
                dmg.createArg().setValue(diskImageFile.getAbsolutePath());
                try {
                    dmg.execute().waitFor();
                } catch (InterruptedException e) {
                    throw new MojoExecutionException("Thread was interrupted while creating DMG " + diskImageFile,
                            e);
                }
            } catch (CommandLineException e) {
                throw new MojoExecutionException("Error creating disk image " + diskImageFile, e);
            }
            if (internetEnable) {
                try {

                    Commandline internetEnable = new Commandline();

                    internetEnable.setExecutable("hdiutil");
                    internetEnable.createArg().setValue("internet-enable");
                    internetEnable.createArg().setValue("-yes");
                    internetEnable.createArg().setValue(diskImageFile.getAbsolutePath());

                    internetEnable.execute();
                } catch (CommandLineException e) {
                    throw new MojoExecutionException("Error internet enabling disk image: " + diskImageFile, e);
                }
            }
            projectHelper.attachArtifact(project, "dmg", null, diskImageFile);
        }

        zipArchiver.setDestFile(zipFile);
        try {
            String[] stubPattern = { buildDirectory.getName() + "/" + bundleDir.getName() + "/Contents/MacOS/"
                    + (keepJavaApplicationStubName ? javaApplicationStub.getName() : bundleName) };

            zipArchiver.addDirectory(buildDirectory.getParentFile(),
                    new String[] { buildDirectory.getName() + "/**" }, stubPattern);

            DirectoryScanner scanner = new DirectoryScanner();
            scanner.setBasedir(buildDirectory.getParentFile());
            scanner.setIncludes(stubPattern);
            scanner.scan();

            String[] stubs = scanner.getIncludedFiles();
            for (int i = 0; i < stubs.length; i++) {
                String s = stubs[i];
                zipArchiver.addFile(new File(buildDirectory.getParentFile(), s), s, 0755);
            }

            zipArchiver.createArchive();
            projectHelper.attachArtifact(project, "zip", null, zipFile);
        } catch (ArchiverException e) {
            throw new MojoExecutionException("Could not create zip archive of application bundle in " + zipFile, e);
        } catch (IOException e) {
            throw new MojoExecutionException("IOException creating zip archive of application bundle in " + zipFile,
                    e);
        }

    }

    private boolean isOsX() {
        return System.getProperty("mrj.version") != null;
    }

    /**
     * Copy all dependencies into the $JAVAROOT directory
     * 
     * @param javaDirectory
     *            where to put jar files
     * @return A list of file names added
     * @throws MojoExecutionException
     */
    private List copyDependencies(File javaDirectory) throws MojoExecutionException {

        ArtifactRepositoryLayout layout = new DefaultRepositoryLayout();

        List list = new ArrayList();

        File repoDirectory = new File(javaDirectory, "repo");
        repoDirectory.mkdirs();

        // First, copy the project's own artifact
        File artifactFile = project.getArtifact().getFile();
        list.add(repoDirectory.getName() + "/" + layout.pathOf(project.getArtifact()));

        try {
            FileUtils.copyFile(artifactFile, new File(repoDirectory, layout.pathOf(project.getArtifact())));
        } catch (IOException e) {
            throw new MojoExecutionException(
                    "Could not copy artifact file " + artifactFile + " to " + javaDirectory);
        }

        Set artifacts = project.getArtifacts();

        Iterator i = artifacts.iterator();

        while (i.hasNext()) {
            Artifact artifact = (Artifact) i.next();

            File file = artifact.getFile();
            File dest = new File(repoDirectory, layout.pathOf(artifact));

            getLog().debug("Adding " + file);

            try {
                FileUtils.copyFile(file, dest);
            } catch (IOException e) {
                throw new MojoExecutionException("Error copying file " + file + " into " + javaDirectory, e);
            }

            list.add(repoDirectory.getName() + "/" + layout.pathOf(artifact));
        }

        return list;

    }

    /**
     * Writes an Info.plist file describing this bundle.
     * 
     * @param infoPlist
     *            The file to write Info.plist contents to
     * @param files
     *            A list of file names of the jar files to add in $JAVAROOT
     * @throws MojoExecutionException
     */
    private void writeInfoPlist(File infoPlist, List files) throws MojoExecutionException {

        VelocityContext velocityContext = new VelocityContext();

        velocityContext.put("mainClass", mainClass);
        if (keepJavaApplicationStubName) {
            velocityContext.put("cfBundleExecutable", javaApplicationStub.getName());
        } else {
            velocityContext.put("cfBundleExecutable", bundleName);
        }

        velocityContext.put("bundleIdentifier", bundleIdentifier);
        velocityContext.put("vmOptions", vmOptions);
        velocityContext.put("bundleName", bundleName);

        velocityContext.put("iconFile", iconFile == null ? "GenericJavaApp.icns" : iconFile.getName());

        velocityContext.put("version", version);

        velocityContext.put("jvmVersion", jvmVersion);

        StringBuffer jarFilesBuffer = new StringBuffer();

        jarFilesBuffer.append("<array>");
        for (int i = 0; i < files.size(); i++) {
            String name = (String) files.get(i);
            jarFilesBuffer.append("<string>");
            jarFilesBuffer.append("$JAVAROOT/").append(name);
            jarFilesBuffer.append("</string>");

        }
        if (additionalClasspath != null) {
            for (int i = 0; i < additionalClasspath.size(); i++) {
                String pathElement = (String) additionalClasspath.get(i);
                jarFilesBuffer.append("<string>");
                jarFilesBuffer.append(pathElement);
                jarFilesBuffer.append("</string>");

            }
        }
        jarFilesBuffer.append("</array>");

        velocityContext.put("classpath", jarFilesBuffer.toString());

        try {

            String encoding = detectEncoding(dictionaryFile, velocityContext);

            getLog().debug("Detected encoding " + encoding + " for dictionary file " + dictionaryFile);

            Writer writer = new OutputStreamWriter(new FileOutputStream(infoPlist), encoding);

            velocity.getEngine().mergeTemplate(dictionaryFile, encoding, velocityContext, writer);

            writer.close();
        } catch (IOException e) {
            throw new MojoExecutionException("Could not write Info.plist to file " + infoPlist, e);
        } catch (ParseErrorException e) {
            throw new MojoExecutionException("Error parsing " + dictionaryFile, e);
        } catch (ResourceNotFoundException e) {
            throw new MojoExecutionException("Could not find resource for template " + dictionaryFile, e);
        } catch (MethodInvocationException e) {
            throw new MojoExecutionException(
                    "MethodInvocationException occured merging Info.plist template " + dictionaryFile, e);
        } catch (Exception e) {
            throw new MojoExecutionException("Exception occured merging Info.plist template " + dictionaryFile, e);
        }

    }

    private String detectEncoding(String dictionaryFile, VelocityContext velocityContext) throws Exception {
        StringWriter sw = new StringWriter();
        velocity.getEngine().mergeTemplate(dictionaryFile, "utf-8", velocityContext, sw);
        return new DefaultEncodingDetector()
                .detectXmlEncoding(new ByteArrayInputStream(sw.toString().getBytes("utf-8")));
    }

    /**
     * Copies given resources to the build directory.
     * 
     * @param fileSets
     *            A list of FileSet objects that represent additional resources to copy.
     * @throws MojoExecutionException
     *             In case af a resource copying error.
     */
    private void copyResources(List fileSets) throws MojoExecutionException {
        final String[] emptyStrArray = {};

        for (Iterator it = fileSets.iterator(); it.hasNext();) {
            FileSet fileSet = (FileSet) it.next();

            File resourceDirectory = new File(fileSet.getDirectory());
            if (!resourceDirectory.isAbsolute()) {
                resourceDirectory = new File(project.getBasedir(), resourceDirectory.getPath());
            }

            if (!resourceDirectory.exists()) {
                getLog().info("Additional resource directory does not exist: " + resourceDirectory);
                continue;
            }

            DirectoryScanner scanner = new DirectoryScanner();

            scanner.setBasedir(resourceDirectory);
            if (fileSet.getIncludes() != null && !fileSet.getIncludes().isEmpty()) {
                scanner.setIncludes((String[]) fileSet.getIncludes().toArray(emptyStrArray));
            } else {
                scanner.setIncludes(DEFAULT_INCLUDES);
            }

            if (fileSet.getExcludes() != null && !fileSet.getExcludes().isEmpty()) {
                scanner.setExcludes((String[]) fileSet.getExcludes().toArray(emptyStrArray));
            }

            if (fileSet.isUseDefaultExcludes()) {
                scanner.addDefaultExcludes();
            }

            scanner.scan();

            List includedFiles = Arrays.asList(scanner.getIncludedFiles());

            getLog().info("Copying " + includedFiles.size() + " additional resource"
                    + (includedFiles.size() > 1 ? "s" : ""));

            for (Iterator j = includedFiles.iterator(); j.hasNext();) {
                String destination = (String) j.next();
                File source = new File(resourceDirectory, destination);
                File destinationFile = new File(buildDirectory, destination);

                if (!destinationFile.getParentFile().exists()) {
                    destinationFile.getParentFile().mkdirs();
                }

                try {
                    FileUtils.copyFile(source, destinationFile);
                } catch (IOException e) {
                    throw new MojoExecutionException("Error copying additional resource " + source, e);
                }
            }
        }
    }

}