ValidationMode.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.cling.invoker.mvnval;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import org.apache.maven.api.services.ModelBuilder.ModelBuilderSession;
import org.apache.maven.api.services.ModelBuilderException;
import org.apache.maven.api.services.ModelBuilderRequest;
import org.apache.maven.api.services.ModelBuilderRequest.RequestType;
import org.apache.maven.api.services.ModelBuilderResult;
import org.apache.maven.api.services.ModelProblem;

/**
 * How far the model builder runs before the verdict is taken.
 */
enum ValidationMode {
    /**
     * Reads the file and raw models only. Resolves no parent and opens no connection, so it works
     * offline and on a POM whose parent is not published yet, at the cost of the checks that need
     * an effective model.
     */
    RAW {
        @Override
        List<ModelProblem> problemsFor(ModelBuilderSession builderSession, ModelBuilderRequest request)
                throws ModelBuilderException {
            return rawProblems(builderSession, request);
        }
    },

    /**
     * Everything {@code raw} checks, plus what needs the effective model: parents and imported
     * boms are resolved, so a dependency whose version comes from the parent is checked too.
     */
    EFFECTIVE {
        @Override
        List<ModelProblem> problemsFor(ModelBuilderSession builderSession, ModelBuilderRequest request)
                throws ModelBuilderException {
            // Raw first, and on its own request. The effective build reports nothing about the
            // reactor, so a subproject this POM names but does not have passes unnoticed through
            // build() alone, while mvn refuses to read the project.
            List<ModelProblem> problems = rawProblems(builderSession, request);
            if (problems.stream().anyMatch(problem -> problem.getSeverity() == ModelProblem.Severity.ERROR)) {
                // Nothing is gained by resolving parents for a POM that is already rejected, and
                // it would go to the network to say so.
                return problems;
            }
            List<ModelProblem> all = new ArrayList<>(problems);
            // No problem is reported twice because the builder caches the file and raw models per
            // source, so this request hits that cache and re-runs neither check. Partition that
            // cache by request type and every raw problem would appear twice.
            try {
                // Completes the model: inheritance, interpolation, then validateEffectiveModel. It
                // throws once it has collected an error, so throwing is the ordinary path here.
                ModelBuilderRequest effectiveRequest = ModelBuilderRequest.builder(request)
                        .requestType(RequestType.BUILD_EFFECTIVE)
                        .build();
                all.addAll(problemsOf(builderSession.build(effectiveRequest)));
                return all;
            } catch (ModelBuilderException e) {
                // Carry the raw warnings out with the exception's own problems. Letting it fly
                // would report only what the effective pass saw and lose the rest.
                ModelBuilderResult result = e.getResult();
                if (result == null) {
                    throw e;
                }
                all.addAll(problemsOf(result));
                return all;
            }
        }
    };

    /** Collects the problems this mode can see, in the order the model builder reports them. */
    abstract List<ModelProblem> problemsFor(ModelBuilderSession builderSession, ModelBuilderRequest request)
            throws ModelBuilderException;

    /**
     * The raw pass. {@code BUILD_PROJECT} is what makes the builder look for the parent beside the
     * POM and check its {@code relativePath}; it does not make this a build, since {@code validate}
     * stops before inheritance.
     */
    private static List<ModelProblem> rawProblems(ModelBuilderSession builderSession, ModelBuilderRequest request)
            throws ModelBuilderException {
        return problemsOf(builderSession.validate(request));
    }

    static List<ModelProblem> problemsOf(ModelBuilderResult result) {
        return result.getProblemCollector().problems().toList();
    }

    static ValidationMode parse(String name) {
        return Arrays.stream(values())
                .filter(mode -> mode.name().equalsIgnoreCase(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Unknown mode '" + name + "', expected one of: "
                        + Arrays.stream(values())
                                .map(mode -> mode.name().toLowerCase(Locale.ROOT))
                                .collect(Collectors.joining(", "))));
    }
}