diff --git a/.gitignore b/.gitignore
index f79c928..cd8e23d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ out/
/bootstrap
/dependencies.xml
.java-version
+logs
\ No newline at end of file
diff --git a/checkstyle/checkstyle-suppressions.xml b/checkstyle/checkstyle-suppressions.xml
index 5db604e..c7e00fa 100644
--- a/checkstyle/checkstyle-suppressions.xml
+++ b/checkstyle/checkstyle-suppressions.xml
@@ -31,8 +31,12 @@ under the License.
-
+
+
+
+
+
diff --git a/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java b/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java
index 51fdec0..1ff6fd3 100644
--- a/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java
+++ b/src/main/java/com/github/streamshub/AbstractAlignmentReporterMojo.java
@@ -146,6 +146,42 @@ public abstract class AbstractAlignmentReporterMojo extends AbstractMojo {
@Parameter(property = "excludes")
private String excludes;
+ /**
+ * A comma-separated list of module names to exclude from processing in multi-module projects, or null to
+ * include all modules. Each module name pattern supports full and partial * wildcards.
+ *
+ * For example, *-test will exclude all modules whose name ends with "-test",
+ * and flink-* will exclude all modules whose name starts with "flink-".
+ *
+ */
+ @Parameter(property = "excludeModules")
+ private String excludeModules;
+
+ /**
+ * Enable shade-aware analysis to detect and report on maven-shade-plugin configurations and their alignment.
+ * When enabled, the plugin will analyze shade configurations across all projects and provide additional
+ * reporting on shaded dependencies.
+ */
+ @Parameter(property = "analyzeShade", defaultValue = "false")
+ private boolean analyzeShade;
+
+ /**
+ * Print detailed shade configuration information in the report.
+ * Only has effect when analyzeShade=true. When enabled, shows all shade plugin configurations
+ * found in the project with their relocation mappings.
+ */
+ @Parameter(property = "printShadeConfigurations", defaultValue = "false")
+ private boolean printShadeConfigurations;
+
+ /**
+ * Include transitive dependencies in shade analysis.
+ * Only has effect when analyzeShade=true. When enabled, analyzes both direct and transitive
+ * dependencies for shade impact, providing comprehensive coverage of all artifacts that
+ * could be affected by shading configurations.
+ */
+ @Parameter(property = "includeTransitiveShaded", defaultValue = "false")
+ private boolean includeTransitiveShaded;
+
private ArtifactFilter scopeFilter;
private static void write(final String string, final File file) throws IOException {
@@ -224,6 +260,37 @@ public void execute() throws MojoExecutionException, MojoFailureException {
String unalignedTransitivesStr = reportUnalignedTransitiveDependenciesSummary(unalignedTransitives);
String unalignedTransitiveDetail = reportUnalignedTransitiveDependencyDetail(alignedDirectDeps, excludeDependencyFilter);
+ String shadeReport = "";
+ String shadeAlignmentSummary = "";
+
+ if (analyzeShade) {
+ ShadeAwareAlignmentReporter shadeReporter = new ShadeAwareAlignmentReporter(getLog());
+
+ // Collect artifacts for shade analysis
+ List artifactsForShadeAnalysis;
+ List artifactsWithPathsForShadeAnalysis = null;
+
+ if (includeTransitiveShaded) {
+ artifactsWithPathsForShadeAnalysis = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);
+ artifactsForShadeAnalysis = artifactsWithPathsForShadeAnalysis.stream()
+ .map(ArtifactWithPath::getArtifact)
+ .collect(Collectors.toList());
+ } else {
+ artifactsForShadeAnalysis = new ArrayList<>(dependencyArtifacts);
+ }
+
+ ShadeAwareAlignmentReporter.ShadeAlignmentResult shadeResult =
+ shadeReporter.analyzeShadeAlignment(getReactorProjectsForShadeAnalysis(),
+ artifactsForShadeAnalysis,
+ alignmentPattern,
+ artifactsWithPathsForShadeAnalysis);
+
+ if (printShadeConfigurations) {
+ shadeReport = shadeReporter.generateShadeReport(shadeResult, alignmentPattern);
+ }
+ shadeAlignmentSummary = shadeReporter.generateShadeAlignmentSummary(shadeResult, alignmentPattern);
+ }
+
if (outputFile != null) {
String projectTitle = getProjectTitle();
@@ -233,6 +300,13 @@ public void execute() throws MojoExecutionException, MojoFailureException {
write(unalignedTransitivesStr, outputFile);
write(unalignedTransitiveDetail, outputFile);
+ if (analyzeShade && !shadeReport.isEmpty()) {
+ write(shadeReport, outputFile);
+ }
+ if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
+ write(shadeAlignmentSummary, outputFile);
+ }
+
getLog().info(String.format("Wrote alignment report tree to: %s",
outputFile));
} else {
@@ -240,6 +314,13 @@ public void execute() throws MojoExecutionException, MojoFailureException {
log(unalignedDirectStr, getLog());
log(unalignedTransitivesStr, getLog());
log(unalignedTransitiveDetail, getLog());
+
+ if (analyzeShade && !shadeReport.isEmpty()) {
+ log(shadeReport, getLog());
+ }
+ if (analyzeShade && !shadeAlignmentSummary.isEmpty()) {
+ log(shadeAlignmentSummary, getLog());
+ }
}
} catch (IOException exception) {
throw new MojoExecutionException(
@@ -558,4 +639,121 @@ private ArtifactFilter createExcludeFilter() {
}
return filter;
}
+
+ /**
+ * Filters reactor projects based on the excludeModules parameter.
+ *
+ * @param projects the list of projects to filter
+ * @return the filtered list of projects
+ */
+ protected List filterModules(final List projects) {
+ if (excludeModules == null || excludeModules.trim()
+ .isEmpty()) {
+ return projects;
+ }
+
+ List patterns = Arrays.asList(excludeModules.split(","));
+ getLog().debug(String.format("+ Filtering modules by exclude patterns: %s", patterns));
+
+ return projects.stream()
+ .filter(project -> {
+ String artifactId = project.getArtifactId();
+ for (String pattern : patterns) {
+ String trimmedPattern = pattern.trim();
+ if (matchesPattern(artifactId, trimmedPattern)) {
+ getLog().debug(String.format("+ Excluding module: %s (matched pattern: %s)", artifactId, trimmedPattern));
+ return false;
+ }
+ }
+ return true;
+ })
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Checks if a string matches a pattern with wildcard support.
+ *
+ * @param text the text to match
+ * @param pattern the pattern (supports * wildcards)
+ * @return true if the text matches the pattern
+ */
+ private boolean matchesPattern(final String text, final String pattern) {
+ if (pattern.equals("*")) {
+ return true;
+ }
+
+ String regex = pattern.replace("*", ".*");
+ return text.matches(regex);
+ }
+
+ /**
+ * Gets the reactor projects to use for shade analysis.
+ * In single-module mode, returns only the current project.
+ * In aggregate mode, returns filtered reactor projects.
+ *
+ * @return the list of projects for shade analysis
+ */
+ protected List getReactorProjectsForShadeAnalysis() {
+ return filterModules(reactorProjects);
+ }
+
+ /**
+ * Collects all transitive dependency artifacts from the given direct dependency nodes.
+ * This method traverses the entire dependency tree and collects all artifacts at all levels.
+ *
+ * @param directDependencies the set of direct dependency nodes to traverse
+ * @return a list of all artifacts (direct + transitive)
+ */
+ private List getAllTransitiveDependencyArtifacts(final Set directDependencies) {
+ List allArtifactsWithPaths = getAllTransitiveDependencyArtifactsWithPaths(directDependencies);
+
+ List artifacts = allArtifactsWithPaths.stream()
+ .map(ArtifactWithPath::getArtifact)
+ .collect(Collectors.toList());
+
+ getLog().debug(String.format("Collected %d total artifacts (direct + transitive) for shade analysis", artifacts.size()));
+
+ return artifacts;
+ }
+
+ /**
+ * Collects all transitive dependency artifacts with their dependency paths.
+ *
+ * @param directDependencies the set of direct dependency nodes to traverse
+ * @return a list of all artifacts with their paths (direct + transitive)
+ */
+ private List getAllTransitiveDependencyArtifactsWithPaths(final Set directDependencies) {
+ Set allArtifacts = new HashSet<>();
+
+ for (DependencyNode directDependency : directDependencies) {
+ collectAllArtifactsWithPathsFromNode(directDependency, new ArrayList<>(), allArtifacts);
+ }
+
+ getLog().debug(String.format("Collected %d total artifacts with paths for shade analysis", allArtifacts.size()));
+
+ return new ArrayList<>(allArtifacts);
+ }
+
+ /**
+ * Recursively collects all artifacts with their paths from a dependency node and its children.
+ *
+ * @param node the dependency node to traverse
+ * @param currentPath the current dependency path (not including this node)
+ * @param collector the set to collect artifacts with paths into
+ */
+ private void collectAllArtifactsWithPathsFromNode(final DependencyNode node,
+ final List currentPath,
+ final Set collector) {
+ // Create the path including this node
+ List pathWithThisNode = new ArrayList<>(currentPath);
+ pathWithThisNode.add(node.getArtifact());
+
+ // Add the current node's artifact with its path
+ collector.add(new ArtifactWithPath(node.getArtifact(), pathWithThisNode));
+
+ // Recursively process all children
+ for (DependencyNode child : node.getChildren()) {
+ collectAllArtifactsWithPathsFromNode(child, pathWithThisNode, collector);
+ }
+ }
}
diff --git a/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java b/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java
index 2b0428f..e2371bd 100644
--- a/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java
+++ b/src/main/java/com/github/streamshub/AggregateAlignmentReporterMojo.java
@@ -20,6 +20,7 @@
package com.github.streamshub;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
@@ -35,7 +36,9 @@ public class AggregateAlignmentReporterMojo extends AbstractAlignmentReporterMoj
protected Set getDirectDependencies(final ArtifactFilter artifactFilter) throws MojoExecutionException {
Set dependencies = new HashSet<>();
- for (MavenProject reactorProject : reactorProjects) {
+ List filteredProjects = filterModules(reactorProjects);
+
+ for (MavenProject reactorProject : filteredProjects) {
dependencies.addAll(getDirectDependencies(reactorProject, artifactFilter));
}
diff --git a/src/main/java/com/github/streamshub/ArtifactWithPath.java b/src/main/java/com/github/streamshub/ArtifactWithPath.java
new file mode 100644
index 0000000..35481c8
--- /dev/null
+++ b/src/main/java/com/github/streamshub/ArtifactWithPath.java
@@ -0,0 +1,128 @@
+/*
+ * 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 com.github.streamshub;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.maven.artifact.Artifact;
+
+/**
+ * Represents an artifact along with its dependency path.
+ * Used for tracking how transitive dependencies are reached in the dependency tree.
+ */
+public final class ArtifactWithPath {
+ private final Artifact artifact;
+ private final List dependencyPath;
+ private final boolean isDirect;
+
+ /**
+ * Creates an ArtifactWithPath for a direct dependency.
+ *
+ * @param artifact the artifact
+ */
+ public ArtifactWithPath(final Artifact artifactParam) {
+ this.artifact = artifactParam;
+ this.dependencyPath = List.of(artifactParam);
+ this.isDirect = true;
+ }
+
+ /**
+ * Creates an ArtifactWithPath for a transitive dependency.
+ *
+ * @param artifact the artifact
+ * @param dependencyPath the full path from root to this artifact (including this artifact)
+ */
+ public ArtifactWithPath(final Artifact artifactParam, final List pathParam) {
+ this.artifact = artifactParam;
+ this.dependencyPath = List.copyOf(pathParam);
+ this.isDirect = pathParam.size() == 1;
+ }
+
+ public Artifact getArtifact() {
+ return artifact;
+ }
+
+ public List getDependencyPath() {
+ return dependencyPath;
+ }
+
+ public boolean isDirect() {
+ return isDirect;
+ }
+
+ public boolean isTransitive() {
+ return !isDirect;
+ }
+
+ /**
+ * Returns the root dependency (the direct dependency that brought this artifact in).
+ * For direct dependencies, returns the artifact itself.
+ */
+ public Artifact getRootDependency() {
+ return dependencyPath.get(0);
+ }
+
+ /**
+ * Formats the dependency path as a string in the format:
+ * "artifact <- parent <- ... <- root"
+ */
+ public String formatDependencyPath() {
+ if (isDirect) {
+ return artifact.toString() + " (direct)";
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ // Add the current artifact first
+ sb.append(artifact.toString());
+
+ // Add the path in reverse order (excluding the current artifact)
+ for (int i = dependencyPath.size() - 2; i >= 0; i--) {
+ sb.append(" <- ");
+ sb.append(dependencyPath.get(i)
+ .toString());
+ }
+
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ArtifactWithPath that = (ArtifactWithPath) o;
+ return Objects.equals(artifact, that.artifact);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(artifact);
+ }
+
+ @Override
+ public String toString() {
+ return formatDependencyPath();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/streamshub/ShadeAnalyzer.java b/src/main/java/com/github/streamshub/ShadeAnalyzer.java
new file mode 100644
index 0000000..d948c9e
--- /dev/null
+++ b/src/main/java/com/github/streamshub/ShadeAnalyzer.java
@@ -0,0 +1,244 @@
+/*
+ * 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 com.github.streamshub;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.maven.model.Plugin;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.project.MavenProject;
+import org.codehaus.plexus.util.xml.Xpp3Dom;
+
+public final class ShadeAnalyzer {
+
+ public static final class ShadeRelocation {
+ private final String pattern;
+ private final String shadedPattern;
+
+ public ShadeRelocation(final String pattern, final String shadedPattern) {
+ this.pattern = pattern;
+ this.shadedPattern = shadedPattern;
+ }
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public String getShadedPattern() {
+ return shadedPattern;
+ }
+
+ public String applyRelocation(final String originalPackage) {
+ if (originalPackage.startsWith(pattern)) {
+ return originalPackage.replace(pattern, shadedPattern);
+ }
+ return originalPackage;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s -> %s", pattern, shadedPattern);
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ShadeRelocation that = (ShadeRelocation) o;
+ return Objects.equals(pattern, that.pattern) && Objects.equals(shadedPattern, that.shadedPattern);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pattern, shadedPattern);
+ }
+ }
+
+ public static final class ShadeConfiguration {
+ private final List relocations;
+ private final boolean createDependencyReducedPom;
+ private final String moduleName;
+
+ public ShadeConfiguration(final List relocations,
+ final boolean createDependencyReducedPom,
+ final String moduleName) {
+ this.relocations = relocations != null ? relocations : new ArrayList<>();
+ this.createDependencyReducedPom = createDependencyReducedPom;
+ this.moduleName = moduleName;
+ }
+
+ public List getRelocations() {
+ return relocations;
+ }
+
+ public boolean isCreateDependencyReducedPom() {
+ return createDependencyReducedPom;
+ }
+
+ public boolean hasRelocations() {
+ return !relocations.isEmpty();
+ }
+
+ public String getModuleName() {
+ return moduleName;
+ }
+ }
+
+ private final Log log;
+
+ public ShadeAnalyzer(final Log log) {
+ this.log = log;
+ }
+
+ public ShadeConfiguration analyzeShadeConfiguration(final MavenProject project) {
+ Plugin shadePlugin = findShadePlugin(project);
+ if (shadePlugin == null) {
+ log.debug(String.format("No maven-shade-plugin found in project: %s", project.getArtifactId()));
+ return new ShadeConfiguration(new ArrayList<>(), false, project.getArtifactId());
+ }
+
+ return parseShadeConfiguration(shadePlugin, project.getArtifactId());
+ }
+
+ public List analyzeAllShadeConfigurations(final List projects) {
+ List configurations = new ArrayList<>();
+
+ for (MavenProject project : projects) {
+ ShadeConfiguration config = analyzeShadeConfiguration(project);
+ if (config.hasRelocations()) {
+ configurations.add(config);
+ log.debug(String.format("Found shade configuration in project: %s with %d relocations",
+ project.getArtifactId(), config.getRelocations()
+ .size()));
+ }
+ }
+
+ return configurations;
+ }
+
+ public boolean isArtifactShaded(final String groupId, final String artifactId, final List shadeConfigs) {
+ // Check both groupId and common package patterns for this artifact
+ for (ShadeConfiguration config : shadeConfigs) {
+ for (ShadeRelocation relocation : config.getRelocations()) {
+ String pattern = relocation.getPattern();
+
+ // Check if groupId matches the pattern directly
+ if (groupId.startsWith(pattern)) {
+ return true;
+ }
+
+ // Check if this looks like a known mapping between Maven groupId and Java package
+ if (isKnownGroupIdToPackageMapping(groupId, artifactId, pattern)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks for known mappings between Maven groupId and Java package names.
+ * This handles cases where the Maven groupId differs from the Java package prefix.
+ */
+ private boolean isKnownGroupIdToPackageMapping(final String groupId, final String artifactId, final String packagePattern) {
+ // Handle cases where groupId is same as package (most common)
+ if (packagePattern.startsWith(groupId)) {
+ return true;
+ }
+
+ // Handle cases where the package pattern contains the artifactId
+ return packagePattern.contains(artifactId);
+ }
+
+
+ private Plugin findShadePlugin(final MavenProject project) {
+ if (project.getBuild() == null || project.getBuild()
+ .getPlugins() == null) {
+ return null;
+ }
+
+ return project.getBuild()
+ .getPlugins()
+ .stream()
+ .filter(plugin -> "org.apache.maven.plugins".equals(plugin.getGroupId())
+ && "maven-shade-plugin".equals(plugin.getArtifactId()))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private ShadeConfiguration parseShadeConfiguration(final Plugin shadePlugin, final String moduleName) {
+ List relocations = new ArrayList<>();
+ boolean createDependencyReducedPom = false;
+
+ Xpp3Dom configuration = (Xpp3Dom) shadePlugin.getConfiguration();
+ if (configuration != null) {
+ createDependencyReducedPom = parseCreateDependencyReducedPom(configuration);
+ relocations.addAll(parseRelocations(configuration));
+ }
+
+ if (shadePlugin.getExecutions() != null) {
+ for (org.apache.maven.model.PluginExecution execution : shadePlugin.getExecutions()) {
+ Xpp3Dom execConfig = (Xpp3Dom) execution.getConfiguration();
+ if (execConfig != null) {
+ relocations.addAll(parseRelocations(execConfig));
+ }
+ }
+ }
+
+ return new ShadeConfiguration(relocations, createDependencyReducedPom, moduleName);
+ }
+
+ private boolean parseCreateDependencyReducedPom(final Xpp3Dom configuration) {
+ Xpp3Dom createReducedPom = configuration.getChild("createDependencyReducedPom");
+ if (createReducedPom != null) {
+ return Boolean.parseBoolean(createReducedPom.getValue());
+ }
+ return false;
+ }
+
+ private List parseRelocations(final Xpp3Dom configuration) {
+ List relocations = new ArrayList<>();
+
+ Xpp3Dom relocationsNode = configuration.getChild("relocations");
+ if (relocationsNode != null) {
+ Xpp3Dom[] relocationNodes = relocationsNode.getChildren("relocation");
+
+ for (Xpp3Dom relocationNode : relocationNodes) {
+ String pattern = getChildValue(relocationNode, "pattern");
+ String shadedPattern = getChildValue(relocationNode, "shadedPattern");
+
+ if (pattern != null && shadedPattern != null) {
+ relocations.add(new ShadeRelocation(pattern, shadedPattern));
+ log.debug(String.format("Found relocation: %s -> %s", pattern, shadedPattern));
+ }
+ }
+ }
+
+ return relocations;
+ }
+
+ private String getChildValue(final Xpp3Dom parent, final String childName) {
+ Xpp3Dom child = parent.getChild(childName);
+ return child != null ? child.getValue() : null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java b/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java
new file mode 100644
index 0000000..ea8b7ef
--- /dev/null
+++ b/src/main/java/com/github/streamshub/ShadeAwareAlignmentReporter.java
@@ -0,0 +1,296 @@
+/*
+ * 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 com.github.streamshub;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.project.MavenProject;
+
+public final class ShadeAwareAlignmentReporter {
+
+ public static final class ShadeAlignmentResult {
+ private final List shadeConfigurations;
+ private final List shadedArtifacts;
+ private final List unshadedArtifacts;
+ private final List shadedArtifactsWithPaths;
+
+ public ShadeAlignmentResult(final List configurations,
+ final List shaded,
+ final List unshaded) {
+ this.shadeConfigurations = configurations;
+ this.shadedArtifacts = shaded;
+ this.unshadedArtifacts = unshaded;
+ this.shadedArtifactsWithPaths = null;
+ }
+
+ public ShadeAlignmentResult(final List configurations,
+ final List shaded,
+ final List unshaded,
+ final List shadedWithPaths) {
+ this.shadeConfigurations = configurations;
+ this.shadedArtifacts = shaded;
+ this.unshadedArtifacts = unshaded;
+ this.shadedArtifactsWithPaths = shadedWithPaths;
+ }
+
+ public List getShadeConfigurations() {
+ return shadeConfigurations;
+ }
+
+ public List getShadedArtifacts() {
+ return shadedArtifacts;
+ }
+
+ public List getUnshadedArtifacts() {
+ return unshadedArtifacts;
+ }
+
+ public boolean hasShadeConfigurations() {
+ return !shadeConfigurations.isEmpty();
+ }
+
+ public List getShadedArtifactsWithPaths() {
+ return shadedArtifactsWithPaths;
+ }
+
+ public boolean hasPathInformation() {
+ return shadedArtifactsWithPaths != null;
+ }
+ }
+
+ private final ShadeAnalyzer shadeAnalyzer;
+ private final Log log;
+
+ public ShadeAwareAlignmentReporter(final Log logger) {
+ this.log = logger;
+ this.shadeAnalyzer = new ShadeAnalyzer(log);
+ }
+
+ public ShadeAlignmentResult analyzeShadeAlignment(final List projects,
+ final List artifacts,
+ final Pattern alignmentPattern) {
+ return analyzeShadeAlignment(projects, artifacts, alignmentPattern, null);
+ }
+
+ public ShadeAlignmentResult analyzeShadeAlignment(final List projects,
+ final List artifacts,
+ final Pattern alignmentPattern,
+ final List artifactsWithPaths) {
+
+ List shadeConfigs = shadeAnalyzer.analyzeAllShadeConfigurations(projects);
+
+ List shadedArtifacts = artifacts.stream()
+ .filter(artifact -> shadeAnalyzer.isArtifactShaded(artifact.getGroupId(), artifact.getArtifactId(), shadeConfigs))
+ .collect(java.util.stream.Collectors.toList());
+
+ List unshadedArtifacts = artifacts.stream()
+ .filter(artifact -> !shadeAnalyzer.isArtifactShaded(artifact.getGroupId(), artifact.getArtifactId(), shadeConfigs))
+ .collect(java.util.stream.Collectors.toList());
+
+ // Filter artifactsWithPaths to only include shaded ones
+ List shadedArtifactsWithPaths = null;
+ if (artifactsWithPaths != null) {
+ shadedArtifactsWithPaths = artifactsWithPaths.stream()
+ .filter(awp -> shadeAnalyzer.isArtifactShaded(awp.getArtifact()
+ .getGroupId(), awp.getArtifact()
+ .getArtifactId(), shadeConfigs))
+ .collect(java.util.stream.Collectors.toList());
+ }
+
+ return new ShadeAlignmentResult(shadeConfigs, shadedArtifacts, unshadedArtifacts, shadedArtifactsWithPaths);
+ }
+
+ public String generateShadeReport(final ShadeAlignmentResult result, final Pattern alignmentPattern) throws IOException {
+ try (StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out)) {
+
+ if (!result.hasShadeConfigurations()) {
+ writer.println("No shade configurations found in the project.");
+ writer.println();
+ return out.toString();
+ }
+
+ writer.println("Shade Configuration Analysis");
+ writer.println("===========================");
+ writer.println();
+
+ writer.println(String.format("Found %d shade configuration(s)", result.getShadeConfigurations()
+ .size()));
+ writer.println();
+
+ int configIndex = 1;
+ for (ShadeAnalyzer.ShadeConfiguration config : result.getShadeConfigurations()) {
+ writer.println(String.format("Configuration #%d (%s):", configIndex++, config.getModuleName()));
+ writer.println(String.format(" - Create dependency reduced POM: %s", config.isCreateDependencyReducedPom()));
+ writer.println(String.format(" - Number of relocations: %d", config.getRelocations()
+ .size()));
+
+ if (!config.getRelocations()
+ .isEmpty()) {
+ writer.println(" - Relocations:");
+ for (ShadeAnalyzer.ShadeRelocation relocation : config.getRelocations()) {
+ writer.println(String.format(" * %s", relocation.toString()));
+ }
+ }
+ writer.println();
+ }
+
+ // Show both aligned and unaligned shaded artifacts, similar to regular dependency reports
+ List alignedShadedArtifacts = result.getShadedArtifacts()
+ .stream()
+ .filter(artifact -> alignmentPattern.matcher(artifact.getVersion()).find())
+ .collect(java.util.stream.Collectors.toList());
+
+ List unalignedShadedArtifacts = result.getShadedArtifacts()
+ .stream()
+ .filter(artifact -> !alignmentPattern.matcher(artifact.getVersion()).find())
+ .collect(java.util.stream.Collectors.toList());
+
+ if (!alignedShadedArtifacts.isEmpty()) {
+ String title = String.format("%d Aligned artifacts affected by shading", alignedShadedArtifacts.size());
+ writer.println(title);
+ writer.println("-".repeat(title.length()));
+ for (Artifact artifact : alignedShadedArtifacts) {
+ writer.println(String.format("Aligned - %s", artifact));
+ }
+ writer.println();
+ }
+
+ if (!unalignedShadedArtifacts.isEmpty()) {
+ String title = String.format("%d Unaligned artifacts affected by shading", unalignedShadedArtifacts.size());
+ writer.println(title);
+ writer.println("-".repeat(title.length()));
+ for (Artifact artifact : unalignedShadedArtifacts) {
+ writer.println(String.format("Unaligned - %s", artifact));
+ }
+ writer.println();
+ }
+
+ return out.toString();
+ }
+ }
+
+ public String generateShadeAlignmentSummary(final ShadeAlignmentResult result, final Pattern alignmentPattern) throws IOException {
+ try (StringWriter out = new StringWriter(); PrintWriter writer = new PrintWriter(out)) {
+
+ if (!result.hasShadeConfigurations()) {
+ return "";
+ }
+
+ writer.println("Shade-Aware Alignment Summary");
+ writer.println("============================");
+ writer.println();
+
+ long alignedShaded = result.getShadedArtifacts()
+ .stream()
+ .filter(artifact -> alignmentPattern.matcher(artifact.getVersion())
+ .find())
+ .count();
+
+ long unalignedShaded = result.getShadedArtifacts()
+ .size() - alignedShaded;
+
+ writer.println(String.format("Shaded artifacts: %d total, %d aligned, %d unaligned",
+ result.getShadedArtifacts()
+ .size(), alignedShaded, unalignedShaded));
+
+ if (unalignedShaded > 0) {
+ writer.println();
+ writer.println("Unaligned Shaded Artifacts:");
+ writer.println("---------------------------");
+ result.getShadedArtifacts()
+ .stream()
+ .filter(artifact -> !alignmentPattern.matcher(artifact.getVersion())
+ .find())
+ .forEach(artifact -> writer.println(String.format("Unaligned - %s", artifact)));
+ }
+
+ // Add path information if available
+ if (result.hasPathInformation() && !result.getShadedArtifactsWithPaths()
+ .isEmpty()) {
+ writer.println();
+ writer.println("Shaded Artifacts with Dependency Paths");
+ writer.println("--------------------------------------");
+
+ // Group by alignment status and dependency type
+ List transitiveAlignedShaded = result.getShadedArtifactsWithPaths()
+ .stream()
+ .filter(ArtifactWithPath::isTransitive)
+ .filter(awp -> alignmentPattern.matcher(awp.getArtifact().getVersion()).find())
+ .sorted(Comparator.comparing(a -> a.getArtifact()
+ .toString()))
+ .collect(java.util.stream.Collectors.toList());
+
+ List transitiveUnalignedShaded = result.getShadedArtifactsWithPaths()
+ .stream()
+ .filter(ArtifactWithPath::isTransitive)
+ .filter(awp -> !alignmentPattern.matcher(awp.getArtifact().getVersion()).find())
+ .sorted(Comparator.comparing(a -> a.getArtifact()
+ .toString()))
+ .collect(java.util.stream.Collectors.toList());
+
+ if (!transitiveAlignedShaded.isEmpty()) {
+ writer.println();
+ writer.println("Direct Aligned Shaded Artifacts:");
+ writer.println("---------------------------------");
+ for (ArtifactWithPath awp : transitiveAlignedShaded) {
+ writer.println(String.format("Aligned - %s", awp.getArtifact()));
+ }
+ }
+
+ if (!transitiveUnalignedShaded.isEmpty()) {
+ writer.println();
+ writer.println("Direct Unaligned Shaded Artifacts:");
+ writer.println("----------------------------------");
+ for (ArtifactWithPath awp : transitiveUnalignedShaded) {
+ writer.println(String.format("Unaligned - %s", awp.getArtifact()));
+ }
+ }
+
+ if (!transitiveAlignedShaded.isEmpty()) {
+ writer.println();
+ writer.println("Transitive Aligned Shaded Artifacts:");
+ writer.println("------------------------------------");
+ for (ArtifactWithPath awp : transitiveAlignedShaded) {
+ writer.println(String.format("Aligned - %s", awp.formatDependencyPath()));
+ }
+ }
+
+ if (!transitiveUnalignedShaded.isEmpty()) {
+ writer.println();
+ writer.println("Transitive Unaligned Shaded Artifacts:");
+ writer.println("--------------------------------------");
+ for (ArtifactWithPath awp : transitiveUnalignedShaded) {
+ writer.println(String.format("Unaligned - %s", awp.formatDependencyPath()));
+ }
+ }
+ }
+
+ writer.println();
+ return out.toString();
+ }
+ }
+}
\ No newline at end of file