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