ProblemCollector.java

/*
 * 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 org.apache.maven.api.services;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.apache.maven.api.Constants;
import org.apache.maven.api.ProtoSession;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;

import static java.util.Objects.requireNonNull;

/**
 * Collects problems that were encountered during project building.
 *
 * @param <P> The type of the problem.
 * @since 4.0.0
 */
@Experimental
public interface ProblemCollector<P extends BuilderProblem> {
    /**
     * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
     * {@link org.apache.maven.api.services.BuilderProblem.Severity#WARNING}. This check is logically equivalent
     * to "is there any problem reported?", given warning is the lowest severity.
     */
    default boolean hasWarningProblems() {
        return hasProblemsFor(BuilderProblem.Severity.WARNING);
    }

    /**
     * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
     * {@link org.apache.maven.api.services.BuilderProblem.Severity#ERROR}.
     */
    default boolean hasErrorProblems() {
        return hasProblemsFor(BuilderProblem.Severity.ERROR);
    }

    /**
     * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
     * {@link org.apache.maven.api.services.BuilderProblem.Severity#FATAL}.
     */
    default boolean hasFatalProblems() {
        return hasProblemsFor(BuilderProblem.Severity.FATAL);
    }

    /**
     * Returns {@code true} if there is at least one problem collected with severity equal or more severe than
     * passed in severity.
     */
    default boolean hasProblemsFor(BuilderProblem.Severity severity) {
        requireNonNull(severity, "severity");
        for (BuilderProblem.Severity s : BuilderProblem.Severity.values()) {
            if (s.ordinal() <= severity.ordinal() && problemsReportedFor(s) > 0) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns total count of problems reported.
     */
    default int totalProblemsReported() {
        return problemsReportedFor(BuilderProblem.Severity.values());
    }

    /**
     * Returns count of problems reported for given severities.
     *
     * @param severities the severity levels to count problems for
     * @return the total count of problems for the specified severities
     */
    int problemsReportedFor(BuilderProblem.Severity... severities);

    /**
     * Returns {@code true} if reported problem count exceeded allowed count, and issues were lost. When this
     * method returns {@code true}, it means that element count of stream returned by method {@link #problems()}
     * and the counter returned by {@link #totalProblemsReported()} are not equal (latter is bigger than former).
     *
     * @return true if the problem collector has overflowed and some problems were not preserved
     */
    boolean problemsOverflow();

    /**
     * Reports a problem: always maintains the counters, but whether problem is preserved in memory, depends on
     * implementation and its configuration.
     *
     * @param problem the problem to report
     * @return {@code true} if passed problem is preserved by this call.
     */
    boolean reportProblem(P problem);

    /**
     * Returns all reported and preserved problems ordered by severity in decreasing order. Note: counters and
     * element count in this stream does not have to be equal.
     */
    @Nonnull
    default Stream<P> problems() {
        Stream<P> result = Stream.empty();
        for (BuilderProblem.Severity severity : BuilderProblem.Severity.values()) {
            result = Stream.concat(result, problems(severity));
        }
        return result;
    }

    /**
     * Returns all reported and preserved problems for given severity. Note: counters and element count in this
     * stream does not have to be equal.
     *
     * @param severity the severity level to get problems for
     * @return a stream of problems with the specified severity
     */
    @Nonnull
    Stream<P> problems(BuilderProblem.Severity severity);

    /**
     * Creates an "empty" problem collector that doesn't store any problems.
     *
     * @param <P> the type of problem
     * @return an empty problem collector
     */
    @Nonnull
    static <P extends BuilderProblem> ProblemCollector<P> empty() {
        return new ProblemCollector<>() {
            @Override
            public boolean problemsOverflow() {
                return false;
            }

            @Override
            public int problemsReportedFor(BuilderProblem.Severity... severities) {
                return 0;
            }

            @Override
            public boolean reportProblem(P problem) {
                throw new IllegalStateException("empty problem collector");
            }

            @Override
            public Stream<P> problems(BuilderProblem.Severity severity) {
                return Stream.empty();
            }
        };
    }

    /**
     * Creates new instance of problem collector with configuration from the provided session.
     *
     * @param <P> the type of problem
     * @param protoSession the session containing configuration for the problem collector
     * @return a new problem collector instance
     */
    @Nonnull
    static <P extends BuilderProblem> ProblemCollector<P> create(@Nullable ProtoSession protoSession) {
        if (protoSession != null
                && protoSession.getUserProperties().containsKey(Constants.MAVEN_BUILDER_MAX_PROBLEMS)) {
            int limit = Integer.parseInt(protoSession.getUserProperties().get(Constants.MAVEN_BUILDER_MAX_PROBLEMS));
            return create(limit, p -> true);
        } else {
            return create(100);
        }
    }

    /**
     * Creates new instance of problem collector with the specified maximum problem count limit,
     * but only preserves problems that match the given filter.
     *
     * @param <P>           the type of problem
     * @param maxCountLimit the maximum number of problems to preserve
     * @param filter        predicate to decide which problems to record
     * @return a new filtered problem collector instance
     */
    @Nonnull
    static <P extends BuilderProblem> ProblemCollector<P> create(int maxCountLimit, Predicate<? super P> filter) {
        return new Impl<>(maxCountLimit, filter);
    }

    /**
     * Creates new instance of problem collector with the specified maximum problem count limit.
     * Visible for testing only.
     *
     * @param <P> the type of problem
     * @param maxCountLimit the maximum number of problems to preserve
     * @return a new problem collector instance
     */
    @Nonnull
    static <P extends BuilderProblem> ProblemCollector<P> create(int maxCountLimit) {
        return create(maxCountLimit, p -> true);
    }

    /**
     * Default implementation of the ProblemCollector interface.
     *
     * @param <P> the type of problem
     */
    class Impl<P extends BuilderProblem> implements ProblemCollector<P> {

        private final int maxCountLimit;
        private final AtomicInteger totalCount;
        private final ConcurrentMap<BuilderProblem.Severity, LongAdder> counters;
        private final ConcurrentMap<BuilderProblem.Severity, List<P>> problems;
        private final Predicate<? super P> filter;

        private static final List<BuilderProblem.Severity> REVERSED_ORDER = Arrays.stream(
                        BuilderProblem.Severity.values())
                .sorted(Comparator.reverseOrder())
                .toList();

        private Impl(int maxCountLimit, Predicate<? super P> filter) {
            if (maxCountLimit < 0) {
                throw new IllegalArgumentException("maxCountLimit must be non-negative");
            }
            this.maxCountLimit = maxCountLimit;
            this.totalCount = new AtomicInteger();
            this.counters = new ConcurrentHashMap<>();
            this.problems = new ConcurrentHashMap<>();
            this.filter = requireNonNull(filter, "filter");
        }

        @Override
        public int problemsReportedFor(BuilderProblem.Severity... severity) {
            int result = 0;
            for (BuilderProblem.Severity s : severity) {
                result += getCounter(s).intValue();
            }
            return result;
        }

        @Override
        public boolean problemsOverflow() {
            return totalCount.get() > maxCountLimit;
        }

        @Override
        public boolean reportProblem(P problem) {
            requireNonNull(problem, "problem");
            // first apply filter
            if (!filter.test(problem)) {
                // drop without counting towards preserved problems
                return false;
            }
            int currentCount = totalCount.incrementAndGet();
            getCounter(problem.getSeverity()).increment();
            if (currentCount <= maxCountLimit || dropProblemWithLowerSeverity(problem.getSeverity())) {
                getProblems(problem.getSeverity()).add(problem);
                return true;
            }
            return false;
        }

        @Override
        public Stream<P> problems(BuilderProblem.Severity severity) {
            requireNonNull(severity, "severity");
            return getProblems(severity).stream();
        }

        private LongAdder getCounter(BuilderProblem.Severity severity) {
            return counters.computeIfAbsent(severity, k -> new LongAdder());
        }

        private List<P> getProblems(BuilderProblem.Severity severity) {
            return problems.computeIfAbsent(severity, k -> new CopyOnWriteArrayList<>());
        }

        private boolean dropProblemWithLowerSeverity(BuilderProblem.Severity severity) {
            for (BuilderProblem.Severity s : REVERSED_ORDER) {
                if (s.ordinal() > severity.ordinal()) {
                    List<P> problems = getProblems(s);
                    while (!problems.isEmpty()) {
                        try {
                            return problems.remove(0) != null;
                        } catch (IndexOutOfBoundsException e) {
                            // empty, continue
                        }
                    }
                }
            }
            return false;
        }
    }
}