1 /*
   2  * Copyright 2012, Oracle and/or its affiliates. All rights reserved.
   3  *
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * This code is free software; you can redistribute it and/or modify it
   7  * under the terms of the GNU General Public License version 2 only, as
   8  * published by the Free Software Foundation.  Oracle designates this
   9  * particular file as subject to the "Classpath" exception as provided
  10  * by Oracle in the LICENSE file that accompanied this code.
  11  *
  12  * This code is distributed in the hope that it will be useful, but WITHOUT
  13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  15  * version 2 for more details (a copy is included in the LICENSE file that
  16  * accompanied this code).
  17  *
  18  * You should have received a copy of the GNU General Public License version
  19  * 2 along with this work; if not, write to the Free Software Foundation,
  20  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  21  *
  22  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  23  * or visit www.oracle.com if you need additional information or have any
  24  * questions.
  25  */
  26 
  27 package com.oracle.appbundler;
  28 
  29 import java.io.BufferedWriter;
  30 import java.io.File;
  31 import java.io.FileWriter;
  32 import java.io.IOException;
  33 import java.io.InputStream;
  34 import java.io.Writer;
  35 import java.net.URL;
  36 import java.nio.file.Files;
  37 import java.nio.file.LinkOption;
  38 import java.nio.file.Path;
  39 import java.nio.file.StandardCopyOption;
  40 import java.util.ArrayList;
  41 
  42 import javax.xml.stream.XMLOutputFactory;
  43 import javax.xml.stream.XMLStreamException;
  44 import javax.xml.stream.XMLStreamWriter;
  45 
  46 import org.apache.tools.ant.BuildException;
  47 import org.apache.tools.ant.DirectoryScanner;
  48 import org.apache.tools.ant.Task;
  49 import org.apache.tools.ant.types.FileSet;
  50 
  51 /**
  52  * App bundler Ant task.
  53  */
  54 public class AppBundlerTask extends Task {
  55     // Output folder for generated bundle
  56     private File outputDirectory = null;
  57 
  58     // General bundle properties
  59     private String name = null;
  60     private String displayName = null;
  61     private String identifier = null;
  62     private File icon = null;
  63 
  64     private String shortVersion = "1.0";
  65     private String signature = "????";
  66     private String copyright = "";
  67 
  68     // JVM info properties
  69     private File runtime = null;
  70     private String mainClassName = null;
  71     private ArrayList<File> classPath = new ArrayList<>();
  72     private ArrayList<File> nativeLibraries = new ArrayList<>();
  73     private ArrayList<String> options = new ArrayList<>();
  74     private ArrayList<String> arguments = new ArrayList<>();
  75 
  76     public static final String EXECUTABLE_NAME = "JavaAppLauncher";
  77     public static final String DEFAULT_ICON_NAME = "GenericApp.icns";
  78     public static final String OS_TYPE_CODE = "APPL";
  79     public static final String CLASS_EXTENSION = ".class";
  80 
  81     public static final String PLIST_DTD = "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";
  82     public static final String PLIST_TAG = "plist";
  83     public static final String PLIST_VERSION_ATTRIBUTE = "version";
  84     public static final String DICT_TAG = "dict";
  85     public static final String KEY_TAG = "key";
  86     public static final String ARRAY_TAG = "array";
  87     public static final String STRING_TAG = "string";
  88 
  89     public static final int BUFFER_SIZE = 1024;
  90 
  91     public void setOutputDirectory(File outputDirectory) {
  92         this.outputDirectory = outputDirectory;
  93     }
  94 
  95     public void setName(String name) {
  96         this.name = name;
  97     }
  98 
  99     public void setDisplayName(String displayName) {
 100         this.displayName = displayName;
 101     }
 102 
 103     public void setIdentifier(String identifier) {
 104         this.identifier = identifier;
 105     }
 106 
 107     public void setIcon(File icon) {
 108         this.icon = icon;
 109     }
 110 
 111     public void setShortVersion(String shortVersion) {
 112         this.shortVersion = shortVersion;
 113     }
 114 
 115     public void setSignature(String signature) {
 116         this.signature = signature;
 117     }
 118 
 119     public void setCopyright(String copyright) {
 120         this.copyright = copyright;
 121     }
 122 
 123     public File getRuntime() {
 124         return runtime;
 125     }
 126 
 127     public void setRuntime(File runtime) {
 128         this.runtime = runtime;
 129     }
 130 
 131     public void setMainClassName(String mainClassName) {
 132         this.mainClassName = mainClassName;
 133     }
 134 
 135     public void addConfiguredClassPath(FileSet classPath) {
 136         File parent = classPath.getDir();
 137 
 138         DirectoryScanner directoryScanner = classPath.getDirectoryScanner(getProject());
 139         String[] includedFiles = directoryScanner.getIncludedFiles();
 140 
 141         for (int i = 0; i < includedFiles.length; i++) {
 142             this.classPath.add(new File(parent, includedFiles[i]));
 143         }
 144     }
 145 
 146     public void addNativeLibrary(File nativeLibrary) throws BuildException {
 147         if (nativeLibrary.isDirectory()) {
 148             throw new BuildException("Native library cannot be a directory.");
 149         }
 150 
 151         nativeLibraries.add(nativeLibrary);
 152     }
 153 
 154     public void addConfiguredOption(Option option) throws BuildException {
 155         String value = option.getValue();
 156 
 157         if (value == null) {
 158             throw new BuildException("Value is required.");
 159         }
 160 
 161         options.add(value);
 162     }
 163 
 164     public void addConfiguredArgument(Argument argument) throws BuildException {
 165         String value = argument.getValue();
 166 
 167         if (value == null) {
 168             throw new BuildException("Value is required.");
 169         }
 170 
 171         arguments.add(value);
 172     }
 173 
 174     @Override
 175     public void execute() throws BuildException {
 176         // Validate required properties
 177         if (outputDirectory == null) {
 178             throw new IllegalStateException("Destination directory is required.");
 179         }
 180 
 181         if (!outputDirectory.exists()) {
 182             throw new IllegalStateException("Destination directory does not exist.");
 183         }
 184 
 185         if (!outputDirectory.isDirectory()) {
 186             throw new IllegalStateException("Invalid destination directory.");
 187         }
 188 
 189         if (name == null) {
 190             throw new IllegalStateException("Name is required.");
 191         }
 192 
 193         if (displayName == null) {
 194             throw new IllegalStateException("Display name is required.");
 195         }
 196 
 197         if (identifier == null) {
 198             throw new IllegalStateException("Identifier is required.");
 199         }
 200 
 201         if (icon != null) {
 202             if (!icon.exists()) {
 203                 throw new IllegalStateException("Icon does not exist.");
 204             }
 205 
 206             if (icon.isDirectory()) {
 207                 throw new IllegalStateException("Invalid icon.");
 208             }
 209         }
 210 
 211         if (shortVersion == null) {
 212             throw new IllegalStateException("Short version is required.");
 213         }
 214 
 215         if (signature == null) {
 216             throw new IllegalStateException("Signature is required.");
 217         }
 218 
 219         if (signature.length() != 4) {
 220             throw new IllegalStateException("Invalid signature.");
 221         }
 222 
 223         if (copyright == null) {
 224             throw new IllegalStateException("Copyright is required.");
 225         }
 226 
 227         if (runtime != null) {
 228             if (!runtime.exists()) {
 229                 throw new IllegalStateException("Runtime does not exist.");
 230             }
 231 
 232             if (!runtime.isDirectory()) {
 233                 throw new IllegalStateException("Invalid runtime.");
 234             }
 235         }
 236 
 237         if (mainClassName == null) {
 238             throw new IllegalStateException("Main class name is required.");
 239         }
 240 
 241         if (classPath.isEmpty()) {
 242             throw new IllegalStateException("Class path is required.");
 243         }
 244 
 245         // Create directory structure
 246         try {
 247             System.out.println("Creating app bundle: " + name);
 248 
 249             File rootDirectory = new File(outputDirectory, name + ".app");
 250             delete(rootDirectory);
 251             rootDirectory.mkdir();
 252 
 253             File contentsDirectory = new File(rootDirectory, "Contents");
 254             contentsDirectory.mkdir();
 255 
 256             File macOSDirectory = new File(contentsDirectory, "MacOS");
 257             macOSDirectory.mkdir();
 258 
 259             File javaDirectory = new File(contentsDirectory, "Java");
 260             javaDirectory.mkdir();
 261 
 262             File classesDirectory = new File(javaDirectory, "Classes");
 263             classesDirectory.mkdir();
 264 
 265             File plugInsDirectory = new File(contentsDirectory, "PlugIns");
 266             plugInsDirectory.mkdir();
 267 
 268             File resourcesDirectory = new File(contentsDirectory, "Resources");
 269             resourcesDirectory.mkdir();
 270 
 271             // Generate Info.plist
 272             File infoPlistFile = new File(contentsDirectory, "Info.plist");
 273             infoPlistFile.createNewFile();
 274             writeInfoPlist(infoPlistFile);
 275 
 276             // Generate PkgInfo
 277             File pkgInfoFile = new File(contentsDirectory, "PkgInfo");
 278             pkgInfoFile.createNewFile();
 279             writePkgInfo(pkgInfoFile);
 280 
 281             // Copy executable to MacOS folder
 282             File executableFile = new File(macOSDirectory, EXECUTABLE_NAME);
 283             copy(getClass().getResource(EXECUTABLE_NAME), executableFile);
 284 
 285             executableFile.setExecutable(true);
 286 
 287             // Copy runtime to PlugIns folder (if specified)
 288             if (runtime != null) {
 289                 copy(runtime, new File(plugInsDirectory, runtime.getName()));
 290             }
 291 
 292             // Copy class path entries to Java folder
 293             for (File entry : classPath) {
 294                 String name = entry.getName();
 295 
 296                 if (entry.isDirectory() || name.endsWith(CLASS_EXTENSION)) {
 297                     copy(entry, new File(classesDirectory, name));
 298                 } else {
 299                     copy(entry, new File(javaDirectory, name));
 300                 }
 301             }
 302 
 303             // Copy native libraries to Java folder
 304             for (File nativeLibrary : nativeLibraries) {
 305                 copy(nativeLibrary, new File(javaDirectory, nativeLibrary.getName()));
 306             }
 307 
 308             // Copy icon to Resources folder
 309             if (icon == null) {
 310                 copy(getClass().getResource(DEFAULT_ICON_NAME), new File(resourcesDirectory,
 311                     DEFAULT_ICON_NAME));
 312             } else {
 313                 copy(icon, new File(resourcesDirectory, icon.getName()));
 314             }
 315         } catch (IOException exception) {
 316             throw new BuildException(exception);
 317         }
 318     }
 319 
 320     private void writeInfoPlist(File file) throws IOException {
 321         Writer out = new BufferedWriter(new FileWriter(file));
 322         XMLOutputFactory output = XMLOutputFactory.newInstance();
 323 
 324         try {
 325             XMLStreamWriter xout = output.createXMLStreamWriter(out);
 326 
 327             // Write XML declaration
 328             xout.writeStartDocument();
 329             xout.writeCharacters("\n");
 330 
 331             // Write plist DTD declaration
 332             xout.writeDTD(PLIST_DTD);
 333             xout.writeCharacters("\n");
 334 
 335             // Begin root element
 336             xout.writeStartElement(PLIST_TAG);
 337             xout.writeAttribute(PLIST_VERSION_ATTRIBUTE, "1.0");
 338             xout.writeCharacters("\n");
 339 
 340             // Begin root dictionary
 341             xout.writeStartElement(DICT_TAG);
 342             xout.writeCharacters("\n");
 343 
 344             // Write bundle properties
 345             writeProperty(xout, "CFBundleDevelopmentRegion", "English");
 346             writeProperty(xout, "CFBundleExecutable", EXECUTABLE_NAME);
 347             writeProperty(xout, "CFBundleIconFile", (icon == null) ? DEFAULT_ICON_NAME : icon.getName());
 348             writeProperty(xout, "CFBundleIdentifier", identifier);
 349             writeProperty(xout, "CFBundleDisplayName", displayName);
 350             writeProperty(xout, "CFBundleInfoDictionaryVersion", "6.0");
 351             writeProperty(xout, "CFBundleName", name);
 352             writeProperty(xout, "CFBundlePackageType", OS_TYPE_CODE);
 353             writeProperty(xout, "CFBundleShortVersionString", shortVersion);
 354             writeProperty(xout, "CFBundleSignature", signature);
 355             writeProperty(xout, "CFBundleVersion", "1");
 356             writeProperty(xout, "NSHumanReadableCopyright", copyright);
 357 
 358             // Write runtime
 359             writeProperty(xout, "JVMRuntime", runtime.getName());
 360 
 361             // Write main class name
 362             writeProperty(xout, "JVMMainClassName", mainClassName);
 363 
 364             // Write options
 365             writeKey(xout, "JVMOptions");
 366 
 367             xout.writeStartElement(ARRAY_TAG);
 368             xout.writeCharacters("\n");
 369 
 370             for (String option : options) {
 371                 writeString(xout, option);
 372             }
 373 
 374             xout.writeEndElement();
 375             xout.writeCharacters("\n");
 376 
 377             // Write arguments
 378             writeKey(xout, "JVMArguments");
 379 
 380             xout.writeStartElement(ARRAY_TAG);
 381             xout.writeCharacters("\n");
 382 
 383             for (String argument : arguments) {
 384                 writeString(xout, argument);
 385             }
 386 
 387             xout.writeEndElement();
 388             xout.writeCharacters("\n");
 389 
 390             // End root dictionary
 391             xout.writeEndElement();
 392             xout.writeCharacters("\n");
 393 
 394             // End root element
 395             xout.writeEndElement();
 396             xout.writeCharacters("\n");
 397 
 398             // Close document
 399             xout.writeEndDocument();
 400             xout.writeCharacters("\n");
 401 
 402             out.flush();
 403         } catch (XMLStreamException exception) {
 404             throw new IOException(exception);
 405         } finally {
 406             out.close();
 407         }
 408     }
 409 
 410     private void writeKey(XMLStreamWriter xout, String key) throws XMLStreamException {
 411         xout.writeStartElement(KEY_TAG);
 412         xout.writeCharacters(key);
 413         xout.writeEndElement();
 414         xout.writeCharacters("\n");
 415     }
 416 
 417     private void writeString(XMLStreamWriter xout, String value) throws XMLStreamException {
 418         xout.writeStartElement(STRING_TAG);
 419         xout.writeCharacters(value);
 420         xout.writeEndElement();
 421         xout.writeCharacters("\n");
 422     }
 423 
 424     private void writeProperty(XMLStreamWriter xout, String key, String value) throws XMLStreamException {
 425         writeKey(xout, key);
 426         writeString(xout, value);
 427     }
 428 
 429     private void writePkgInfo(File file) throws IOException {
 430         Writer out = new BufferedWriter(new FileWriter(file));
 431 
 432         try {
 433             out.write(OS_TYPE_CODE + signature);
 434             out.flush();
 435         } finally {
 436             out.close();
 437         }
 438     }
 439 
 440     private static void delete(File file) throws IOException {
 441         Path filePath = file.toPath();
 442 
 443         if (Files.exists(filePath, LinkOption.NOFOLLOW_LINKS)) {
 444             if (Files.isDirectory(filePath, LinkOption.NOFOLLOW_LINKS)) {
 445                 File[] files = file.listFiles();
 446 
 447                 for (int i = 0; i < files.length; i++) {
 448                     delete(files[i]);
 449                 }
 450             }
 451 
 452             Files.delete(filePath);
 453         }
 454     }
 455 
 456     private static void copy(URL location, File file) throws IOException {
 457         try (InputStream in = location.openStream()) {
 458             Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
 459         }
 460     }
 461 
 462     private static void copy(File source, File destination) throws IOException {
 463         Path sourcePath = source.toPath();
 464         Path destinationPath = destination.toPath();
 465 
 466         Files.copy(sourcePath, destinationPath, StandardCopyOption.REPLACE_EXISTING, LinkOption.NOFOLLOW_LINKS);
 467 
 468         if (Files.isDirectory(sourcePath, LinkOption.NOFOLLOW_LINKS)) {
 469             String[] files = source.list();
 470 
 471             for (int i = 0; i < files.length; i++) {
 472                 String file = files[i];
 473                 copy(new File(source, file), new File(destination, file));
 474             }
 475         }
 476     }
 477 }