Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions enforcer-rules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<!-- test only: generates module-info.class fixtures for the module rules; production code reads
module-info via java.lang.module.ModuleDescriptor (accessed reflectively, see JavaModuleInfoReader). -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
* Resolver helper class.
*/
@Named
class ResolverUtil {
public class ResolverUtil {

private final RepositorySystem repositorySystem;

Expand Down Expand Up @@ -117,7 +117,7 @@ DependencyNode resolveTransitiveDependencies(boolean verbose, boolean excludeOpt
return resolveTransitiveDependencies(verbose, false, excludeOptional, excludedScopes);
}

DependencyNode resolveTransitiveDependencies(
public DependencyNode resolveTransitiveDependencies(
boolean verbose, boolean resolve, boolean excludeOptional, List<String> excludedScopes)
throws EnforcerRuleException {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.enforcer.rules.modules;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
import org.apache.maven.enforcer.rules.AbstractStandardEnforcerRule;
import org.apache.maven.project.MavenProject;

/**
* Base class for rules that inspect the {@code module-info.class} of the project's main output.
* Two output layouts are supported:
*
* <ul>
* <li><b>Classic</b>: the descriptor sits directly in {@code ${project.build.outputDirectory}}
* (one Maven project = one Java module);</li>
* <li><b>Module source hierarchy</b> (Maven&nbsp;4, POM model 4.1.0): one Maven project compiles
* several modules, each to its own subdirectory
* {@code ${project.build.outputDirectory}/<module-name>/module-info.class}.</li>
* </ul>
*
* Subclasses call {@link #moduleOutputs()} and enforce a specific constraint on each returned
* {@link ModuleOutput}.
*/
abstract class AbstractModuleInfoRule extends AbstractStandardEnforcerRule {

protected final MavenProject project;

protected AbstractModuleInfoRule(MavenProject project) {
this.project = project;
}

/** One compiled output that may form a Java module. */
protected static final class ModuleOutput {
private final File root;
private final JavaModuleInfo moduleInfo;

private ModuleOutput(File root, JavaModuleInfo moduleInfo) {
this.root = root;
this.moduleInfo = moduleInfo;
}

/** The directory the module's classes are compiled to. */
File root() {
return root;
}

/** The parsed module descriptor, or {@code null} if {@link #root()} has no {@code module-info.class}. */
JavaModuleInfo moduleInfo() {
return moduleInfo;
}
}

/** The directory the main classes (and any {@code module-info.class}) are compiled to. */
protected File outputDirectory() {
return new File(project.getBuild().getOutputDirectory());
}

/**
* Discover the project's module outputs.
*
* <p>If the output directory itself holds a {@code module-info.class} (classic layout), exactly
* that one output is returned. Otherwise, if at least one first-level subdirectory holds a
* {@code module-info.class} (Maven&nbsp;4 module source hierarchy), every first-level
* subdirectory is returned as one output each — including non-modular ones, whose
* {@link ModuleOutput#moduleInfo()} is {@code null}, so rules can flag them. Failing both, the
* output directory is returned as a single non-modular output.
*
* @throws EnforcerRuleException if a {@code module-info.class} exists but cannot be read
*/
protected List<ModuleOutput> moduleOutputs() throws EnforcerRuleException {
File outputDirectory = outputDirectory();
JavaModuleInfo topLevel = readModuleInfo(outputDirectory);
if (topLevel != null) {
return Collections.singletonList(new ModuleOutput(outputDirectory, topLevel));
}
File[] subdirectories = outputDirectory.listFiles(File::isDirectory);
if (subdirectories != null) {
Arrays.sort(subdirectories);
List<ModuleOutput> outputs = new ArrayList<>();
boolean modular = false;
for (File subdirectory : subdirectories) {
JavaModuleInfo moduleInfo = readModuleInfo(subdirectory);
modular |= moduleInfo != null;
outputs.add(new ModuleOutput(subdirectory, moduleInfo));
}
if (modular) {
getLog().debug("Detected module source hierarchy layout below " + outputDirectory + " ("
+ outputs.size() + " module output(s))");
return outputs;
}
}
return Collections.singletonList(new ModuleOutput(outputDirectory, null));
}

/**
* Read the module descriptor of one output directory.
*
* @return the parsed {@link JavaModuleInfo}, or {@code null} if the directory has no
* {@code module-info.class}
* @throws EnforcerRuleException if a {@code module-info.class} exists but cannot be read
*/
private JavaModuleInfo readModuleInfo(File directory) throws EnforcerRuleException {
File moduleInfo = new File(directory, "module-info.class");
if (!moduleInfo.isFile()) {
getLog().debug("No module-info.class in " + directory);
return null;
}
try (InputStream in = Files.newInputStream(moduleInfo.toPath())) {
return JavaModuleInfoReader.read(in);
} catch (IOException e) {
throw new EnforcerRuleException("Failed to read " + moduleInfo + ": " + e.getMessage(), e);
}
}

/** Short rule name for log messages, e.g. {@code RequireMinimalExports}. */
protected String ruleName() {
return getClass().getSimpleName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.enforcer.rules.modules;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

/**
* Determines the module identity and package set of one dependency artifact (a JAR or an exploded
* classes directory, e.g. a reactor sibling's {@code target/classes}).
*
* <p>Modular artifacts and automatic modules are read through
* {@code java.lang.module.ModuleFinder}, which derives automatic-module names and always reports
* the complete package set. Like {@link JavaModuleInfoReader}, the Java&nbsp;9 module API is
* accessed reflectively to keep the Java&nbsp;8 compile baseline (see the design note there).
* Artifacts the finder cannot model (no descriptor and no derivable automatic name, or running on
* a Java&nbsp;8 JVM) fall back to a plain class-file scan and are reported as non-modular.</p>
*/
final class ArtifactModuleScanner {

private ArtifactModuleScanner() {}

/** The module view of one scanned artifact. */
static final class ScannedArtifact {
private final JavaModuleInfo moduleInfo;
private final boolean automatic;
private final Set<String> packages;

private ScannedArtifact(JavaModuleInfo moduleInfo, boolean automatic, Set<String> packages) {
this.moduleInfo = moduleInfo;
this.automatic = automatic;
this.packages = packages;
}

/**
* The module descriptor ({@link JavaModuleInfo#packages()} filled from
* {@code ModuleDescriptor.packages()}), or {@code null} for a non-modular artifact.
*/
JavaModuleInfo moduleInfo() {
return moduleInfo;
}

/** {@code true} if the descriptor was derived for an automatic module (plain JAR). */
boolean isAutomatic() {
return automatic;
}

/** All packages containing at least one class file. */
Set<String> packages() {
return packages;
}
}

/**
* Scan one artifact.
*
* @param file the artifact's JAR file or classes directory
* @return the scanned module view, never {@code null}
* @throws IOException if the artifact cannot be read at all
*/
static ScannedArtifact scan(File file) throws IOException {
// A directory without module-info.class must not go through ModuleFinder: the finder would
// treat it as a *directory of modules* and scan its entries, yielding nonsense.
boolean tryFinder = file.isFile() || new File(file, "module-info.class").isFile();
if (tryFinder && isModuleApiAvailable()) {
try {
ScannedArtifact scanned = scanWithModuleFinder(file);
if (scanned != null) {
return scanned;
}
} catch (InvocationTargetException e) {
// e.g. FindException for a JAR whose file name yields no valid automatic-module
// name — legal on the classpath, so degrade to a non-modular scan.
} catch (ReflectiveOperationException e) {
throw new IOException("Could not read module information of " + file, e);
}
}
return new ScannedArtifact(null, false, scanPackages(file));
}

private static boolean isModuleApiAvailable() {
try {
Class.forName("java.lang.module.ModuleFinder");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

/** Read the artifact through {@code ModuleFinder.of(path)}; {@code null} if it finds no module. */
private static ScannedArtifact scanWithModuleFinder(File file) throws ReflectiveOperationException {
Class<?> finderType = Class.forName("java.lang.module.ModuleFinder");
Object pathArray = Array.newInstance(Path.class, 1);
Array.set(pathArray, 0, file.toPath());
// wrap: a bare Path[] argument would be unpacked by Method.invoke as the whole varargs list
Object finder = finderType.getMethod("of", Path[].class).invoke(null, new Object[] {pathArray});
Set<?> references = (Set<?>) finderType.getMethod("findAll").invoke(finder);
if (references.size() != 1) {
return null;
}
Object reference = references.iterator().next();
Object descriptor = Class.forName("java.lang.module.ModuleReference")
.getMethod("descriptor")
.invoke(reference);
boolean automatic = (Boolean) Class.forName("java.lang.module.ModuleDescriptor")
.getMethod("isAutomatic")
.invoke(descriptor);
JavaModuleInfo moduleInfo = JavaModuleInfoReader.fromDescriptor(descriptor);
return new ScannedArtifact(moduleInfo, automatic, moduleInfo.packages());
}

/** Plain scan: every package that holds at least one {@code .class} file. */
private static Set<String> scanPackages(File file) throws IOException {
Set<String> packages = new TreeSet<String>();
if (file.isDirectory()) {
collectPackages(file, "", packages);
} else if (file.isFile()) {
try (JarFile jar = new JarFile(file)) {
for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements(); ) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
int slash = name.lastIndexOf('/');
if (slash > 0 && name.endsWith(".class") && !name.startsWith("META-INF/")) {
packages.add(name.substring(0, slash).replace('/', '.'));
}
}
}
}
return packages;
}

private static void collectPackages(File directory, String packageName, Set<String> packages) {
File[] entries = directory.listFiles();
if (entries == null) {
return;
}
for (File entry : entries) {
if (entry.isDirectory()) {
String childPackage = packageName.isEmpty() ? entry.getName() : packageName + "." + entry.getName();
collectPackages(entry, childPackage, packages);
} else if (!packageName.isEmpty() && entry.getName().endsWith(".class")) {
packages.add(packageName);
}
}
}
}
Loading
Loading