001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2020-2024 Andres Almiray.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.kordamp.maven.checker;
019
020import org.apache.maven.model.Dependency;
021import org.apache.maven.model.Model;
022import org.apache.maven.project.MavenProject;
023
024import java.util.ArrayList;
025import java.util.List;
026
027import static java.lang.System.lineSeparator;
028
029/**
030 * Checks if a POM file is a minimal BOM file.
031 * <p>
032 * The following blocks are required:
033 * <ul>
034 *   <li>&lt;dependencyManagement&gt;</li>
035 * </ul>
036 * <p>
037 * The following blocks are forbidden:
038 * <ul>
039 *   <li>&lt;build&gt;</li>
040 *   <li>&lt;reporting&gt;</li>
041 *   <li>&lt;dependencies&gt;</li>
042 *   <li>&lt;repositories&gt;</li>
043 *   <li>&lt;pluginRepositories&gt;</li>
044 *   <li>&lt;profiles&gt;</li>
045 *   <li>&lt;modules&gt;</li>
046 * </ul>
047 *
048 * @author Andres Almiray
049 * @since 1.0.0
050 */
051public class BomChecker {
052    public static class Configuration {
053        private boolean failOnError;
054
055        public boolean isFailOnError() {
056            return failOnError;
057        }
058
059        /**
060         * Sets the value for {@code failOnError}.
061         *
062         * @param failOnError if {@code true} fails the build when an error is encountered.
063         */
064        public Configuration withFailOnError(boolean failOnError) {
065            this.failOnError = failOnError;
066            return this;
067        }
068    }
069
070    /**
071     * Checks the resolved model of the given MaveProject for compliance.
072     *
073     * @param log           the logger to use.
074     * @param project       the project to be checked.
075     * @param configuration configuration required for inspection.
076     * @throws PomCheckException if the POM is invalid
077     */
078    public static void check(Logger log, MavenProject project, Configuration configuration) throws PomCheckException {
079        Model model = project.getOriginalModel();
080
081        List<String> errors = new ArrayList<>();
082
083        // 1. is it packaged as 'pom'?
084        log.debug("Checking <packaging>");
085        if (!"pom".equals(model.getPackaging())) {
086            errors.add("The value of <packaging> must be 'pom'.");
087        }
088
089        log.debug("Checking <dependencyManagement>");
090        // 2. must have a <dependencyManagement> block
091        if (null != model.getDependencyManagement()) {
092            List<Dependency> dependencies = model.getDependencyManagement().getDependencies();
093            if (dependencies == null || dependencies.isEmpty()) {
094                errors.add("No dependencies have been defined in <dependencyManagement>.");
095            }
096        } else {
097            errors.add("No <dependencyManagement> block has been defined.");
098        }
099
100        log.debug("Checking <build>");
101        if (null != model.getBuild()) {
102            errors.add("The <build> block should not be present.");
103        }
104
105        log.debug("Checking <reporting>");
106        if (null != model.getReporting()) {
107            errors.add("The <reporting> block should not be present.");
108        }
109
110        log.debug("Checking <dependencies>");
111        if (null != model.getDependencies() && !model.getDependencies().isEmpty()) {
112            errors.add("The <dependencies> block should not be present.");
113        }
114
115        log.debug("Checking <repositories>");
116        if (null != model.getRepositories() && !model.getRepositories().isEmpty()) {
117            errors.add("The <repositories> block should not be present.");
118        }
119
120        log.debug("Checking <pluginRepositories>");
121        if (null != model.getPluginRepositories() && !model.getPluginRepositories().isEmpty()) {
122            errors.add("The <pluginRepositories> block should not be present.");
123        }
124
125        log.debug("Checking <profiles>");
126        if (null != model.getProfiles() && !model.getProfiles().isEmpty()) {
127            errors.add("The <profiles> block should not be present.");
128        }
129
130        log.debug("Checking <modules>");
131        if (null != model.getModules() && !model.getModules().isEmpty()) {
132            errors.add("The <modules> block should not be present.");
133        }
134
135        if (!errors.isEmpty()) {
136            StringBuilder b = new StringBuilder(lineSeparator())
137                .append("The POM file")
138                .append(lineSeparator())
139                .append(project.getFile().getAbsolutePath())
140                .append(lineSeparator())
141                .append("is not a valid BOM due to the following reasons:")
142                .append(lineSeparator());
143            for (String s : errors) {
144                b.append(" * ").append(s).append(lineSeparator());
145            }
146
147            if (configuration.isFailOnError()) {
148                throw new PomCheckException(b.toString());
149            } else {
150                log.warn(b.toString());
151            }
152        } else {
153            log.info("BOM {} passes all checks.", project.getFile().getAbsolutePath());
154        }
155    }
156}