1 /* 2 * Copyright (c) 2020, 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 package jdk.incubator.jpackage.internal; 26 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.Reader; 30 import java.lang.module.ModuleDescriptor; 31 import java.lang.module.ModuleReference; 32 import java.nio.file.Files; 33 import java.nio.file.InvalidPathException; 34 import java.nio.file.Path; 35 import java.text.MessageFormat; 36 import java.util.Collections; 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Optional; 41 import java.util.Properties; 42 import java.util.Set; 43 import java.util.function.Supplier; 44 import java.util.jar.Attributes; 45 import java.util.jar.JarFile; 46 import java.util.jar.Manifest; 47 import java.util.stream.Collectors; 48 import java.util.stream.Stream; 49 import static jdk.incubator.jpackage.internal.StandardBundlerParam.PREDEFINED_RUNTIME_IMAGE; 50 51 /** 52 * Extracts data needed to run application from parameters. 53 */ 54 final class LauncherData { 55 boolean isModular() { 56 return moduleInfo != null; 57 } 58 59 String qualifiedClassName() { 60 return qualifiedClassName; 61 } 62 63 String packageName() { 64 int sepIdx = qualifiedClassName.lastIndexOf('.'); 65 if (sepIdx < 0) { 66 return ""; 67 } 68 return qualifiedClassName.substring(sepIdx + 1); 69 } 70 71 String moduleName() { 72 verifyIsModular(true); 73 return moduleInfo.name; 74 } 75 76 List<Path> modulePath() { 77 verifyIsModular(true); 78 return modulePath; 79 } 80 81 Path mainJarName() { 82 verifyIsModular(false); 83 return mainJarName; 84 } 85 86 List<Path> classPath() { 87 return classPath; 88 } 89 90 String getAppVersion() { 91 if (isModular()) { 92 return moduleInfo.version; 93 } 94 95 return null; 96 } 97 98 private LauncherData() { 99 } 100 101 private void verifyIsModular(boolean isModular) { 102 if ((moduleInfo != null) != isModular) { 103 throw new IllegalStateException(); 104 } 105 } 106 107 static LauncherData create(Map<String, ? super Object> params) throws 108 ConfigException, IOException { 109 110 final String mainModule = getMainModule(params); 111 final LauncherData result; 112 if (mainModule == null) { 113 result = createNonModular(params); 114 } else { 115 result = createModular(mainModule, params); 116 } 117 result.initClasspath(params); 118 return result; 119 } 120 121 private static LauncherData createModular(String mainModule, 122 Map<String, ? super Object> params) throws ConfigException, 123 IOException { 124 125 LauncherData launcherData = new LauncherData(); 126 127 final int sepIdx = mainModule.indexOf("/"); 128 final String moduleName; 129 if (sepIdx > 0) { 130 launcherData.qualifiedClassName = mainModule.substring(sepIdx + 1); 131 moduleName = mainModule.substring(0, sepIdx); 132 } else { 133 moduleName = mainModule; 134 } 135 launcherData.modulePath = getModulePath(params); 136 137 // Try to find module in the specified module path list. 138 ModuleReference moduleRef = JLinkBundlerHelper.createModuleFinder( 139 launcherData.modulePath).find(moduleName).orElse(null); 140 141 if (moduleRef != null) { 142 launcherData.moduleInfo = ModuleInfo.fromModuleDescriptor( 143 moduleRef.descriptor()); 144 } else if (params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID())) { 145 // Failed to find module in the specified module path list and 146 // there is external runtime given to jpackage. 147 // Lookup module in this runtime. 148 Path cookedRuntime = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); 149 launcherData.moduleInfo = ModuleInfo.fromCookedRuntime(moduleName, 150 cookedRuntime); 151 } 152 153 if (launcherData.moduleInfo == null) { 154 throw new ConfigException(MessageFormat.format(I18N.getString( 155 "error.no-module-in-path"), moduleName), null); 156 } 157 158 if (launcherData.qualifiedClassName == null) { 159 launcherData.qualifiedClassName = launcherData.moduleInfo.mainClass; 160 if (launcherData.qualifiedClassName == null) { 161 throw new ConfigException(I18N.getString("ERR_NoMainClass"), null); 162 } 163 } 164 165 return launcherData; 166 } 167 168 private static LauncherData createNonModular( 169 Map<String, ? super Object> params) throws ConfigException, IOException { 170 LauncherData launcherData = new LauncherData(); 171 172 launcherData.qualifiedClassName = getMainClass(params); 173 174 launcherData.mainJarName = getMainJarName(params); 175 if (launcherData.mainJarName == null && launcherData.qualifiedClassName 176 == null) { 177 throw new ConfigException(I18N.getString("error.no-main-jar-parameter"), 178 null); 179 } 180 181 Path mainJarDir = StandardBundlerParam.SOURCE_DIR.fetchFrom(params); 182 if (mainJarDir == null && launcherData.qualifiedClassName == null) { 183 throw new ConfigException(I18N.getString("error.no-input-parameter"), 184 null); 185 } 186 187 final Path mainJarPath; 188 if (launcherData.mainJarName != null && mainJarDir != null) { 189 mainJarPath = mainJarDir.resolve(launcherData.mainJarName); 190 if (!Files.exists(mainJarPath)) { 191 throw new ConfigException(MessageFormat.format(I18N.getString( 192 "error.main-jar-does-not-exist"), 193 launcherData.mainJarName), I18N.getString( 194 "error.main-jar-does-not-exist.advice")); 195 } 196 } else { 197 mainJarPath = null; 198 } 199 200 if (launcherData.qualifiedClassName == null) { 201 if (mainJarPath == null) { 202 throw new ConfigException(I18N.getString("error.no-main-class"), 203 I18N.getString("error.no-main-class.advice")); 204 } 205 206 try (JarFile jf = new JarFile(mainJarPath.toFile())) { 207 Manifest m = jf.getManifest(); 208 Attributes attrs = (m != null) ? m.getMainAttributes() : null; 209 if (attrs != null) { 210 launcherData.qualifiedClassName = attrs.getValue( 211 Attributes.Name.MAIN_CLASS); 212 } 213 } 214 } 215 216 if (launcherData.qualifiedClassName == null) { 217 throw new ConfigException(MessageFormat.format(I18N.getString( 218 "error.no-main-class-with-main-jar"), 219 launcherData.mainJarName), MessageFormat.format( 220 I18N.getString( 221 "error.no-main-class-with-main-jar.advice"), 222 launcherData.mainJarName)); 223 } 224 225 return launcherData; 226 } 227 228 private void initClasspath(Map<String, ? super Object> params) 229 throws IOException { 230 Path inputDir = StandardBundlerParam.SOURCE_DIR.fetchFrom(params); 231 if (inputDir == null) { 232 classPath = Collections.emptyList(); 233 } else { 234 try (Stream<Path> walk = Files.walk(inputDir, 1)) { 235 Set<Path> jars = walk.filter(Files::isRegularFile) 236 .filter(file -> file.toString().endsWith(".jar")) 237 .map(Path::getFileName) 238 .collect(Collectors.toSet()); 239 jars.remove(mainJarName); 240 classPath = jars.stream().sorted().collect(Collectors.toList()); 241 } 242 } 243 } 244 245 private static String getMainClass(Map<String, ? super Object> params) { 246 return getStringParam(params, Arguments.CLIOptions.APPCLASS.getId()); 247 } 248 249 private static Path getMainJarName(Map<String, ? super Object> params) 250 throws ConfigException { 251 return getPathParam(params, Arguments.CLIOptions.MAIN_JAR.getId()); 252 } 253 254 private static String getMainModule(Map<String, ? super Object> params) { 255 return getStringParam(params, Arguments.CLIOptions.MODULE.getId()); 256 } 257 258 private static String getStringParam(Map<String, ? super Object> params, 259 String paramName) { 260 Optional<Object> value = Optional.ofNullable(params.get(paramName)); 261 if (value.isPresent()) { 262 return value.get().toString(); 263 } 264 return null; 265 } 266 267 private static <T> T getPathParam(Map<String, ? super Object> params, 268 String paramName, Supplier<T> func) throws ConfigException { 269 try { 270 return func.get(); 271 } catch (InvalidPathException ex) { 272 throw new ConfigException(MessageFormat.format(I18N.getString( 273 "error.not-path-parameter"), paramName, 274 ex.getLocalizedMessage()), null, ex); 275 } 276 } 277 278 private static Path getPathParam(Map<String, ? super Object> params, 279 String paramName) throws ConfigException { 280 return getPathParam(params, paramName, () -> { 281 String value = getStringParam(params, paramName); 282 Path result = null; 283 if (value != null) { 284 result = Path.of(value); 285 } 286 return result; 287 }); 288 } 289 290 private static List<Path> getModulePath(Map<String, ? super Object> params) 291 throws ConfigException { 292 List<Path> modulePath = getPathListParameter(Arguments.CLIOptions.MODULE_PATH.getId(), params); 293 294 if (params.containsKey(PREDEFINED_RUNTIME_IMAGE.getID())) { 295 Path runtimePath = PREDEFINED_RUNTIME_IMAGE.fetchFrom(params); 296 runtimePath = runtimePath.resolve("lib"); 297 modulePath = Stream.of(modulePath, List.of(runtimePath)) 298 .flatMap(List::stream) 299 .collect(Collectors.toUnmodifiableList()); 300 } 301 302 return modulePath; 303 } 304 305 private static List<Path> getPathListParameter(String paramName, 306 Map<String, ? super Object> params) throws ConfigException { 307 return getPathParam(params, paramName, () -> { 308 String value = params.getOrDefault(paramName, "").toString(); 309 return List.of(value.split(File.pathSeparator)).stream() 310 .map(Path::of) 311 .collect(Collectors.toUnmodifiableList()); 312 }); 313 } 314 315 private String qualifiedClassName; 316 private Path mainJarName; 317 private List<Path> classPath; 318 private List<Path> modulePath; 319 private ModuleInfo moduleInfo; 320 321 private static final class ModuleInfo { 322 String name; 323 String version; 324 String mainClass; 325 326 static ModuleInfo fromModuleDescriptor(ModuleDescriptor md) { 327 ModuleInfo result = new ModuleInfo(); 328 result.name = md.name(); 329 result.mainClass = md.mainClass().orElse(null); 330 331 ModuleDescriptor.Version ver = md.version().orElse(null); 332 if (ver != null) { 333 result.version = ver.toString(); 334 } else { 335 result.version = md.rawVersion().orElse(null); 336 } 337 338 return result; 339 } 340 341 static ModuleInfo fromCookedRuntime(String moduleName, 342 Path cookedRuntime) { 343 Objects.requireNonNull(moduleName); 344 345 // We can't extract info about version and main class of a module 346 // linked in external runtime without running ModuleFinder in that 347 // runtime. But this is too much work as the runtime might have been 348 // coocked without native launchers. So just make sure the module 349 // is linked in the runtime by simply analysing the data 350 // of `release` file. 351 352 final Path releaseFile; 353 if (!Platform.isMac()) { 354 releaseFile = cookedRuntime.resolve("release"); 355 } else { 356 // On Mac `cookedRuntime` can be runtime root or runtime home. 357 Path runtimeHome = cookedRuntime.resolve("Contents/Home"); 358 if (!Files.isDirectory(runtimeHome)) { 359 runtimeHome = cookedRuntime; 360 } 361 releaseFile = runtimeHome.resolve("release"); 362 } 363 364 try (Reader reader = Files.newBufferedReader(releaseFile)) { 365 Properties props = new Properties(); 366 props.load(reader); 367 String moduleList = props.getProperty("MODULES"); 368 if (moduleList == null) { 369 return null; 370 } 371 372 if ((moduleList.startsWith("\"") && moduleList.endsWith("\"")) 373 || (moduleList.startsWith("\'") && moduleList.endsWith( 374 "\'"))) { 375 moduleList = moduleList.substring(1, moduleList.length() - 1); 376 } 377 378 if (!List.of(moduleList.split("\\s+")).contains(moduleName)) { 379 return null; 380 } 381 } catch (IOException|IllegalArgumentException ex) { 382 Log.verbose(ex); 383 return null; 384 } 385 386 ModuleInfo result = new ModuleInfo(); 387 result.name = moduleName; 388 389 return result; 390 } 391 } 392 }