1 /*
   2  * Copyright (c) 2014, 2016 Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  */
   5 package com.oracle.appbundlers.utils;
   6 
   7 import static com.oracle.appbundlers.utils.Config.CONFIG_INSTANCE;
   8 import static java.lang.String.format;
   9 import static java.util.Arrays.asList;
  10 import static java.util.stream.Collectors.joining;
  11 
  12 import java.io.BufferedWriter;
  13 import java.io.File;
  14 import java.io.IOException;
  15 import java.io.PrintWriter;
  16 import java.io.StringWriter;
  17 import java.nio.charset.StandardCharsets;
  18 import java.nio.file.Files;
  19 import java.nio.file.Path;
  20 import java.util.Collection;
  21 import java.util.HashMap;
  22 import java.util.List;
  23 import java.util.Map;
  24 import java.util.Set;
  25 import java.util.concurrent.ExecutionException;
  26 import java.util.function.Function;
  27 import java.util.logging.Level;
  28 import java.util.logging.Logger;
  29 import java.util.stream.Collectors;
  30 
  31 import javax.xml.parsers.DocumentBuilder;
  32 import javax.xml.parsers.DocumentBuilderFactory;
  33 import javax.xml.parsers.ParserConfigurationException;
  34 import javax.xml.transform.OutputKeys;
  35 import javax.xml.transform.Transformer;
  36 import javax.xml.transform.TransformerException;
  37 import javax.xml.transform.TransformerFactory;
  38 import javax.xml.transform.dom.DOMSource;
  39 import javax.xml.transform.stream.StreamResult;
  40 
  41 import org.w3c.dom.DOMException;
  42 import org.w3c.dom.Document;
  43 import org.w3c.dom.Element;
  44 
  45 import com.oracle.appbundlers.utils.installers.AbstractBundlerUtils;
  46 import com.oracle.tools.packager.ConfigException;
  47 import com.oracle.tools.packager.RelativeFileSet;
  48 import com.oracle.tools.packager.UnsupportedPlatformException;
  49 import com.sun.javafx.tools.packager.bundlers.BundleParams;
  50 
  51 /**
  52  * @author Andrei Eremeev <andrei.eremeev@oracle.com>
  53  */
  54 public class AntBundlingManager extends BundlingManager {
  55 
  56     private static final Logger LOG = Logger
  57             .getLogger(AntBundlingManager.class.getName());
  58 
  59     @SuppressWarnings("serial")
  60     private final static Map<String, Location> toAntEntry = new HashMap<String, Location>() {
  61         {
  62             put(APP_RESOURCES, new Location("fx:resources", ""));
  63             put(LICENSE_FILE, new Location("fx:resources", ""));
  64             put(APPLICATION_CLASS, new Location("fx:application", "mainClass"));
  65             put(IDENTIFIER, new Location("fx:application", "id"));
  66             put(VERSION, new Location("fx:application", "version"));
  67             put(APP_NAME,
  68                     new Location("fx:application", "name"));
  69             put(VENDOR, new Location("fx:info", "vendor"));
  70             put(TITLE, new Location("fx:info", "title"));
  71             put(DESCRIPTION, new Location("fx:info", "description"));
  72             put(ICON, new Location("fx:info", ""));
  73             put(EMAIL, new Location("fx:info", "email"));
  74             put(COPYRIGHT, new Location("fx:info", "copyright"));
  75             put(LICENSE_TYPE, new Location("fx:info", "license"));
  76             put(CATEGORY, new Location("fx:info", "category"));
  77             put(SHORTCUT_HINT, new Location("fx:preferences", "shortcut"));
  78             put(MENU_HINT, new Location("fx:preferences", "menu"));
  79             put(SYSTEM_WIDE, new Location("fx:preferences", "install"));
  80             put(BundleParams.PARAM_RUNTIME,
  81                     new Location("fx:platform", "baseDir"));
  82             put(JVM_OPTIONS, new Location("fx:platform", ""));
  83             put(JVM_PROPERTIES, new Location("fx:platform", ""));
  84             put(USER_JVM_OPTIONS, new Location("fx:platform", ""));
  85             put(ARGUMENTS, new Location("fx:application", ""));
  86             put(FILE_ASSOCIATIONS, new Location("fx:info", ""));
  87             put(SECONDARY_LAUNCHERS, Location.DUMMY);
  88             put(STRIP_NATIVE_COMMANDS, new Location("fx:runtime", STRIP_NATIVE_COMMANDS));
  89             put(ADD_MODS, new Location("fx:runtime", ""));
  90             put(LIMIT_MODS, new Location("fx:runtime",""));
  91             put(MODULEPATH, new Location("fx:runtime",""));
  92             put(MAIN_MODULE, new Location("fx:application", MAIN_MODULE));
  93         }
  94     };
  95 
  96     public AntBundlingManager(AbstractBundlerUtils bundler) {
  97         super(bundler);
  98     }
  99 
 100     @Override
 101     public boolean validate(Map<String, Object> params)
 102             throws UnsupportedPlatformException, ConfigException {
 103         return true;
 104     }
 105 
 106     private Data createDocument()
 107             throws DOMException, ParserConfigurationException {
 108         DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 109         DocumentBuilder builder = factory.newDocumentBuilder();
 110         Document document = builder.newDocument();
 111         Element project = document.createElement("project");
 112         project.setAttribute("default", "fx-deploy");
 113         project.setAttribute("xmlns:fx", "javafx:com.sun.javafx.tools.ant");
 114         document.appendChild(project);
 115 
 116         Element taskDef = document.createElement("taskdef");
 117         taskDef.setAttribute("resource", "com/sun/javafx/tools/ant/antlib.xml");
 118         taskDef.setAttribute("uri", "javafx:com.sun.javafx.tools.ant");
 119         taskDef.setAttribute("classpath", CONFIG_INSTANCE.getAntJavaFx());
 120         project.appendChild(taskDef);
 121 
 122         Element target = document.createElement("target");
 123         target.setAttribute("name", "fx-deploy");
 124         project.appendChild(target);
 125         return new Data(document, target);
 126     }
 127 
 128     private String documentToXml(Document document)
 129             throws TransformerException {
 130         TransformerFactory factory = TransformerFactory.newInstance();
 131         factory.setAttribute("indent-number", 4);
 132         Transformer transformer = factory.newTransformer();
 133         DOMSource source = new DOMSource(document);
 134         StringWriter writer = new StringWriter();
 135         StreamResult result = new StreamResult(new PrintWriter(writer));
 136         transformer.setOutputProperty(OutputKeys.METHOD, "xml");
 137         transformer.setOutputProperty(OutputKeys.INDENT, "yes");
 138         transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
 139         transformer.transform(source, result);
 140         return writer.toString();
 141     }
 142 
 143     @SuppressWarnings("unchecked")
 144     private void appendToFXDeploy(Document document, Element fxDeploy,
 145             Map<String, Object> params) throws IOException {
 146         final Map<String, Element> ant = toAntEntry.values().stream()
 147                 .filter(location -> location != Location.DUMMY)
 148                 .map(location -> location.element).distinct().collect(Collectors
 149                         .toMap(Function.identity(), document::createElement));
 150         ant.forEach((str, element) -> fxDeploy.appendChild(element));
 151         for (Map.Entry<String, Object> entry : params.entrySet()) {
 152             String key = entry.getKey();
 153             Object value = entry.getValue();
 154             System.out.println("key = " + key);
 155             System.out.println("value = " + value);
 156             Location location = toAntEntry.get(key);
 157             if (location != null) {
 158                 Element parentEl = ant.get(location.element);
 159                 switch (key) {
 160                 case "appResources": {
 161                     RelativeFileSet relFileSet = (RelativeFileSet) value;
 162                     Element e = document.createElement("fx:fileset");
 163                     e.setAttribute("dir",
 164                             relFileSet.getBaseDirectory().getAbsolutePath());
 165                     e.setAttribute("includes", relFileSet.getIncludedFiles()
 166                             .stream().collect(joining(",")));
 167                     parentEl.appendChild(e);
 168                     break;
 169                 }
 170                 case "licenseFile": {
 171                     String file = (String) value;
 172                     Element e = document.createElement("fx:fileset");
 173                     RelativeFileSet relFileSet = null;
 174                     relFileSet = com.oracle.tools.packager.StandardBundlerParam.APP_RESOURCES.fetchFrom(params);
 175                     e.setAttribute("dir",
 176                             relFileSet.getBaseDirectory().getAbsolutePath());
 177                     e.setAttribute("includes", file);
 178                     e.setAttribute("type", "license");
 179                     parentEl.appendChild(e);
 180                     break;
 181                 }
 182                 case "icon": {
 183                     File icon = (File) value;
 184                     Element e = document.createElement("fx:icon");
 185                     e.setAttribute("href", icon.getAbsolutePath());
 186                     parentEl.appendChild(e);
 187                     break;
 188                 }
 189                 case "jvmOptions": {
 190                     createJvmOptionsEntries(document, parentEl, value);
 191                     break;
 192                 }
 193                 case "jvmProperties": {
 194                     createJvmPropertiesEntries(document, parentEl, value);
 195                     break;
 196                 }
 197                 case "userJvmOptions": {
 198                     createUserJvmOptionsEntries(document, parentEl, value);
 199                     break;
 200                 }
 201                 case "secondaryLaunchers": {
 202                     List<Map<String, Object>> launchers = (List<Map<String, Object>>) value;
 203                     for (Map<String, Object> properties : launchers) {
 204                         Element launcherEl = document
 205                                 .createElement("fx:secondaryLauncher");
 206 
 207                         for (Map.Entry<String, Object> keyVal : properties
 208                                 .entrySet()) {
 209 
 210                             switch (keyVal.getKey()) {
 211                             case "jvmOptions": {
 212                                 createJvmOptionsEntries(document, launcherEl,
 213                                         keyVal.getValue());
 214                                 break;
 215                             }
 216                             case "jvmProperties": {
 217                                 createJvmPropertiesEntries(document, launcherEl,
 218                                         keyVal.getValue());
 219                                 break;
 220                             }
 221                             case "userJvmOptions": {
 222                                 createUserJvmOptionsEntries(document,
 223                                         launcherEl, keyVal.getValue());
 224                                 break;
 225                             }
 226                             case "arguments": {
 227                                 createArgumentEntries(document, launcherEl,
 228                                         keyVal.getValue());
 229                                 break;
 230                             }
 231 
 232                             case MAIN_MODULE:
 233                                 launcherEl.setAttribute(MAIN_MODULE, (String) keyVal.getValue());
 234                             break;
 235 
 236                             case APPLICATION_CLASS:
 237                                 launcherEl.setAttribute(location.attribute, (String) keyVal.getValue());
 238                             break;
 239 
 240                             default:
 241                                 launcherEl.appendChild(
 242                                         createBundleArgumentEntry(document,
 243                                                 keyVal.getKey(),
 244                                                 keyVal.getValue()));
 245                             }
 246                         }
 247 
 248                         fxDeploy.appendChild(launcherEl);
 249                     }
 250                     break;
 251                 }
 252                 case "runtime": {
 253                     RelativeFileSet relFileSet = (RelativeFileSet) value;
 254                     parentEl.setAttribute(location.attribute,
 255                             relFileSet.getBaseDirectory().getAbsolutePath());
 256                     break;
 257                 }
 258                 case "arguments": {
 259                     createArgumentEntries(document, parentEl, value);
 260                     break;
 261                 }
 262                 case "fileAssociations": {
 263                     List<Map<String, Object>> associations = (List<Map<String, Object>>) value;
 264                     for (Map<String, Object> association : associations) {
 265                         Element el = document.createElement("fx:association");
 266                         List<String> extensions = (List<String>) association
 267                                 .get(FA_EXTENSIONS);
 268                         List<String> contentTypes = (List<String>) association
 269                                 .get(FA_CONTENT_TYPE);
 270                         String description = (String) association
 271                                 .get(FA_DESCRIPTION);
 272                         el.setAttribute("extension", extensions.stream()
 273                                 .collect(Collectors.joining(" ")));
 274                         el.setAttribute("mimetype", contentTypes.stream()
 275                                 .collect(Collectors.joining(" ")));
 276                         if (description != null) {
 277                             el.setAttribute("description", description);
 278                         }
 279                         File icon = (File) association.get(FA_ICON);
 280                         if (icon != null) {
 281                             el.setAttribute("icon", icon.getAbsolutePath());
 282                         }
 283                         parentEl.appendChild(el);
 284                     }
 285                     break;
 286                 }
 287 
 288                 case STRIP_NATIVE_COMMANDS:
 289                     parentEl.setAttribute(STRIP_NATIVE_COMMANDS,
 290                             value.toString());
 291                     break;
 292                 case ADD_MODS:
 293                     Element addModsElement = document
 294                             .createElement("fx:" + ADD_MODS);
 295                     addModsElement.setAttribute("value", getValueAsString(value));
 296                     parentEl.appendChild(addModsElement);
 297                     break;
 298                 case LIMIT_MODS:
 299                     Element limitModsElement = document
 300                             .createElement("fx:" + LIMIT_MODS);
 301                     limitModsElement.setAttribute("value", getValueAsString(value));
 302                     parentEl.appendChild(limitModsElement);
 303                     break;
 304                 case MODULEPATH:
 305                     Element modulePath = document
 306                             .createElement("fx:" + MODULEPATH);
 307                     modulePath.setAttribute("value", getValueAsString(value));
 308                     parentEl.appendChild(modulePath);
 309                     break;
 310                 case MAIN_MODULE:
 311                     if(value instanceof String) {
 312                         parentEl.setAttribute(MAIN_MODULE, ((String) value).split("/")[0]);
 313                         parentEl.setAttribute("mainClass", ((String) value).split("/")[1]);
 314                     }
 315                     break;
 316                 default:
 317                     checkValue(value);
 318                     parentEl.setAttribute(location.attribute, value.toString());
 319                     break;
 320                 }
 321             } else {
 322                 fxDeploy.appendChild(
 323                         createBundleArgumentEntry(document, key, value));
 324             }
 325         }
 326     }
 327 
 328     @SuppressWarnings({ "unchecked", "rawtypes" })
 329     private String getValueAsString(Object value) {
 330         String actualValue = null;
 331         if (value instanceof String) {
 332             actualValue = value.toString();
 333         } else if (value instanceof List) {
 334             actualValue = String.join(",", (List) value);
 335         } else if (value instanceof Set) {
 336             actualValue = String.join(",", (Set) value);
 337         } else if (value instanceof Object) {
 338             actualValue = value.toString();
 339         }
 340         return actualValue;
 341     }
 342 
 343     private Element createBundleArgumentEntry(Document document, String argName,
 344             Object value) throws IOException {
 345         Element bundleArgument = document.createElement("fx:bundleArgument");
 346         String argValue;
 347         if ("mainJar".equals(argName)) {
 348             RelativeFileSet fileSet = (RelativeFileSet) value;
 349             argValue = fileSet.getIncludedFiles().iterator().next();
 350         } else {
 351             checkValue(value);
 352             argValue = value.toString();
 353         }
 354         bundleArgument.setAttribute("arg", argName);
 355         bundleArgument.setAttribute("value", argValue);
 356 
 357         return bundleArgument;
 358     }
 359 
 360     @SuppressWarnings("unchecked")
 361     private void createJvmOptionsEntries(Document document, Element parentEl,
 362             Object value) {
 363         Collection<String> col = (Collection<String>) value;
 364         col.forEach(arg -> {
 365             Element e = document.createElement("fx:jvmarg");
 366             e.setAttribute("value", arg);
 367             parentEl.appendChild(e);
 368         });
 369     }
 370 
 371     @SuppressWarnings("unchecked")
 372     private void createJvmPropertiesEntries(Document document, Element parentEl,
 373             Object value) {
 374         Map<String, String> properties = (Map<String, String>) value;
 375         properties.forEach((k, v) -> {
 376             Element e = document.createElement("fx:property");
 377             e.setAttribute("name", k);
 378             e.setAttribute("value", v);
 379             parentEl.appendChild(e);
 380         });
 381     }
 382 
 383     @SuppressWarnings("unchecked")
 384     private void createUserJvmOptionsEntries(Document document,
 385             Element parentEl, Object value) {
 386         Map<String, String> properties = (Map<String, String>) value;
 387         properties.forEach((k, v) -> {
 388             Element e = document.createElement("fx:jvmuserarg");
 389             e.setAttribute("name", k);
 390             e.setAttribute("value", v);
 391             parentEl.appendChild(e);
 392         });
 393     }
 394 
 395     @SuppressWarnings("unchecked")
 396     private void createArgumentEntries(Document document, Element parentEl,
 397             Object value) {
 398         List<String> arguments = (List<String>) value;
 399         for (String arg : arguments) {
 400             Element e = document.createElement("fx:argument");
 401             e.setTextContent(arg);
 402             parentEl.appendChild(e);
 403         }
 404     }
 405 
 406     private void checkValue(Object value) throws IOException {
 407         if (!(value instanceof String) && !(value instanceof Boolean)
 408                 && !(value instanceof File)) {
 409             throw new IOException(format("Value is not mapped : %s, %s",
 410                     value.getClass(), value.toString()));
 411         }
 412     }
 413 
 414     private Element fxDeploy(Document document, Map<String, Object> params,
 415             File file) throws IOException {
 416         Element fxDeploy = document.createElement("fx:deploy");
 417         String bundleType = getBundler().getBundleType();
 418         fxDeploy.setAttribute("nativeBundles",
 419                 "image".equalsIgnoreCase(bundleType) ? "image"
 420                         : getBundler().getID());
 421         if (!file.getName().equals("bundles")) {
 422             throw new IllegalArgumentException(
 423                     "Invalid bundle directory : " + file);
 424         }
 425         fxDeploy.setAttribute("outdir", file.getParentFile().getAbsolutePath());
 426         fxDeploy.setAttribute("outfile", "test");
 427         fxDeploy.setAttribute("verbose", "true");
 428         appendToFXDeploy(document, fxDeploy, params);
 429         return fxDeploy;
 430     }
 431 
 432     private void writeToFile(Path file, String buildXml) throws IOException {
 433         try (BufferedWriter writer = Files.newBufferedWriter(file,
 434                 StandardCharsets.UTF_8)) {
 435             writer.append(buildXml);
 436         }
 437     }
 438 
 439     @Override
 440     public File execute(Map<String, Object> params, File file)
 441             throws IOException {
 442         try {
 443             Data data = createDocument();
 444             Element fxDeploy = fxDeploy(data.document, params, file);
 445             data.fxDeployTarget.appendChild(fxDeploy);
 446             Path buildXmlFile = Utils.getTempDir().resolve("build.xml");
 447             String buildXml = documentToXml(data.document);
 448             // TODO: WTF?
 449             LOG.log(Level.INFO, "build.xml:\n{0}\n", buildXml);
 450             writeToFile(buildXmlFile, buildXml);
 451 
 452             final List<String> command = asList(CONFIG_INSTANCE.antExec(), "-f",
 453                     buildXmlFile.toString());
 454 
 455             @SuppressWarnings("serial")
 456             ProcessOutput process = Utils.runCommand(command,
 457                     /* verbose = */ true,
 458                     /* timeout = */ CONFIG_INSTANCE.getInstallTimeout(),
 459                     new HashMap<String, String>() {
 460                         {
 461                             put("JAVA_HOME", CONFIG_INSTANCE.getJavaHome());
 462                         }
 463                     });
 464             if (process.isTimeoutExceeded()) {
 465                 throw new IOException(
 466                         "The command " + command + " hasn't finished in "
 467                                 + CONFIG_INSTANCE.getInstallTimeout()
 468                                 + " milliseconds");
 469             }
 470 
 471             if (process.exitCode() != 0) {
 472                 throw new IOException(
 473                         "Process finished with not zero exit code");
 474             }
 475             // TODO: Proper exception handle?
 476             return file;
 477         } catch (DOMException | ParserConfigurationException
 478                 | TransformerException | ExecutionException e) {
 479             throw new IOException(e);
 480         }
 481     }
 482 
 483     @Override
 484     public String getShortName() {
 485         return "ant";
 486     }
 487 
 488     private static class Location {
 489 
 490         public static final Location DUMMY = new Location("dummy", "dummy");
 491 
 492         public final String element;
 493         public final String attribute;
 494 
 495         public Location(String element, String attribute) {
 496             this.element = element;
 497             this.attribute = attribute;
 498         }
 499     }
 500 
 501     private static class Data {
 502 
 503         public final Document document;
 504         public final Element fxDeployTarget;
 505 
 506         public Data(Document document, Element fxDeploy) {
 507             this.document = document;
 508             this.fxDeployTarget = fxDeploy;
 509         }
 510     }
 511 }