View Javadoc
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.api.services;
20  
21  import java.util.Arrays;
22  import java.util.Comparator;
23  import java.util.List;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  import java.util.concurrent.CopyOnWriteArrayList;
27  import java.util.concurrent.atomic.AtomicInteger;
28  import java.util.concurrent.atomic.LongAdder;
29  import java.util.function.Predicate;
30  import java.util.stream.Stream;
31  
32  import org.apache.maven.api.Constants;
33  import org.apache.maven.api.ProtoSession;
34  import org.apache.maven.api.annotations.Experimental;
35  import org.apache.maven.api.annotations.Nonnull;
36  import org.apache.maven.api.annotations.Nullable;
37  
38  import static java.util.Objects.requireNonNull;
39  
40  /**
41   * Collects problems that were encountered during project building.
42   *
43   * @param <P> The type of the problem.
44   * @since 4.0.0
45   */
46  @Experimental
47  public interface ProblemCollector<P extends BuilderProblem> {
48      /**
49       * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
50       * {@link org.apache.maven.api.services.BuilderProblem.Severity#WARNING}. This check is logically equivalent
51       * to "is there any problem reported?", given warning is the lowest severity.
52       */
53      default boolean hasWarningProblems() {
54          return hasProblemsFor(BuilderProblem.Severity.WARNING);
55      }
56  
57      /**
58       * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
59       * {@link org.apache.maven.api.services.BuilderProblem.Severity#ERROR}.
60       */
61      default boolean hasErrorProblems() {
62          return hasProblemsFor(BuilderProblem.Severity.ERROR);
63      }
64  
65      /**
66       * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
67       * {@link org.apache.maven.api.services.BuilderProblem.Severity#FATAL}.
68       */
69      default boolean hasFatalProblems() {
70          return hasProblemsFor(BuilderProblem.Severity.FATAL);
71      }
72  
73      /**
74       * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
75       * passed in severity.
76       */
77      default boolean hasProblemsFor(BuilderProblem.Severity severity) {
78          requireNonNull(severity, "severity");
79          for (BuilderProblem.Severity s : BuilderProblem.Severity.values()) {
80              if (s.ordinal() <= severity.ordinal() && problemsReportedFor(s) > 0) {
81                  return true;
82              }
83          }
84          return false;
85      }
86  
87      /**
88       * Returns total count of problems reported.
89       */
90      default int totalProblemsReported() {
91          return problemsReportedFor(BuilderProblem.Severity.values());
92      }
93  
94      /**
95       * Returns count of problems reported for given severities.
96       *
97       * @param severities the severity levels to count problems for
98       * @return the total count of problems for the specified severities
99       */
100     int problemsReportedFor(BuilderProblem.Severity... severities);
101 
102     /**
103      * Returns {@code true} if reported problem count exceeded allowed count, and issues were lost. When this
104      * method returns {@code true}, it means that element count of stream returned by method {@link #problems()}
105      * and the counter returned by {@link #totalProblemsReported()} are not equal (latter is bigger than former).
106      *
107      * @return true if the problem collector has overflowed and some problems were not preserved
108      */
109     boolean problemsOverflow();
110 
111     /**
112      * Reports a problem: always maintains the counters, but whether problem is preserved in memory, depends on
113      * implementation and its configuration.
114      *
115      * @param problem the problem to report
116      * @return {@code true} if passed problem is preserved by this call.
117      */
118     boolean reportProblem(P problem);
119 
120     /**
121      * Returns all reported and preserved problems ordered by severity in decreasing order. Note: counters and
122      * element count in this stream does not have to be equal.
123      */
124     @Nonnull
125     default Stream<P> problems() {
126         Stream<P> result = Stream.empty();
127         for (BuilderProblem.Severity severity : BuilderProblem.Severity.values()) {
128             result = Stream.concat(result, problems(severity));
129         }
130         return result;
131     }
132 
133     /**
134      * Returns all reported and preserved problems for given severity. Note: counters and element count in this
135      * stream does not have to be equal.
136      *
137      * @param severity the severity level to get problems for
138      * @return a stream of problems with the specified severity
139      */
140     @Nonnull
141     Stream<P> problems(BuilderProblem.Severity severity);
142 
143     /**
144      * Creates an "empty" problem collector that doesn't store any problems.
145      *
146      * @param <P> the type of problem
147      * @return an empty problem collector
148      */
149     @Nonnull
150     static <P extends BuilderProblem> ProblemCollector<P> empty() {
151         return new ProblemCollector<>() {
152             @Override
153             public boolean problemsOverflow() {
154                 return false;
155             }
156 
157             @Override
158             public int problemsReportedFor(BuilderProblem.Severity... severities) {
159                 return 0;
160             }
161 
162             @Override
163             public boolean reportProblem(P problem) {
164                 throw new IllegalStateException("empty problem collector");
165             }
166 
167             @Override
168             public Stream<P> problems(BuilderProblem.Severity severity) {
169                 return Stream.empty();
170             }
171         };
172     }
173 
174     /**
175      * Creates new instance of problem collector with configuration from the provided session.
176      *
177      * @param <P> the type of problem
178      * @param protoSession the session containing configuration for the problem collector
179      * @return a new problem collector instance
180      */
181     @Nonnull
182     static <P extends BuilderProblem> ProblemCollector<P> create(@Nullable ProtoSession protoSession) {
183         if (protoSession != null
184                 && protoSession.getUserProperties().containsKey(Constants.MAVEN_BUILDER_MAX_PROBLEMS)) {
185             int limit = Integer.parseInt(protoSession.getUserProperties().get(Constants.MAVEN_BUILDER_MAX_PROBLEMS));
186             return create(limit, p -> true);
187         } else {
188             return create(100);
189         }
190     }
191 
192     /**
193      * Creates new instance of problem collector with the specified maximum problem count limit,
194      * but only preserves problems that match the given filter.
195      *
196      * @param <P>           the type of problem
197      * @param maxCountLimit the maximum number of problems to preserve
198      * @param filter        predicate to decide which problems to record
199      * @return a new filtered problem collector instance
200      */
201     @Nonnull
202     static <P extends BuilderProblem> ProblemCollector<P> create(int maxCountLimit, Predicate<? super P> filter) {
203         return new Impl<>(maxCountLimit, filter);
204     }
205 
206     /**
207      * Creates new instance of problem collector with the specified maximum problem count limit.
208      * Visible for testing only.
209      *
210      * @param <P> the type of problem
211      * @param maxCountLimit the maximum number of problems to preserve
212      * @return a new problem collector instance
213      */
214     @Nonnull
215     static <P extends BuilderProblem> ProblemCollector<P> create(int maxCountLimit) {
216         return create(maxCountLimit, p -> true);
217     }
218 
219     /**
220      * Default implementation of the ProblemCollector interface.
221      *
222      * @param <P> the type of problem
223      */
224     class Impl<P extends BuilderProblem> implements ProblemCollector<P> {
225 
226         private final int maxCountLimit;
227         private final AtomicInteger totalCount;
228         private final ConcurrentMap<BuilderProblem.Severity, LongAdder> counters;
229         private final ConcurrentMap<BuilderProblem.Severity, List<P>> problems;
230         private final Predicate<? super P> filter;
231 
232         private static final List<BuilderProblem.Severity> REVERSED_ORDER = Arrays.stream(
233                         BuilderProblem.Severity.values())
234                 .sorted(Comparator.reverseOrder())
235                 .toList();
236 
237         private Impl(int maxCountLimit, Predicate<? super P> filter) {
238             if (maxCountLimit < 0) {
239                 throw new IllegalArgumentException("maxCountLimit must be non-negative");
240             }
241             this.maxCountLimit = maxCountLimit;
242             this.totalCount = new AtomicInteger();
243             this.counters = new ConcurrentHashMap<>();
244             this.problems = new ConcurrentHashMap<>();
245             this.filter = requireNonNull(filter, "filter");
246         }
247 
248         @Override
249         public int problemsReportedFor(BuilderProblem.Severity... severity) {
250             int result = 0;
251             for (BuilderProblem.Severity s : severity) {
252                 result += getCounter(s).intValue();
253             }
254             return result;
255         }
256 
257         @Override
258         public boolean problemsOverflow() {
259             return totalCount.get() > maxCountLimit;
260         }
261 
262         @Override
263         public boolean reportProblem(P problem) {
264             requireNonNull(problem, "problem");
265             // first apply filter
266             if (!filter.test(problem)) {
267                 // drop without counting towards preserved problems
268                 return false;
269             }
270             int currentCount = totalCount.incrementAndGet();
271             getCounter(problem.getSeverity()).increment();
272             if (currentCount <= maxCountLimit || dropProblemWithLowerSeverity(problem.getSeverity())) {
273                 getProblems(problem.getSeverity()).add(problem);
274                 return true;
275             }
276             return false;
277         }
278 
279         @Override
280         public Stream<P> problems(BuilderProblem.Severity severity) {
281             requireNonNull(severity, "severity");
282             return getProblems(severity).stream();
283         }
284 
285         private LongAdder getCounter(BuilderProblem.Severity severity) {
286             return counters.computeIfAbsent(severity, k -> new LongAdder());
287         }
288 
289         private List<P> getProblems(BuilderProblem.Severity severity) {
290             return problems.computeIfAbsent(severity, k -> new CopyOnWriteArrayList<>());
291         }
292 
293         private boolean dropProblemWithLowerSeverity(BuilderProblem.Severity severity) {
294             for (BuilderProblem.Severity s : REVERSED_ORDER) {
295                 if (s.ordinal() > severity.ordinal()) {
296                     List<P> problems = getProblems(s);
297                     while (!problems.isEmpty()) {
298                         try {
299                             return problems.remove(0) != null;
300                         } catch (IndexOutOfBoundsException e) {
301                             // empty, continue
302                         }
303                     }
304                 }
305             }
306             return false;
307         }
308     }
309 }