View Javadoc
1   package com.github.mikkoi.maven_enforcer_plugin.rule;
2   
3   import javax.inject.Inject;
4   import javax.inject.Named;
5   
6   import org.apache.maven.enforcer.rule.api.AbstractEnforcerRule;
7   import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
8   import org.apache.maven.execution.MavenSession;
9   import org.apache.maven.model.Dependency;
10  import org.apache.maven.project.MavenProject;
11  
12  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.List;
16  import java.util.Objects;
17  import java.util.function.Predicate;
18  
19  /**
20   * Maven Enforcer Custom Rule.
21   */
22  @Named("dependOnAllProjects")
23  public class DependOnAllProjects extends AbstractEnforcerRule {
24  
25      /**
26       * Constant value
27       * Size of indentation inside a <dependency>> definition.
28       */
29      private static final String INDENT_DEPENDENCY = "    ";
30      /**
31       * Inject needed Maven component.
32        */
33      private final MavenSession mavenSession;
34      /**
35       * Exclude by project [groupId:]artifactId[:packagingType].
36       * Default value: No projects excluded.
37       * If includes list contains any items, they are evaluated first.
38       * Then excludes are excluded from them.
39       */
40      private List<String> excludes;
41      /**
42       * Include by project [groupId:]artifactId[:packagingType].
43       * Default value: all projects included.
44       */
45      private List<String> includes;
46      /**
47       * Error if unknown project in includes/excludes.
48       * If a wildcard (*) is used in the name, this parameter has no effect.
49       */
50      @SuppressWarnings("unused") // Not actually unused. Set via Plexus/Sisu Container.
51      private boolean errorIfUnknownProject;
52      /**
53       * Include Maven root project.
54       */
55      @SuppressWarnings("unused") // Not actually unused. Set via Plexus/Sisu Container.
56      private boolean includeRootProject;
57  
58      /**
59       * Constructor.
60       *
61       * @param session            Initialized MavenSession object.
62       */
63      @Inject
64      @SuppressFBWarnings
65      public DependOnAllProjects(MavenSession session) {
66          this.mavenSession = Objects.requireNonNull(session);
67      }
68  
69      /**
70       * Format Dependency object to XML snippet.
71       * User can simply copy-paste this to the project.
72       * @param dependency Dependency object
73       * @param indent     Indentation string, e.g. "    "
74       * @return Formatted XML snippet
75       */
76      public static String formatDependency(Dependency dependency, String indent) {
77          final String newLine = System.lineSeparator();
78          final StringBuilder sb = new StringBuilder();
79          sb.append("<dependency>").append(newLine);
80          sb.append(indent).append(String.format("<groupId>%s</groupId>", dependency.getGroupId()))
81              .append(newLine);
82          sb.append(indent)
83              .append(String.format("<artifactId>%s</artifactId>", dependency.getArtifactId()))
84              .append(newLine);
85          // .append(indent).append(String.format("<version>%s</version>", dependency.getVersion())).append(newLine)
86          if (!"jar".equals(dependency.getType())) {
87              sb.append(indent).append(String.format("<type>%s</type>", dependency.getType()))
88                  .append(newLine);
89          }
90          sb.append("</dependency>");
91          return sb.toString();
92      }
93  
94      /**
95       * Convert string for matching.
96       *
97       * @param s String
98       * @return converted string
99       */
100     public static String convertStringForMatching(String s) {
101         String t = s.replace(".", "\\.");
102         t = t.replace("*", ".*");
103         if (!t.contains(":")) {
104             t = ".*:" + t + ":.*";
105         }
106         if (Arrays.stream(t.split(":")).count() < 3) {
107             t = t + ":.*";
108         }
109         return t;
110     }
111 
112     /**
113      * Compare two MavenProject objects.
114      * GroupId, ArtifactId, Version and Packaging must match.
115      *
116      * @param a MavenProject a
117      * @param b MavenProject b
118      * @return true if projects are equal
119      */
120     public static boolean projectsAreEquals(MavenProject a, MavenProject b) {
121         return a.getGroupId().equals(b.getGroupId())
122             && a.getArtifactId().equals(b.getArtifactId()) && a.getVersion().equals(b.getVersion())
123             && a.getPackaging().equals(b.getPackaging());
124     }
125 
126     /**
127      * Compare two Dependency objects.
128      * GroupId, ArtifactId, Version and Type must match.
129      *
130      * @param a Dependency a
131      * @param b Dependency b
132      * @return true if dependencies are equal
133      */
134     public static boolean dependenciesAreEquals(Dependency a, Dependency b) {
135         return a.getGroupId().equals(b.getGroupId())
136             && a.getArtifactId().equals(b.getArtifactId()) && a.getVersion().equals(b.getVersion())
137             && a.getType().equals(b.getType());
138     }
139 
140     /**
141      * Does the list (Iterable) of Dependency objects contain the MavenProject?
142      *
143      * @param projects Iterable of Dependency objects
144      * @param project  a Maven project object
145      * @return true if project is found
146      */
147     public static boolean dependenciesContains(Iterable<Dependency> projects,
148                                                MavenProject project) {
149         for (Dependency p : projects) {
150             if (dependenciesAreEquals(p, projectToDependency(project))) {
151                 return true;
152             }
153         }
154         return false;
155     }
156 
157     /**
158      * Does any of the projects in Maven Reactor build match with this project name?
159      * Name must not have a wildcard!
160      *
161      * @param projects    Iterable of MavenProject objects
162      * @param projectName Project name, e.g. "artifactId", "groupId:artifactId", "groupId:artifactId:packingType".
163      * @return Boolean
164      */
165     public static boolean projectsContains(Iterable<MavenProject> projects, String projectName) {
166         List<String> ids = Arrays.asList(projectName.split(":"));
167         for (MavenProject project : projects) {
168             if (ids.size() == 1) {
169                 if (project.getArtifactId().equals(ids.get(0))) {
170                     return true;
171                 }
172             } else if (ids.size() == 2) {
173                 if (project.getGroupId().equals(ids.get(0))
174                     && project.getArtifactId().equals(ids.get(1))) {
175                     return true;
176                 }
177             } else {
178                 assert ids.size() == 3;
179                 if (project.getGroupId().equals(ids.get(0))
180                     && project.getArtifactId().equals(ids.get(1))
181                     && project.getPackaging().equals(ids.get(2))) {
182                     return true;
183                 }
184             }
185         }
186         return false;
187     }
188 
189     /**
190      * Convert MavenProject to Dependency object.
191      *
192      * @param mavenProject MavenProject object
193      * @return Dependency object
194      */
195     public static Dependency projectToDependency(MavenProject mavenProject) {
196         Dependency d = new Dependency();
197         d.setGroupId(mavenProject.getGroupId());
198         d.setArtifactId(mavenProject.getArtifactId());
199         d.setVersion(mavenProject.getVersion());
200         d.setType(mavenProject.getPackaging());
201         return d;
202     }
203 
204     /**
205      * Match projects with includes and excludes.
206      * Includes are * by default. Then excludes are excluded from the includes.
207      *
208      * @param includes     Included projects. Default: *
209      * @param excludes     Excluded projects. Default: none
210      * @param mavenProject Initialized MavenProject object.
211      * @return True or false.
212      */
213     public static boolean isProjectIncluded(List<String> includes, List<String> excludes,
214                                             MavenProject mavenProject) {
215         String projectId =
216             String.format("%s:%s:%s", mavenProject.getGroupId(), mavenProject.getArtifactId(),
217                 mavenProject.getPackaging());
218 
219         // Match from the end of the id, artifactId alone is enough.
220         Predicate<String> predicateForProjectId =
221             s -> projectId.matches(convertStringForMatching(s));
222         return includes.stream().anyMatch(predicateForProjectId)
223             && excludes.stream().noneMatch(predicateForProjectId);
224     }
225 
226     /**
227      * Validate parameters provided via properties
228      * either on the command line or using configuration element in pom.
229      *
230      * @throws EnforcerRuleException if parameter validation fails.
231      */
232     private void validateAndPrepareParameters() throws EnforcerRuleException {
233         getLog().debug("includes=" + includes);
234         getLog().debug("excludes=" + excludes);
235         getLog().debug("errorIfUnknownProject=" + errorIfUnknownProject);
236         getLog().debug("includeRootProject=" + includeRootProject);
237 
238         final List<MavenProject> reactorProjects =
239             mavenSession.getProjectDependencyGraph().getSortedProjects();
240         getLog().debug("reactorProjects=" + reactorProjects);
241 
242         if (includes == null) {
243             includes = new ArrayList<>();
244         }
245         if (excludes == null) {
246             excludes = new ArrayList<>();
247         }
248         for (String a : includes) {
249             getLog().debug(String.format("Check include '%s'", a));
250             if (a == null || a.isEmpty() || a.matches("^[\t\n ]*$")) {
251                 throw new EnforcerRuleException(
252                     "Failure in parameter 'includes'. String is null, empty or contains only whitespace");
253             }
254             List<String> ids = Arrays.asList(a.split(":"));
255             if (ids.size() > 3) {
256                 throw new EnforcerRuleException(
257                     "Failure in parameter 'includes'. String is invalid");
258             }
259             if (errorIfUnknownProject && !a.contains("*")
260                 && !projectsContains(reactorProjects, a)) {
261                 throw new EnforcerRuleException(String.format(
262                     "Failure in parameter 'excludes'. Project '%s' not found in build", a));
263             }
264         }
265         if (includes.isEmpty()) {
266             includes.add("*");
267         }
268 
269         for (String a : excludes) {
270             getLog().debug(String.format("Check exclude '%s'", a));
271             if (a == null || a.isEmpty() || a.matches("^[\t\n ]*$")) {
272                 throw new EnforcerRuleException(
273                     "Failure in parameter 'includes'. String is null, empty or contains only whitespace");
274             }
275             List<String> ids = Arrays.asList(a.split(":"));
276             if (ids.size() > 3) {
277                 throw new EnforcerRuleException(
278                     "Failure in parameter 'excludes'. String is invalid");
279             }
280             if (errorIfUnknownProject && !a.contains("*")
281                 && !projectsContains(reactorProjects, a)) {
282                 throw new EnforcerRuleException(String.format(
283                     "Failure in parameter 'excludes'. Project '%s' not found in build", a));
284             }
285         }
286 
287         getLog().debug("includes(resolved)=" + includes);
288         getLog().debug("excludes(resolved)=" + excludes);
289         getLog().debug("errorIfUnknownProject(resolved)=" + errorIfUnknownProject);
290         getLog().debug("includeRootProject(resolved)=" + includeRootProject);
291     }
292 
293     /**
294      * The rule logic.
295      * Collect all projects in the build and filter according to includes/excludes.
296      * Match the list with the dependencies of the current project.
297      * If the two lists do not match, Raise EnforcerRuleException
298      *
299      * @throws EnforcerRuleException if rule fails.
300      */
301     public void dependOnAllProjects() throws EnforcerRuleException {
302         List<MavenProject> includedProjects = new ArrayList<>();
303         MavenProject currentProject = mavenSession.getCurrentProject();
304         getLog().debug(String.format("Current Project: %s:%s", currentProject.getGroupId(),
305             currentProject.getArtifactId()));
306 
307         getLog().debug("Iterate through all projects in Maven Dependency Graph, i.e. the build.");
308         mavenSession.getProjectDependencyGraph().getSortedProjects().forEach(project -> {
309             String projectId =
310                 String.format("%s:%s:%s", project.getGroupId(), project.getArtifactId(),
311                     project.getVersion());
312             getLog().debug("    " + projectId);
313 
314             if (isIncluded(project)) {
315                 if (projectsAreEquals(project, currentProject) || (!this.includeRootProject
316                     && projectsAreEquals(project, mavenSession.getTopLevelProject()))) {
317                     getLog().debug("Filter out project: "
318                         + String.format("%s:%s", project.getGroupId(), project.getArtifactId()));
319                 } else {
320                     includedProjects.add(project);
321                 }
322             }
323         });
324         getLog().debug("includedProjects=%s" + includedProjects);
325         List<MavenProject> missingProjects = new ArrayList<>();
326         for (MavenProject project : includedProjects) {
327             @SuppressWarnings("unchecked") List<Dependency> dependencies =
328                 currentProject.getDependencies();
329             if (!dependenciesContains(dependencies, project)) {
330                 missingProjects.add(project);
331             }
332         }
333         if (!missingProjects.isEmpty()) {
334             List<String> errors = new ArrayList<>(missingProjects.size());
335             for (MavenProject missingProject : missingProjects) {
336                 errors.add(String.format("Project '%s:%s' is missing dependency '%s:%s:%s'.",
337                     currentProject.getGroupId(), currentProject.getArtifactId(),
338                     missingProject.getGroupId(), missingProject.getArtifactId(),
339                     missingProject.getPackaging()));
340             }
341             StringBuilder sb = new StringBuilder();
342             sb.append(String.format("Missing definitions from the project '%s:%s':",
343                 currentProject.getGroupId(), currentProject.getArtifactId()));
344             sb.append(System.lineSeparator());
345             sb.append("<!--     Created by Maven Enforcer rule dependOnAllProjects     --->");
346             sb.append(System.lineSeparator());
347             for (MavenProject missingProject : missingProjects) {
348                 sb.append(formatDependency(projectToDependency(missingProject), INDENT_DEPENDENCY));
349                 sb.append(System.lineSeparator());
350             }
351             sb.append("<!--     / Created by Maven Enforcer rule dependOnAllProjects     --->");
352             errors.add(sb.toString());
353             throw new EnforcerRuleException(String.join("\n", errors));
354         }
355         getLog().debug("End of iterate");
356     }
357 
358     /**
359      * The main entry point for rule.
360      *
361      * @throws EnforcerRuleException if rule fails.
362      */
363     @Override
364     public void execute() throws EnforcerRuleException {
365         MavenProject currentProject = mavenSession.getCurrentProject();
366         getLog().debug(String.format("Current Project: %s:%s", currentProject.getGroupId(),
367             currentProject.getArtifactId()));
368         MavenProject topLevelProject = mavenSession.getTopLevelProject();
369         getLog().debug(String.format("Top Level Project: %s:%s", topLevelProject.getGroupId(),
370             topLevelProject.getArtifactId()));
371 
372         validateAndPrepareParameters();
373 
374         dependOnAllProjects();
375     }
376 
377     /**
378      * String representation of the rule.
379      * Output is used in verbose Maven logs, can help during investigate problems.
380      *
381      * @return rule description
382      */
383     @Override
384     public String toString() {
385         return String.format(
386             "DependOnAllProjects[includes=%s;excludes=%s;includeRootProject=%b;errorIfUnknownProject=%b]",
387             includes, excludes, includeRootProject, errorIfUnknownProject);
388     }
389 
390     private boolean isIncluded(MavenProject mavenProject) {
391         boolean r = isProjectIncluded(this.includes, this.excludes, mavenProject);
392         getLog().debug(String.format("isIncluded(%s:%s:%s:%s): %b", mavenProject.getGroupId(),
393             mavenProject.getArtifactId(), mavenProject.getVersion(), mavenProject.getPackaging(),
394             r));
395         return r;
396     }
397 
398 }