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      int problemsReportedFor(BuilderProblem.Severity... severities);
97  
98      /**
99       * Returns {@code true} if reported problem count exceeded allowed count, and issues were lost. When this
100      * method returns {@code true}, it means that element count of stream returned by method {@link #problems()}
101      * and the counter returned by {@link #totalProblemsReported()} are not equal (latter is bigger than former).
102      */
103     boolean problemsOverflow();
104 
105     /**
106      * Reports a problem: always maintains the counters, but whether problem is preserved in memory, depends on
107      * implementation and its configuration.
108      *
109      * @return {@code true} if passed problem is preserved by this call.
110      */
111     boolean reportProblem(P problem);
112 
113     /**
114      * Returns all reported and preserved problems ordered by severity in decreasing order. Note: counters and
115      * element count in this stream does not have to be equal.
116      */
117     @Nonnull
118     default Stream<P> problems() {
119         Stream<P> result = Stream.empty();
120         for (BuilderProblem.Severity severity : BuilderProblem.Severity.values()) {
121             result = Stream.concat(result, problems(severity));
122         }
123         return result;
124     }
125 
126     /**
127      * Returns all reported and preserved problems for given severity. Note: counters and element count in this
128      * stream does not have to be equal.
129      */
130     @Nonnull
131     Stream<P> problems(BuilderProblem.Severity severity);
132 
133     /**
134      * Creates "empty" problem collector.
135      */
136     @Nonnull
137     static <P extends BuilderProblem> ProblemCollector<P> empty() {
138         return new ProblemCollector<>() {
139             @Override
140             public boolean problemsOverflow() {
141                 return false;
142             }
143 
144             @Override
145             public int problemsReportedFor(BuilderProblem.Severity... severities) {
146                 return 0;
147             }
148 
149             @Override
150             public boolean reportProblem(P problem) {
151                 throw new IllegalStateException("empty problem collector");
152             }
153 
154             @Override
155             public Stream<P> problems(BuilderProblem.Severity severity) {
156                 return Stream.empty();
157             }
158         };
159     }
160 
161     /**
162      * Creates new instance of problem collector.
163      */
164     @Nonnull
165     static <P extends BuilderProblem> ProblemCollector<P> create(@Nullable ProtoSession protoSession) {
166         if (protoSession != null
167                 && protoSession.getUserProperties().containsKey(Constants.MAVEN_BUILDER_MAX_PROBLEMS)) {
168             return new Impl<>(
169                     Integer.parseInt(protoSession.getUserProperties().get(Constants.MAVEN_BUILDER_MAX_PROBLEMS)));
170         } else {
171             return create(100);
172         }
173     }
174 
175     /**
176      * Creates new instance of problem collector. Visible for testing only.
177      */
178     @Nonnull
179     static <P extends BuilderProblem> ProblemCollector<P> create(int maxCountLimit) {
180         return new Impl<>(maxCountLimit);
181     }
182 
183     class Impl<P extends BuilderProblem> implements ProblemCollector<P> {
184 
185         private final int maxCountLimit;
186         private final AtomicInteger totalCount;
187         private final ConcurrentMap<BuilderProblem.Severity, LongAdder> counters;
188         private final ConcurrentMap<BuilderProblem.Severity, List<P>> problems;
189 
190         private static final List<BuilderProblem.Severity> REVERSED_ORDER = Arrays.stream(
191                         BuilderProblem.Severity.values())
192                 .sorted(Comparator.reverseOrder())
193                 .toList();
194 
195         private Impl(int maxCountLimit) {
196             if (maxCountLimit < 0) {
197                 throw new IllegalArgumentException("maxCountLimit must be non-negative");
198             }
199             this.maxCountLimit = maxCountLimit;
200             this.totalCount = new AtomicInteger();
201             this.counters = new ConcurrentHashMap<>();
202             this.problems = new ConcurrentHashMap<>();
203         }
204 
205         @Override
206         public int problemsReportedFor(BuilderProblem.Severity... severity) {
207             int result = 0;
208             for (BuilderProblem.Severity s : severity) {
209                 result += getCounter(s).intValue();
210             }
211             return result;
212         }
213 
214         @Override
215         public boolean problemsOverflow() {
216             return totalCount.get() > maxCountLimit;
217         }
218 
219         @Override
220         public boolean reportProblem(P problem) {
221             requireNonNull(problem, "problem");
222             int currentCount = totalCount.incrementAndGet();
223             getCounter(problem.getSeverity()).increment();
224             if (currentCount <= maxCountLimit || dropProblemWithLowerSeverity(problem.getSeverity())) {
225                 getProblems(problem.getSeverity()).add(problem);
226                 return true;
227             }
228             return false;
229         }
230 
231         @Override
232         public Stream<P> problems(BuilderProblem.Severity severity) {
233             requireNonNull(severity, "severity");
234             return getProblems(severity).stream();
235         }
236 
237         private LongAdder getCounter(BuilderProblem.Severity severity) {
238             return counters.computeIfAbsent(severity, k -> new LongAdder());
239         }
240 
241         private List<P> getProblems(BuilderProblem.Severity severity) {
242             return problems.computeIfAbsent(severity, k -> new CopyOnWriteArrayList<>());
243         }
244 
245         private boolean dropProblemWithLowerSeverity(BuilderProblem.Severity severity) {
246             for (BuilderProblem.Severity s : REVERSED_ORDER) {
247                 if (s.ordinal() > severity.ordinal()) {
248                     List<P> problems = getProblems(s);
249                     while (!problems.isEmpty()) {
250                         try {
251                             return problems.remove(0) != null;
252                         } catch (IndexOutOfBoundsException e) {
253                             // empty, continue
254                         }
255                     }
256                 }
257             }
258             return false;
259         }
260     }
261 }