1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.maven.internal.impl; 20 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.UncheckedIOException; 24 import java.lang.module.ModuleDescriptor; 25 import java.nio.file.Files; 26 import java.nio.file.Path; 27 import java.util.Collection; 28 import java.util.Collections; 29 import java.util.HashMap; 30 import java.util.Map; 31 import java.util.jar.Attributes; 32 import java.util.jar.JarFile; 33 import java.util.jar.Manifest; 34 import java.util.stream.Stream; 35 import java.util.zip.ZipEntry; 36 37 import org.apache.maven.api.JavaPathType; 38 import org.apache.maven.api.annotations.Nonnull; 39 40 /** 41 * Information about the modules contained in a path element. 42 * The path element may be a JAR file or a directory. Directories may use either package hierarchy 43 * or module hierarchy, but not module source hierarchy. The latter is excluded because this class 44 * is for path elements of compiled codes. 45 */ 46 class PathModularization { 47 /** 48 * A unique constant for all non-modular dependencies. 49 */ 50 public static final PathModularization NONE = new PathModularization(); 51 52 /** 53 * Name of the file to use as a sentinel value for deciding if a directory or a JAR is modular. 54 */ 55 private static final String MODULE_INFO = "module-info.class"; 56 57 /** 58 * The attribute for automatic module name in {@code META-INF/MANIFEST.MF} files. 59 */ 60 private static final Attributes.Name AUTO_MODULE_NAME = new Attributes.Name("Automatic-Module-Name"); 61 62 /** 63 * Filename of the path specified at construction time. 64 */ 65 private final String filename; 66 67 /** 68 * Module information for the path specified at construction time. 69 * This map is usually either empty if no module was found, or a singleton map. 70 * It may however contain more than one entry if module hierarchy was detected, 71 * in which case there is one key per sub-directory. 72 * 73 * <p>This map may contain null values if the constructor was invoked with {@code resolve} 74 * parameter set to false. This is more efficient when only the module existence needs to 75 * be tested, and module descriptors are not needed.</p> 76 * 77 * @see #getModuleNames() 78 */ 79 private final Map<Path, String> descriptors; 80 81 /** 82 * Whether module hierarchy was detected. If false, then package hierarchy is assumed. 83 * In a package hierarchy, the {@linkplain #descriptors} map has either zero or one entry. 84 * In a module hierarchy, the descriptors map may have an arbitrary number of entries, 85 * including one (so the map size cannot be used as a criterion). 86 * 87 * @see #isModuleHierarchy() 88 */ 89 private final boolean isModuleHierarchy; 90 91 /** 92 * Constructs an empty instance for non-modular dependencies. 93 * 94 * @see #NONE 95 */ 96 private PathModularization() { 97 filename = "(none)"; 98 descriptors = Collections.emptyMap(); 99 isModuleHierarchy = false; 100 } 101 102 /** 103 * Finds module information in the given JAR file, output directory, or test output directory. 104 * If no module is found, or if module information cannot be extracted, then this constructor 105 * builds an empty map. 106 * 107 * <p>If the {@code resolve} parameter value is {@code false}, then some or all map values may 108 * be null instead of the actual module name. This option can avoid the cost of reading module 109 * descriptors when only the modules existence needs to be verified.</p> 110 * 111 * <p><b>Algorithm:</b> 112 * If the given path is a directory, then there is a choice: 113 * </p> 114 * <ul> 115 * <li><b>Package hierarchy:</b> if a {@code module-info.class} file is found at the root, 116 * then builds a singleton map with the module name declared in that descriptor.</li> 117 * <li><b>Module hierarchy:</b> if {@code module-info.class} files are found in sub-directories, 118 * at a deep intentionally restricted to one level, then builds a map of module names found 119 * in the descriptor of each sub-directory.</li> 120 * </ul> 121 * 122 * Otherwise if the given path is a JAR file, then there is a choice: 123 * <ul> 124 * <li>If a {@code module-info.class} file is found in the root directory or in a 125 * {@code "META-INF/versions/{n}/"} subdirectory, builds a singleton map with 126 * the module name declared in that descriptor.</li> 127 * <li>Otherwise if an {@code "Automatic-Module-Name"} attribute is declared in the 128 * {@code META-INF/MANIFEST.MF} file, builds a singleton map with the value of that attribute.</li> 129 * </ul> 130 * 131 * Otherwise builds an empty map. 132 * 133 * @param path directory or JAR file to test 134 * @param resolve whether the module names are requested. If false, null values may be used instead 135 * @throws IOException if an error occurred while reading the JAR file or the module descriptor 136 */ 137 PathModularization(Path path, boolean resolve) throws IOException { 138 filename = path.getFileName().toString(); 139 if (Files.isDirectory(path)) { 140 /* 141 * Package hierarchy: only one module with descriptor at the root. 142 * This is the layout of output directories in projects using the 143 * classical (Java 8 and before) way to organize source files. 144 */ 145 Path file = path.resolve(MODULE_INFO); 146 if (Files.isRegularFile(file)) { 147 String name = null; 148 if (resolve) { 149 try (InputStream in = Files.newInputStream(file)) { 150 name = getModuleName(in); 151 } 152 } 153 descriptors = Collections.singletonMap(file, name); 154 isModuleHierarchy = false; 155 return; 156 } 157 /* 158 * Module hierarchy: many modules, one per directory, with descriptor at the root of the sub-directory. 159 * This is the layout of output directories in projects using the new (Java 9 and later) way to organize 160 * source files. 161 */ 162 if (Files.isDirectory(file)) { 163 Map<Path, String> names = new HashMap<>(); 164 try (Stream<Path> subdirs = Files.list(file)) { 165 subdirs.filter(Files::isDirectory).forEach((subdir) -> { 166 Path mf = subdir.resolve(MODULE_INFO); 167 if (Files.isRegularFile(mf)) { 168 String name = null; 169 if (resolve) { 170 try (InputStream in = Files.newInputStream(mf)) { 171 name = getModuleName(in); 172 } catch (IOException e) { 173 throw new UncheckedIOException(e); 174 } 175 } 176 names.put(mf, name); 177 } 178 }); 179 } catch (UncheckedIOException e) { 180 throw e.getCause(); 181 } 182 if (!names.isEmpty()) { 183 descriptors = Collections.unmodifiableMap(names); 184 isModuleHierarchy = true; 185 return; 186 } 187 } 188 } else if (Files.isRegularFile(path)) { 189 /* 190 * JAR file: can contain only one module, with descriptor at the root. 191 * If no descriptor, the "Automatic-Module-Name" manifest attribute is 192 * taken as a fallback. 193 */ 194 try (JarFile jar = new JarFile(path.toFile())) { 195 ZipEntry entry = jar.getEntry(MODULE_INFO); 196 if (entry != null) { 197 String name = null; 198 if (resolve) { 199 try (InputStream in = jar.getInputStream(entry)) { 200 name = getModuleName(in); 201 } 202 } 203 descriptors = Collections.singletonMap(path, name); 204 isModuleHierarchy = false; 205 return; 206 } 207 // No module descriptor, check manifest file. 208 Manifest mf = jar.getManifest(); 209 if (mf != null) { 210 Object name = mf.getMainAttributes().get(AUTO_MODULE_NAME); 211 if (name instanceof String) { 212 descriptors = Collections.singletonMap(path, (String) name); 213 isModuleHierarchy = false; 214 return; 215 } 216 } 217 } 218 } 219 descriptors = Collections.emptyMap(); 220 isModuleHierarchy = false; 221 } 222 223 /** 224 * {@return the module name declared in the given {@code module-info} descriptor}. 225 * The input stream may be for a file or for an entry in a JAR file. 226 */ 227 @Nonnull 228 private static String getModuleName(InputStream in) throws IOException { 229 return ModuleDescriptor.read(in).name(); 230 } 231 232 /** 233 * {@return the type of path detected}. The return value is {@link JavaPathType#MODULES} 234 * if the dependency is a modular JAR file or a directory containing module descriptor(s), 235 * or {@link JavaPathType#CLASSES} otherwise. A JAR file without module descriptor but with 236 * an "Automatic-Module-Name" manifest attribute is considered modular. 237 */ 238 public JavaPathType getPathType() { 239 return descriptors.isEmpty() ? JavaPathType.CLASSES : JavaPathType.MODULES; 240 } 241 242 /** 243 * If the module has no name, adds the filename of the JAR file in the given collection. 244 * This method should be invoked for dependencies placed on {@link JavaPathType#MODULES} 245 * for preparing a warning asking to not deploy the build artifact on a public repository. 246 * If the module has an explicit name either with a {@code module-info.class} file or with 247 * an {@code "Automatic-Module-Name"} attribute in the {@code META-INF/MANIFEST.MF} file, 248 * then this method does nothing. 249 */ 250 public void addIfFilenameBasedAutomodules(Collection<String> automodulesDetected) { 251 if (descriptors.isEmpty()) { 252 automodulesDetected.add(filename); 253 } 254 } 255 256 /** 257 * {@return whether module hierarchy was detected}. If {@code false}, then package hierarchy is assumed. 258 * In a package hierarchy, the {@linkplain #getModuleNames()} map of modules has either zero or one entry. 259 * In a module hierarchy, the descriptors map may have an arbitrary number of entries, 260 * including one (so the map size cannot be used as a criterion). 261 */ 262 public boolean isModuleHierarchy() { 263 return isModuleHierarchy; 264 } 265 266 /** 267 * {@return the module names for the path specified at construction time}. 268 * This map is usually either empty if no module was found, or a singleton map. 269 * It may however contain more than one entry if module hierarchy was detected, 270 * in which case there is one key per sub-directory. 271 * 272 * <p>This map may contain null values if the constructor was invoked with {@code resolve} 273 * parameter set to false. This is more efficient when only the module existence needs to 274 * be tested, and module descriptors are not needed.</p> 275 */ 276 @Nonnull 277 public Map<Path, String> getModuleNames() { 278 return descriptors; 279 } 280 281 /** 282 * {@return whether the dependency contains a module of the given name}. 283 */ 284 public boolean containsModule(String name) { 285 return descriptors.containsValue(name); 286 } 287 288 /** 289 * {@return a string representation of this object for debugging purposes}. 290 * This string representation may change in any future version. 291 */ 292 @Override 293 public String toString() { 294 return getClass().getCanonicalName() + '[' + filename + ']'; 295 } 296 }