--- /dev/null 2018-11-09 14:39:02.000000000 -0500 +++ new/src/jdk.jpackager/share/classes/jdk/jpackager/internal/StandardBundlerParam.java 2018-11-09 14:39:00.962216600 -0500 @@ -0,0 +1,906 @@ +/* + * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackager.internal; + +import jdk.jpackager.internal.bundlers.BundleParams; +import jdk.jpackager.internal.builders.AbstractAppImageBuilder; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.HashSet; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * StandardBundlerParams + * + * A parameter to a bundler. + * + * Also contains static definitions of all of the common bundler parameters. + * (additional platform specific and mode specific bundler parameters + * are defined in each of the specific bundlers) + * + * Also contains static methods that operate on maps of parameters. + */ +public class StandardBundlerParam extends BundlerParamInfo { + + private static final ResourceBundle I18N = ResourceBundle.getBundle( + "jdk.jpackager.internal.resources.StandardBundlerParam"); + private static final String JAVABASEJMOD = "java.base.jmod"; + + public StandardBundlerParam(String name, String description, String id, + Class valueType, + Function, T> defaultValueFunction, + BiFunction, T> stringConverter) + { + this.name = name; + this.description = description; + this.id = id; + this.valueType = valueType; + this.defaultValueFunction = defaultValueFunction; + this.stringConverter = stringConverter; + } + + public static final StandardBundlerParam APP_RESOURCES = + new StandardBundlerParam<>( + I18N.getString("param.app-resources.name"), + I18N.getString("param.app-resource.description"), + BundleParams.PARAM_APP_RESOURCES, + RelativeFileSet.class, + null, // no default. Required parameter + null // no string translation, + // tool must provide complex type + ); + + @SuppressWarnings("unchecked") + public static final + StandardBundlerParam> APP_RESOURCES_LIST = + new StandardBundlerParam<>( + I18N.getString("param.app-resources-list.name"), + I18N.getString("param.app-resource-list.description"), + BundleParams.PARAM_APP_RESOURCES + "List", + (Class>) (Object) List.class, + // Default is appResources, as a single item list + p -> new ArrayList<>(Collections.singletonList( + APP_RESOURCES.fetchFrom(p))), + StandardBundlerParam::createAppResourcesListFromString + ); + + public static final StandardBundlerParam SOURCE_DIR = + new StandardBundlerParam<>( + I18N.getString("param.source-dir.name"), + I18N.getString("param.source-dir.description"), + Arguments.CLIOptions.INPUT.getId(), + String.class, + p -> null, + (s, p) -> { + String value = String.valueOf(s); + if (value.charAt(value.length() - 1) == + File.separatorChar) { + return value.substring(0, value.length() - 1); + } + else { + return value; + } + } + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> SOURCE_FILES = + new StandardBundlerParam<>( + I18N.getString("param.source-files.name"), + I18N.getString("param.source-files.description"), + Arguments.CLIOptions.FILES.getId(), + (Class>) (Object) List.class, + p -> null, + (s, p) -> null + ); + + // note that each bundler is likely to replace this one with + // their own converter + public static final StandardBundlerParam MAIN_JAR = + new StandardBundlerParam<>( + I18N.getString("param.main-jar.name"), + I18N.getString("param.main-jar.description"), + Arguments.CLIOptions.MAIN_JAR.getId(), + RelativeFileSet.class, + params -> { + extractMainClassInfoFromAppResources(params); + return (RelativeFileSet) params.get("mainJar"); + }, + (s, p) -> getMainJar(s, p) + ); + + // TODO: test CLASSPATH jar manifest Attributet + public static final StandardBundlerParam CLASSPATH = + new StandardBundlerParam<>( + I18N.getString("param.classpath.name"), + I18N.getString("param.classpath.description"), + "classpath", + String.class, + params -> { + extractMainClassInfoFromAppResources(params); + String cp = (String) params.get("classpath"); + return cp == null ? "" : cp; + }, + (s, p) -> s.replace(File.pathSeparator, " ") + ); + + public static final StandardBundlerParam MAIN_CLASS = + new StandardBundlerParam<>( + I18N.getString("param.main-class.name"), + I18N.getString("param.main-class.description"), + Arguments.CLIOptions.APPCLASS.getId(), + String.class, + params -> { + if (Arguments.CREATE_JRE_INSTALLER.fetchFrom(params)) { + return null; + } + extractMainClassInfoFromAppResources(params); + String s = (String) params.get( + BundleParams.PARAM_APPLICATION_CLASS); + if (s == null) { + s = JLinkBundlerHelper.getMainClass(params); + } + return s; + }, + (s, p) -> s + ); + + public static final StandardBundlerParam APP_NAME = + new StandardBundlerParam<>( + I18N.getString("param.app-name.name"), + I18N.getString("param.app-name.description"), + Arguments.CLIOptions.NAME.getId(), + String.class, + params -> { + String s = MAIN_CLASS.fetchFrom(params); + if (s == null) return null; + + int idx = s.lastIndexOf("."); + if (idx >= 0) { + return s.substring(idx+1); + } + return s; + }, + (s, p) -> s + ); + + private static Pattern TO_FS_NAME = Pattern.compile("\\s|[\\\\/?:*<>|]"); + // keep out invalid/undesireable filename characters + + public static final StandardBundlerParam APP_FS_NAME = + new StandardBundlerParam<>( + I18N.getString("param.app-fs-name.name"), + I18N.getString("param.app-fs-name.description"), + "name.fs", + String.class, + params -> TO_FS_NAME.matcher( + APP_NAME.fetchFrom(params)).replaceAll(""), + (s, p) -> s + ); + + public static final StandardBundlerParam ICON = + new StandardBundlerParam<>( + I18N.getString("param.icon-file.name"), + I18N.getString("param.icon-file.description"), + Arguments.CLIOptions.ICON.getId(), + File.class, + params -> null, + (s, p) -> new File(s) + ); + + public static final StandardBundlerParam VENDOR = + new StandardBundlerParam<>( + I18N.getString("param.vendor.name"), + I18N.getString("param.vendor.description"), + Arguments.CLIOptions.VENDOR.getId(), + String.class, + params -> I18N.getString("param.vendor.default"), + (s, p) -> s + ); + + public static final StandardBundlerParam CATEGORY = + new StandardBundlerParam<>( + I18N.getString("param.category.name"), + I18N.getString("param.category.description"), + Arguments.CLIOptions.CATEGORY.getId(), + String.class, + params -> I18N.getString("param.category.default"), + (s, p) -> s + ); + + public static final StandardBundlerParam DESCRIPTION = + new StandardBundlerParam<>( + I18N.getString("param.description.name"), + I18N.getString("param.description.description"), + Arguments.CLIOptions.DESCRIPTION.getId(), + String.class, + params -> params.containsKey(APP_NAME.getID()) + ? APP_NAME.fetchFrom(params) + : I18N.getString("param.description.default"), + (s, p) -> s + ); + + public static final StandardBundlerParam COPYRIGHT = + new StandardBundlerParam<>( + I18N.getString("param.copyright.name"), + I18N.getString("param.copyright.description"), + Arguments.CLIOptions.COPYRIGHT.getId(), + String.class, + params -> MessageFormat.format(I18N.getString( + "param.copyright.default"), new Date()), + (s, p) -> s + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> ARGUMENTS = + new StandardBundlerParam<>( + I18N.getString("param.arguments.name"), + I18N.getString("param.arguments.description"), + Arguments.CLIOptions.ARGUMENTS.getId(), + (Class>) (Object) List.class, + params -> Collections.emptyList(), + (s, p) -> splitStringWithEscapes(s) + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> JVM_OPTIONS = + new StandardBundlerParam<>( + I18N.getString("param.jvm-options.name"), + I18N.getString("param.jvm-options.description"), + Arguments.CLIOptions.JVM_ARGS.getId(), + (Class>) (Object) List.class, + params -> Collections.emptyList(), + (s, p) -> Arrays.asList(s.split("\n\n")) + ); + + @SuppressWarnings("unchecked") + public static final + StandardBundlerParam> JVM_PROPERTIES = + new StandardBundlerParam<>( + I18N.getString("param.jvm-system-properties.name"), + I18N.getString("param.jvm-system-properties.description"), + "jvmProperties", + (Class>) (Object) Map.class, + params -> Collections.emptyMap(), + (s, params) -> { + Map map = new HashMap<>(); + try { + Properties p = new Properties(); + p.load(new StringReader(s)); + for (Map.Entry entry : p.entrySet()) { + map.put((String)entry.getKey(), + (String)entry.getValue()); + } + } catch (IOException e) { + e.printStackTrace(); + } + return map; + } + ); + + public static final StandardBundlerParam TITLE = + new StandardBundlerParam<>( + I18N.getString("param.title.name"), + I18N.getString("param.title.description"), + BundleParams.PARAM_TITLE, + String.class, + APP_NAME::fetchFrom, + (s, p) -> s + ); + + // note that each bundler is likely to replace this one with + // their own converter + public static final StandardBundlerParam VERSION = + new StandardBundlerParam<>( + I18N.getString("param.version.name"), + I18N.getString("param.version.description"), + Arguments.CLIOptions.VERSION.getId(), + String.class, + params -> I18N.getString("param.version.default"), + (s, p) -> s + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> LICENSE_FILE = + new StandardBundlerParam<>( + I18N.getString("param.license-file.name"), + I18N.getString("param.license-file.description"), + Arguments.CLIOptions.LICENSE_FILE.getId(), + (Class>)(Object)List.class, + params -> Collections.emptyList(), + (s, p) -> Arrays.asList(s.split(",")) + ); + + public static final StandardBundlerParam BUILD_ROOT = + new StandardBundlerParam<>( + I18N.getString("param.build-root.name"), + I18N.getString("param.build-root.description"), + Arguments.CLIOptions.BUILD_ROOT.getId(), + File.class, + params -> { + try { + return Files.createTempDirectory( + "jdk.jpackager").toFile(); + } catch (IOException ioe) { + return null; + } + }, + (s, p) -> new File(s) + ); + + public static final StandardBundlerParam IDENTIFIER = + new StandardBundlerParam<>( + I18N.getString("param.identifier.name"), + I18N.getString("param.identifier.description"), + Arguments.CLIOptions.IDENTIFIER.getId(), + String.class, + params -> { + String s = MAIN_CLASS.fetchFrom(params); + if (s == null) return null; + + int idx = s.lastIndexOf("."); + if (idx >= 1) { + return s.substring(0, idx); + } + return s; + }, + (s, p) -> s + ); + + public static final StandardBundlerParam PREFERENCES_ID = + new StandardBundlerParam<>( + I18N.getString("param.preferences-id.name"), + I18N.getString("param.preferences-id.description"), + "preferencesID", + String.class, + p -> Optional.ofNullable(IDENTIFIER.fetchFrom(p)). + orElse("").replace('.', '/'), + (s, p) -> s + ); + + public static final StandardBundlerParam VERBOSE = + new StandardBundlerParam<>( + I18N.getString("param.verbose.name"), + I18N.getString("param.verbose.description"), + Arguments.CLIOptions.VERBOSE.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, and we actually do want null + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? + true : Boolean.valueOf(s) + ); + + public static final StandardBundlerParam FORCE = + new StandardBundlerParam<>( + I18N.getString("param.force.name"), + I18N.getString("param.force.description"), + Arguments.CLIOptions.FORCE.getId(), + Boolean.class, + params -> false, + // valueOf(null) is false, and we actually do want null + (s, p) -> (s == null || "null".equalsIgnoreCase(s)) ? + true : Boolean.valueOf(s) + ); + + public static final StandardBundlerParam DROP_IN_RESOURCES_ROOT = + new StandardBundlerParam<>( + I18N.getString("param.drop-in-resources-root.name"), + I18N.getString("param.drop-in-resources-root.description"), + "dropinResourcesRoot", + File.class, + params -> new File("."), + (s, p) -> new File(s) + ); + + public static final BundlerParamInfo INSTALL_DIR = + new StandardBundlerParam<>( + I18N.getString("param.install-dir.name"), + I18N.getString("param.install-dir.description"), + Arguments.CLIOptions.INSTALL_DIR.getId(), + String.class, + params -> null, + (s, p) -> s + ); + + public static final StandardBundlerParam PREDEFINED_APP_IMAGE = + new StandardBundlerParam<>( + I18N.getString("param.predefined-app-image.name"), + I18N.getString("param.predefined-app-image.description"), + Arguments.CLIOptions.PREDEFINED_APP_IMAGE.getId(), + File.class, + params -> null, + (s, p) -> new File(s)); + + public static final StandardBundlerParam PREDEFINED_RUNTIME_IMAGE = + new StandardBundlerParam<>( + I18N.getString("param.predefined-runtime-image.name"), + I18N.getString("param.predefined-runtime-image.description"), + Arguments.CLIOptions.PREDEFINED_RUNTIME_IMAGE.getId(), + File.class, + params -> null, + (s, p) -> new File(s)); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam>> SECONDARY_LAUNCHERS = + new StandardBundlerParam<>( + I18N.getString("param.secondary-launchers.name"), + I18N.getString("param.secondary-launchers.description"), + Arguments.CLIOptions.SECONDARY_LAUNCHER.getId(), + (Class>>) (Object) + List.class, + params -> new ArrayList<>(1), + // valueOf(null) is false, and we actually do want null + (s, p) -> null + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam + >> FILE_ASSOCIATIONS = + new StandardBundlerParam<>( + I18N.getString("param.file-associations.name"), + I18N.getString("param.file-associations.description"), + Arguments.CLIOptions.FILE_ASSOCIATIONS.getId(), + (Class>>) (Object) + List.class, + params -> new ArrayList<>(1), + // valueOf(null) is false, and we actually do want null + (s, p) -> null + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> FA_EXTENSIONS = + new StandardBundlerParam<>( + I18N.getString("param.fa-extension.name"), + I18N.getString("param.fa-extension.description"), + "fileAssociation.extension", + (Class>) (Object) List.class, + params -> null, // null means not matched to an extension + (s, p) -> Arrays.asList(s.split("(,|\\s)+")) + ); + + @SuppressWarnings("unchecked") + public static final StandardBundlerParam> FA_CONTENT_TYPE = + new StandardBundlerParam<>( + I18N.getString("param.fa-content-type.name"), + I18N.getString("param.fa-content-type.description"), + "fileAssociation.contentType", + (Class>) (Object) List.class, + params -> null, + // null means not matched to a content/mime type + (s, p) -> Arrays.asList(s.split("(,|\\s)+")) + ); + + public static final StandardBundlerParam FA_DESCRIPTION = + new StandardBundlerParam<>( + I18N.getString("param.fa-description.name"), + I18N.getString("param.fa-description.description"), + "fileAssociation.description", + String.class, + params -> APP_NAME.fetchFrom(params) + " File", + null + ); + + public static final StandardBundlerParam FA_ICON = + new StandardBundlerParam<>( + I18N.getString("param.fa-icon.name"), + I18N.getString("param.fa-icon.description"), + "fileAssociation.icon", + File.class, + ICON::fetchFrom, + (s, p) -> new File(s) + ); + + @SuppressWarnings("unchecked") + public static final BundlerParamInfo> MODULE_PATH = + new StandardBundlerParam<>( + I18N.getString("param.module-path.name"), + I18N.getString("param.module-path.description"), + Arguments.CLIOptions.MODULE_PATH.getId(), + (Class>) (Object)List.class, + p -> { return getDefaultModulePath(); }, + (s, p) -> { + List modulePath = Arrays.asList(s + .split(File.pathSeparator)).stream() + .map(ss -> new File(ss).toPath()) + .collect(Collectors.toList()); + Path javaBasePath = null; + if (modulePath != null) { + javaBasePath = JLinkBundlerHelper + .findPathOfModule(modulePath, JAVABASEJMOD); + } else { + modulePath = new ArrayList(); + } + + // Add the default JDK module path to the module path. + if (javaBasePath == null) { + List jdkModulePath = getDefaultModulePath(); + + if (jdkModulePath != null) { + modulePath.addAll(jdkModulePath); + javaBasePath = + JLinkBundlerHelper.findPathOfModule( + modulePath, JAVABASEJMOD); + } + } + + if (javaBasePath == null || + !Files.exists(javaBasePath)) { + Log.error(String.format(I18N.getString( + "warning.no.jdk.modules.found"))); + } + + return modulePath; + }); + + public static final BundlerParamInfo MODULE = + new StandardBundlerParam<>( + I18N.getString("param.main.module.name"), + I18N.getString("param.main.module.description"), + Arguments.CLIOptions.MODULE.getId(), + String.class, + p -> null, + (s, p) -> { + return String.valueOf(s); + }); + + @SuppressWarnings("unchecked") + public static final BundlerParamInfo> ADD_MODULES = + new StandardBundlerParam<>( + I18N.getString("param.add-modules.name"), + I18N.getString("param.add-modules.description"), + Arguments.CLIOptions.ADD_MODULES.getId(), + (Class>) (Object) Set.class, + p -> new LinkedHashSet(), + (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) + ); + + @SuppressWarnings("unchecked") + public static final BundlerParamInfo> LIMIT_MODULES = + new StandardBundlerParam<>( + I18N.getString("param.limit-modules.name"), + I18N.getString("param.limit-modules.description"), + Arguments.CLIOptions.LIMIT_MODULES.getId(), + (Class>) (Object) Set.class, + p -> new LinkedHashSet(), + (s, p) -> new LinkedHashSet<>(Arrays.asList(s.split(","))) + ); + + public static final BundlerParamInfo STRIP_NATIVE_COMMANDS = + new StandardBundlerParam<>( + I18N.getString("param.strip-executables.name"), + I18N.getString("param.strip-executables.description"), + Arguments.CLIOptions.STRIP_NATIVE_COMMANDS.getId(), + Boolean.class, + p -> Boolean.FALSE, + (s, p) -> Boolean.valueOf(s) + ); + + public static final BundlerParamInfo SINGLETON = + new StandardBundlerParam<> ( + I18N.getString("param.singleton.name"), + I18N.getString("param.singleton.description"), + Arguments.CLIOptions.SINGLETON.getId(), + Boolean.class, + params -> Boolean.FALSE, + (s, p) -> Boolean.valueOf(s) + ); + + public static File getPredefinedAppImage(Map p) { + File applicationImage = null; + if (PREDEFINED_APP_IMAGE.fetchFrom(p) != null) { + applicationImage = PREDEFINED_APP_IMAGE.fetchFrom(p); + Log.debug("Using App Image from " + applicationImage); + if (!applicationImage.exists()) { + throw new RuntimeException( + MessageFormat.format(I18N.getString( + "message.app-image-dir-does-not-exist"), + PREDEFINED_APP_IMAGE.getID(), + applicationImage.toString())); + } + } + return applicationImage; + } + + public static void copyPredefinedRuntimeImage( + Map p, + AbstractAppImageBuilder appBuilder) + throws IOException , ConfigException { + File image = PREDEFINED_RUNTIME_IMAGE.fetchFrom(p); + if (!image.exists()) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "message.runtime-image-dir-does-not-exist"), + PREDEFINED_RUNTIME_IMAGE.getID(), + image.toString()), + MessageFormat.format(I18N.getString( + "message.runtime-image-dir-does-not-exist.advice"), + PREDEFINED_RUNTIME_IMAGE.getID())); + } + IOUtils.copyRecursive(image.toPath(), appBuilder.getRoot()); + appBuilder.prepareApplicationFiles(); + } + + public static void extractMainClassInfoFromAppResources( + Map params) { + boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); + boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); + boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); + boolean hasModule = params.containsKey(MODULE.getID()); + boolean jreInstaller = + params.containsKey(Arguments.CREATE_JRE_INSTALLER.getID()); + + if (hasMainClass && hasMainJar && hasMainJarClassPath || hasModule || + jreInstaller) { + return; + } + + // it's a pair. + // The [0] is the srcdir [1] is the file relative to sourcedir + List filesToCheck = new ArrayList<>(); + + if (hasMainJar) { + RelativeFileSet rfs = MAIN_JAR.fetchFrom(params); + for (String s : rfs.getIncludedFiles()) { + filesToCheck.add( + new String[] {rfs.getBaseDirectory().toString(), s}); + } + } else if (hasMainJarClassPath) { + for (String s : CLASSPATH.fetchFrom(params).split("\\s+")) { + if (APP_RESOURCES.fetchFrom(params) != null) { + filesToCheck.add( + new String[] {APP_RESOURCES.fetchFrom(params) + .getBaseDirectory().toString(), s}); + } + } + } else { + List rfsl = APP_RESOURCES_LIST.fetchFrom(params); + if (rfsl == null || rfsl.isEmpty()) { + return; + } + for (RelativeFileSet rfs : rfsl) { + if (rfs == null) continue; + + for (String s : rfs.getIncludedFiles()) { + filesToCheck.add( + new String[]{rfs.getBaseDirectory().toString(), s}); + } + } + } + + // presume the set iterates in-order + for (String[] fnames : filesToCheck) { + try { + // only sniff jars + if (!fnames[1].toLowerCase().endsWith(".jar")) continue; + + File file = new File(fnames[0], fnames[1]); + // that actually exist + if (!file.exists()) continue; + + try (JarFile jf = new JarFile(file)) { + Manifest m = jf.getManifest(); + Attributes attrs = (m != null) ? + m.getMainAttributes() : null; + + if (attrs != null) { + if (!hasMainJar) { + if (fnames[0] == null) { + fnames[0] = file.getParentFile().toString(); + } + params.put(MAIN_JAR.getID(), new RelativeFileSet( + new File(fnames[0]), + new LinkedHashSet<>(Collections + .singletonList(file)))); + } + if (!hasMainJarClassPath) { + String cp = + attrs.getValue(Attributes.Name.CLASS_PATH); + params.put(CLASSPATH.getID(), + cp == null ? "" : cp); + } + break; + } + } + } catch (IOException ignore) { + ignore.printStackTrace(); + } + } + } + + public static void validateMainClassInfoFromAppResources( + Map params) throws ConfigException { + boolean hasMainClass = params.containsKey(MAIN_CLASS.getID()); + boolean hasMainJar = params.containsKey(MAIN_JAR.getID()); + boolean hasMainJarClassPath = params.containsKey(CLASSPATH.getID()); + boolean hasModule = params.containsKey(MODULE.getID()); + boolean hasAppImage = params.containsKey(PREDEFINED_APP_IMAGE.getID()); + boolean jreInstaller = + params.containsKey(Arguments.CREATE_JRE_INSTALLER.getID()); + + if (hasMainClass && hasMainJar && hasMainJarClassPath || + hasModule || jreInstaller || hasAppImage) { + return; + } + + extractMainClassInfoFromAppResources(params); + + if (!params.containsKey(MAIN_CLASS.getID())) { + if (hasMainJar) { + throw new ConfigException( + MessageFormat.format(I18N.getString( + "error.no-main-class-with-main-jar"), + MAIN_JAR.fetchFrom(params)), + MessageFormat.format(I18N.getString( + "error.no-main-class-with-main-jar.advice"), + MAIN_JAR.fetchFrom(params))); + } else if (hasMainJarClassPath) { + throw new ConfigException( + I18N.getString("error.no-main-class-with-classpath"), + I18N.getString( + "error.no-main-class-with-classpath.advice")); + } else { + throw new ConfigException( + I18N.getString("error.no-main-class"), + I18N.getString("error.no-main-class.advice")); + } + } + } + + + private static List splitStringWithEscapes(String s) { + List l = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + boolean escaped = false; + for (char c : s.toCharArray()) { + if (escaped) { + current.append(c); + } else if ('"' == c) { + quoted = !quoted; + } else if (!quoted && Character.isWhitespace(c)) { + l.add(current.toString()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + l.add(current.toString()); + return l; + } + + private static List + createAppResourcesListFromString(String s, + Map objectObjectMap) { + List result = new ArrayList<>(); + for (String path : s.split("[:;]")) { + File f = new File(path); + if (f.getName().equals("*") || path.endsWith("/") || + path.endsWith("\\")) { + if (f.getName().equals("*")) { + f = f.getParentFile(); + } + Set theFiles = new HashSet<>(); + try { + Files.walk(f.toPath()) + .filter(Files::isRegularFile) + .forEach(p -> theFiles.add(p.toFile())); + } catch (IOException e) { + e.printStackTrace(); + } + result.add(new RelativeFileSet(f, theFiles)); + } else { + result.add(new RelativeFileSet(f.getParentFile(), + Collections.singleton(f))); + } + } + return result; + } + + private static RelativeFileSet getMainJar( + String moduleName, Map params) { + for (RelativeFileSet rfs : APP_RESOURCES_LIST.fetchFrom(params)) { + File appResourcesRoot = rfs.getBaseDirectory(); + File mainJarFile = new File(appResourcesRoot, moduleName); + + if (mainJarFile.exists()) { + return new RelativeFileSet(appResourcesRoot, + new LinkedHashSet<>(Collections.singletonList( + mainJarFile))); + } + else { + List modulePath = MODULE_PATH.fetchFrom(params); + Path modularJarPath = JLinkBundlerHelper.findPathOfModule( + modulePath, moduleName); + + if (modularJarPath != null && Files.exists(modularJarPath)) { + return new RelativeFileSet(appResourcesRoot, + new LinkedHashSet<>(Collections.singletonList( + modularJarPath.toFile()))); + } + } + } + + throw new IllegalArgumentException( + new ConfigException(MessageFormat.format(I18N.getString( + "error.main-jar-does-not-exist"), + moduleName), I18N.getString( + "error.main-jar-does-not-exist.advice"))); + } + + public static List getDefaultModulePath() { + List result = new ArrayList(); + Path jdkModulePath = Paths.get( + System.getProperty("java.home"), "jmods").toAbsolutePath(); + + if (jdkModulePath != null && Files.exists(jdkModulePath)) { + result.add(jdkModulePath); + } + else { + // On a developer build the JDK Home isn't where we expect it + // relative to the jmods directory. Do some extra + // processing to find it. + Map env = System.getenv(); + + if (env.containsKey("JDK_HOME")) { + jdkModulePath = Paths.get(env.get("JDK_HOME"), + ".." + File.separator + "images" + + File.separator + "jmods").toAbsolutePath(); + + if (jdkModulePath != null && Files.exists(jdkModulePath)) { + result.add(jdkModulePath); + } + } + } + + return result; + } +}