1 /*
   2  * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.jpackage.internal;
  27 
  28 import java.io.File;
  29 import java.io.IOException;
  30 import java.math.BigInteger;
  31 import java.text.MessageFormat;
  32 import java.util.Arrays;
  33 import java.util.Collection;
  34 import java.util.HashMap;
  35 import java.util.Map;
  36 import java.util.Optional;
  37 import java.util.ResourceBundle;
  38 
  39 import static jdk.jpackage.internal.StandardBundlerParam.*;
  40 import static jdk.jpackage.internal.MacBaseInstallerBundler.*;
  41 
  42 public class MacAppBundler extends AbstractImageBundler {
  43 
  44     private static final ResourceBundle I18N = ResourceBundle.getBundle(
  45             "jdk.jpackage.internal.resources.MacResources");
  46 
  47     private static final String TEMPLATE_BUNDLE_ICON = "GenericApp.icns";
  48 
  49     public static Map<String, String> getMacCategories() {
  50         Map<String, String> map = new HashMap<>();
  51         map.put("Business", "public.app-category.business");
  52         map.put("Developer Tools", "public.app-category.developer-tools");
  53         map.put("Education", "public.app-category.education");
  54         map.put("Entertainment", "public.app-category.entertainment");
  55         map.put("Finance", "public.app-category.finance");
  56         map.put("Games", "public.app-category.games");
  57         map.put("Graphics & Design", "public.app-category.graphics-design");
  58         map.put("Healthcare & Fitness",
  59                 "public.app-category.healthcare-fitness");
  60         map.put("Lifestyle", "public.app-category.lifestyle");
  61         map.put("Medical", "public.app-category.medical");
  62         map.put("Music", "public.app-category.music");
  63         map.put("News", "public.app-category.news");
  64         map.put("Photography", "public.app-category.photography");
  65         map.put("Productivity", "public.app-category.productivity");
  66         map.put("Reference", "public.app-category.reference");
  67         map.put("Social Networking", "public.app-category.social-networking");
  68         map.put("Sports", "public.app-category.sports");
  69         map.put("Travel", "public.app-category.travel");
  70         map.put("Utilities", "public.app-category.utilities");
  71         map.put("Video", "public.app-category.video");
  72         map.put("Weather", "public.app-category.weather");
  73 
  74         map.put("Action Games", "public.app-category.action-games");
  75         map.put("Adventure Games", "public.app-category.adventure-games");
  76         map.put("Arcade Games", "public.app-category.arcade-games");
  77         map.put("Board Games", "public.app-category.board-games");
  78         map.put("Card Games", "public.app-category.card-games");
  79         map.put("Casino Games", "public.app-category.casino-games");
  80         map.put("Dice Games", "public.app-category.dice-games");
  81         map.put("Educational Games", "public.app-category.educational-games");
  82         map.put("Family Games", "public.app-category.family-games");
  83         map.put("Kids Games", "public.app-category.kids-games");
  84         map.put("Music Games", "public.app-category.music-games");
  85         map.put("Puzzle Games", "public.app-category.puzzle-games");
  86         map.put("Racing Games", "public.app-category.racing-games");
  87         map.put("Role Playing Games", "public.app-category.role-playing-games");
  88         map.put("Simulation Games", "public.app-category.simulation-games");
  89         map.put("Sports Games", "public.app-category.sports-games");
  90         map.put("Strategy Games", "public.app-category.strategy-games");
  91         map.put("Trivia Games", "public.app-category.trivia-games");
  92         map.put("Word Games", "public.app-category.word-games");
  93 
  94         return map;
  95     }
  96 
  97     public static final EnumeratedBundlerParam<String> MAC_CATEGORY =
  98             new EnumeratedBundlerParam<>(
  99                     Arguments.CLIOptions.MAC_APP_STORE_CATEGORY.getId(),
 100                     String.class,
 101                     params -> "Unknown",
 102                     (s, p) -> s,
 103                     getMacCategories(),
 104                     false //strict - for MacStoreBundler this should be strict
 105             );
 106 
 107     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_NAME =
 108             new StandardBundlerParam<>(
 109                     Arguments.CLIOptions.MAC_BUNDLE_NAME.getId(),
 110                     String.class,
 111                     params -> null,
 112                     (s, p) -> s);
 113 
 114     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_IDENTIFIER =
 115             new StandardBundlerParam<>(
 116                     Arguments.CLIOptions.MAC_BUNDLE_IDENTIFIER.getId(),
 117                     String.class,
 118                     IDENTIFIER::fetchFrom,
 119                     (s, p) -> s);
 120 
 121     public static final BundlerParamInfo<String> MAC_CF_BUNDLE_VERSION =
 122             new StandardBundlerParam<>(
 123                     "mac.CFBundleVersion",
 124                     String.class,
 125                     p -> {
 126                         String s = VERSION.fetchFrom(p);
 127                         if (validCFBundleVersion(s)) {
 128                             return s;
 129                         } else {
 130                             return "100";
 131                         }
 132                     },
 133                     (s, p) -> s);
 134 
 135     public static final BundlerParamInfo<String> DEFAULT_ICNS_ICON =
 136             new StandardBundlerParam<>(
 137             ".mac.default.icns",
 138             String.class,
 139             params -> TEMPLATE_BUNDLE_ICON,
 140             (s, p) -> s);
 141 
 142     public static final BundlerParamInfo<String> DEVELOPER_ID_APP_SIGNING_KEY =
 143             new StandardBundlerParam<>(
 144             "mac.signing-key-developer-id-app",
 145             String.class,
 146             params -> {
 147                     String result = MacBaseInstallerBundler.findKey(
 148                             "Developer ID Application: "
 149                             + SIGNING_KEY_USER.fetchFrom(params),
 150                             SIGNING_KEYCHAIN.fetchFrom(params),
 151                             VERBOSE.fetchFrom(params));
 152                     if (result != null) {
 153                         MacCertificate certificate = new MacCertificate(result,
 154                                 VERBOSE.fetchFrom(params));
 155 
 156                         if (!certificate.isValid()) {
 157                             Log.error(MessageFormat.format(I18N.getString(
 158                                     "error.certificate.expired"), result));
 159                         }
 160                     }
 161 
 162                     return result;
 163                 },
 164             (s, p) -> s);
 165 
 166     public static final BundlerParamInfo<String> BUNDLE_ID_SIGNING_PREFIX =
 167             new StandardBundlerParam<>(
 168             Arguments.CLIOptions.MAC_BUNDLE_SIGNING_PREFIX.getId(),
 169             String.class,
 170             params -> IDENTIFIER.fetchFrom(params) + ".",
 171             (s, p) -> s);
 172 
 173     public static final BundlerParamInfo<File> ICON_ICNS =
 174             new StandardBundlerParam<>(
 175             "icon.icns",
 176             File.class,
 177             params -> {
 178                 File f = ICON.fetchFrom(params);
 179                 if (f != null && !f.getName().toLowerCase().endsWith(".icns")) {
 180                     Log.error(MessageFormat.format(
 181                             I18N.getString("message.icon-not-icns"), f));
 182                     return null;
 183                 }
 184                 return f;
 185             },
 186             (s, p) -> new File(s));
 187 
 188     public static boolean validCFBundleVersion(String v) {
 189         // CFBundleVersion (String - iOS, OS X) specifies the build version
 190         // number of the bundle, which identifies an iteration (released or
 191         // unreleased) of the bundle. The build version number should be a
 192         // string comprised of three non-negative, period-separated integers
 193         // with the first integer being greater than zero. The string should
 194         // only contain numeric (0-9) and period (.) characters. Leading zeros
 195         // are truncated from each integer and will be ignored (that is,
 196         // 1.02.3 is equivalent to 1.2.3). This key is not localizable.
 197 
 198         if (v == null) {
 199             return false;
 200         }
 201 
 202         String p[] = v.split("\\.");
 203         if (p.length > 3 || p.length < 1) {
 204             Log.verbose(I18N.getString(
 205                     "message.version-string-too-many-components"));
 206             return false;
 207         }
 208 
 209         try {
 210             BigInteger n = new BigInteger(p[0]);
 211             if (BigInteger.ONE.compareTo(n) > 0) {
 212                 Log.verbose(I18N.getString(
 213                         "message.version-string-first-number-not-zero"));
 214                 return false;
 215             }
 216             if (p.length > 1) {
 217                 n = new BigInteger(p[1]);
 218                 if (BigInteger.ZERO.compareTo(n) > 0) {
 219                     Log.verbose(I18N.getString(
 220                             "message.version-string-no-negative-numbers"));
 221                     return false;
 222                 }
 223             }
 224             if (p.length > 2) {
 225                 n = new BigInteger(p[2]);
 226                 if (BigInteger.ZERO.compareTo(n) > 0) {
 227                     Log.verbose(I18N.getString(
 228                             "message.version-string-no-negative-numbers"));
 229                     return false;
 230                 }
 231             }
 232         } catch (NumberFormatException ne) {
 233             Log.verbose(I18N.getString("message.version-string-numbers-only"));
 234             Log.verbose(ne);
 235             return false;
 236         }
 237 
 238         return true;
 239     }
 240 
 241     @Override
 242     public boolean validate(Map<String, ? super Object> params)
 243             throws UnsupportedPlatformException, ConfigException {
 244         try {
 245             return doValidate(params);
 246         } catch (RuntimeException re) {
 247             if (re.getCause() instanceof ConfigException) {
 248                 throw (ConfigException) re.getCause();
 249             } else {
 250                 throw new ConfigException(re);
 251             }
 252         }
 253     }
 254 
 255     private boolean doValidate(Map<String, ? super Object> params)
 256             throws UnsupportedPlatformException, ConfigException {
 257         if (Platform.getPlatform() != Platform.MAC) {
 258             throw new UnsupportedPlatformException();
 259         }
 260 
 261         imageBundleValidation(params);
 262 
 263         if (StandardBundlerParam.getPredefinedAppImage(params) != null) {
 264             return true;
 265         }
 266 
 267         // validate short version
 268         if (!validCFBundleVersion(MAC_CF_BUNDLE_VERSION.fetchFrom(params))) {
 269             throw new ConfigException(
 270                     I18N.getString("error.invalid-cfbundle-version"),
 271                     I18N.getString("error.invalid-cfbundle-version.advice"));
 272         }
 273 
 274         // reject explicitly set sign to true and no valid signature key
 275         if (Optional.ofNullable(MacAppImageBuilder.
 276                     SIGN_BUNDLE.fetchFrom(params)).orElse(Boolean.FALSE)) {
 277             String signingIdentity =
 278                     DEVELOPER_ID_APP_SIGNING_KEY.fetchFrom(params);
 279             if (signingIdentity == null) {
 280                 throw new ConfigException(
 281                         I18N.getString("error.explicit-sign-no-cert"),
 282                         I18N.getString("error.explicit-sign-no-cert.advice"));
 283             }
 284         }
 285 
 286         return true;
 287     }
 288 
 289     File doBundle(Map<String, ? super Object> params, File outputDirectory,
 290             boolean dependentTask) throws PackagerException {
 291         if (StandardBundlerParam.isRuntimeInstaller(params)) {
 292             return PREDEFINED_RUNTIME_IMAGE.fetchFrom(params);
 293         } else {
 294             return doAppBundle(params, outputDirectory, dependentTask);
 295         }
 296     }
 297 
 298     File doAppBundle(Map<String, ? super Object> params, File outputDirectory,
 299             boolean dependentTask) throws PackagerException {
 300         try {
 301             File rootDirectory = createRoot(params, outputDirectory,
 302                     dependentTask, APP_NAME.fetchFrom(params) + ".app");
 303             AbstractAppImageBuilder appBuilder =
 304                     new MacAppImageBuilder(params, outputDirectory.toPath());
 305             if (PREDEFINED_RUNTIME_IMAGE.fetchFrom(params) == null ) {
 306                 JLinkBundlerHelper.execute(params, appBuilder);
 307             } else {
 308                 StandardBundlerParam.copyPredefinedRuntimeImage(
 309                         params, appBuilder);
 310             }
 311             return rootDirectory;
 312         } catch (PackagerException pe) {
 313             throw pe;
 314         } catch (Exception ex) {
 315             Log.verbose(ex);
 316             throw new PackagerException(ex);
 317         }
 318     }
 319 
 320     /////////////////////////////////////////////////////////////////////////
 321     // Implement Bundler
 322     /////////////////////////////////////////////////////////////////////////
 323 
 324     @Override
 325     public String getName() {
 326         return I18N.getString("app.bundler.name");
 327     }
 328 
 329     @Override
 330     public String getDescription() {
 331         return I18N.getString("app.bundler.description");
 332     }
 333 
 334     @Override
 335     public String getID() {
 336         return "mac.app";
 337     }
 338 
 339     @Override
 340     public String getBundleType() {
 341         return "IMAGE";
 342     }
 343 
 344     @Override
 345     public Collection<BundlerParamInfo<?>> getBundleParameters() {
 346         return getAppBundleParameters();
 347     }
 348 
 349     public static Collection<BundlerParamInfo<?>> getAppBundleParameters() {
 350         return Arrays.asList(
 351                 APP_NAME,
 352                 APP_RESOURCES,
 353                 ARGUMENTS,
 354                 BUNDLE_ID_SIGNING_PREFIX,
 355                 CLASSPATH,
 356                 DEVELOPER_ID_APP_SIGNING_KEY,
 357                 ICON_ICNS,
 358                 JAVA_OPTIONS,
 359                 MAC_CATEGORY,
 360                 MAC_CF_BUNDLE_IDENTIFIER,
 361                 MAC_CF_BUNDLE_NAME,
 362                 MAC_CF_BUNDLE_VERSION,
 363                 MAIN_CLASS,
 364                 MAIN_JAR,
 365                 SIGNING_KEYCHAIN,
 366                 VERSION,
 367                 VERBOSE
 368         );
 369     }
 370 
 371 
 372     @Override
 373     public File execute(Map<String, ? super Object> params,
 374             File outputParentDir) throws PackagerException {
 375         return doBundle(params, outputParentDir, false);
 376     }
 377 
 378     @Override
 379     public boolean supported(boolean runtimeInstaller) {
 380         return Platform.getPlatform() == Platform.MAC;
 381     }
 382 
 383 }