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 }