AbstractPropertyHelperMojo.java

/*
 * Licensed 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.basepom.mojo.propertyhelper;

import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;
import static org.basepom.mojo.propertyhelper.IgnoreWarnFail.checkIgnoreWarnFailState;

import org.basepom.mojo.propertyhelper.definitions.DateDefinition;
import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
import org.basepom.mojo.propertyhelper.definitions.MacroDefinition;
import org.basepom.mojo.propertyhelper.definitions.NumberDefinition;
import org.basepom.mojo.propertyhelper.definitions.PropertyGroupDefinition;
import org.basepom.mojo.propertyhelper.definitions.StringDefinition;
import org.basepom.mojo.propertyhelper.definitions.UuidDefinition;
import org.basepom.mojo.propertyhelper.fields.NumberField;
import org.basepom.mojo.propertyhelper.groups.PropertyGroup;
import org.basepom.mojo.propertyhelper.groups.PropertyResult;
import org.basepom.mojo.propertyhelper.macros.MacroType;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import javax.inject.Inject;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;

public abstract class AbstractPropertyHelperMojo extends AbstractMojo implements FieldContext {

    private static final FluentLogger LOG = FluentLogger.forEnclosingClass();

    protected final ValueCache valueCache = new ValueCache();
    private IgnoreWarnFail onDuplicateField = IgnoreWarnFail.FAIL;

    /**
     * Defines the action to take if a field is defined multiple times (e.g. as a number and a string).
     * <br>
     * Options are
     * <ul>
     *     <li><code>ignore</code> - ignore multiple definitions silently, retain just the first one found</li>
     *     <li><code>warn</code> - like ignore, but also log a warning message</li>
     *     <li><code>fail</code> - fail the build with an exception</li>
     * </ul>
     */
    @Parameter(defaultValue = "fail", alias = "onDuplicateProperty")
    public void setOnDuplicateField(String onDuplicateField) {
        this.onDuplicateField = IgnoreWarnFail.forString(onDuplicateField);
    }

    private List<String> activeGroups = List.of();

    /**
     * The property groups to activate. If none are given, all property groups are activated.
     * <pre>{@code
     * <activeGroups>
     *     <activeGroup>group1</activeGroup>
     *     <activeGroup>group2</activeGroup>
     *     ...
     * </activeGroups>
     * }</pre>
     */
    @Parameter
    public void setActiveGroups(String... activeGroups) {
        this.activeGroups = Arrays.asList(activeGroups);
    }

    /**
     * Define property groups. A property group contains one or more property definitions. Property groups are active by default unless they are explicitly
     * listed with {@code <activeGroups>...</activeGroups}.
     * <pre>{@code
     * <propertyGroups>
     *     <propertyGroup>
     *         <id>...</id>
     *
     *         <activeOnRelease>true|false</activeOnRelease>
     *         <activeOnSnapshot>true|false</activeOnSnapshot>
     *         <onDuplicateProperty>ignore|warn|fail</onDuplicateProperty>
     *         <onMissingField>ignore|warn|fail</onMissingField>
     *
     *         <properties>
     *             <property>
     *                 <name>...</name>
     *                 <value>...</value>
     *                 <transformers>...</transformers>
     *             </property>
     *             ...
     *         </properties>
     *     </propertyGroup>
     *     ...
     * </propertyGroups>
     * }</pre>
     */
    @Parameter
    public void setPropertyGroups(PropertyGroupDefinition... propertyGroups) {
        this.propertyGroupDefinitions = Arrays.asList(propertyGroups);
    }

    private List<PropertyGroupDefinition> propertyGroupDefinitions = List.of();

    /**
     * Number property definitions.
     *
     * <pre>{@code
     * <numbers>
     *     <number>
     *         <id>...</id>
     *         <skip>true|false</skip>
     *         <export>true|false</export>
     *
     *         <fieldNumber>...</fieldNumber>
     *         <increment>...</increment>
     *         <format>...</format>
     *         <regexp>...</regexp>
     *         <transformers>...</transformers>
     *
     *         <propertyFile>...</propertyFile>
     *         <propertyNameInFile>...</propertyNameInFile>
     *         <initialValue>...</initialValue>
     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
     *     </number>
     *     ...
     * </numbers>
     * }</pre>
     */
    @Parameter
    public void setNumbers(NumberDefinition... numberDefinitions) {
        this.numberDefinitions = Arrays.asList(numberDefinitions);
    }

    private List<NumberDefinition> numberDefinitions = List.of();

    /**
     * String property definitions.
     *
     * <pre>{@code
     * <strings>
     *     <string>
     *         <id>...</id>
     *         <skip>true|false</skip>
     *         <export>true|false</export>
     *
     *         <values>
     *             <value>...</value>
     *             ...
     *         </values>
     *         <blankIsValid>true|false</blankIsValid>
     *         <onMissingValue>ignore|warn|fail</onMissingValue
     *         <format>...</format>
     *         <regexp>...</regexp>
     *         <transformers>...</transformers>
     *
     *         <propertyFile>...</propertyFile>
     *         <propertyNameInFile>...</propertyNameInFile>
     *         <initialValue>...</initialValue>
     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
     *     </string>
     *     ...
     * </strings>
     * }</pre>
     */
    @Parameter
    public void setStrings(StringDefinition... stringDefinitions) {
        this.stringDefinitions = Arrays.asList(stringDefinitions);
    }

    private List<StringDefinition> stringDefinitions = List.of();

    /**
     * Date property definitions.
     *
     * <pre>{@code
     * <dates>
     *     <date>
     *         <id>...</id>
     *         <skip>true|false</skip>
     *         <export>true|false</export>
     *
     *         <value>...</value>
     *         <timezone>...</timezone>
     *         <format>...</format>
     *         <regexp>...</regexp>
     *         <transformers>...</transformers>
     *
     *         <propertyFile>...</propertyFile>
     *         <propertyNameInFile>...</propertyNameInFile>
     *         <initialValue>...</initialValue>
     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
     *     </date>
     *     ...
     * </dates>
     * }</pre>
     */
    @Parameter
    public void setDates(DateDefinition... dateDefinitions) {
        this.dateDefinitions = Arrays.asList(dateDefinitions);
    }

    private List<DateDefinition> dateDefinitions = List.of();

    /**
     * Macro definitions.
     *
     * <pre>{@code
     * <macros>
     *     <macro>
     *         <id>...</id>
     *         <skip>true|false</skip>
     *         <export>true|false</export>
     *
     *         <macroType>...</macroType>
     *         <macroClass>...</macroClass>
     *         <properties>
     *             <some-name>some-value</some-name>
     *             ...
     *         </properties>
     *
     *         <format>...</format>
     *         <regexp>...</regexp>
     *         <transformers>...</transformers>
     *
     *         <propertyFile>...</propertyFile>
     *         <propertyNameInFile>...</propertyNameInFile>
     *         <initialValue>...</initialValue>
     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
     *     </macro>
     *     ...
     * </macros>
     * }</pre>
     */
    @Parameter
    public void setMacros(MacroDefinition... macroDefinitions) {
        this.macroDefinitions = Arrays.asList(macroDefinitions);
    }

    private List<MacroDefinition> macroDefinitions = List.of();

    /**
     * Uuid definitions.
     *
     * <pre>{@code
     * <uuids>
     *     <uuid>
     *         <id>...</id>
     *         <skip>true|false</skip>
     *         <export>true|false</export>
     *
     *         <value>...</value>
     *         <format>...</format>
     *         <regexp>...</regexp>
     *         <transformers>...</transformers>
     *
     *         <propertyFile>...</propertyFile>
     *         <propertyNameInFile>...</propertyNameInFile>
     *         <initialValue>...</initialValue>
     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
     *     </uuid>
     *     ...
     * </uuids>
     * }</pre>
     */
    @Parameter
    public void setUuids(UuidDefinition... uuidDefinitions) {
        this.uuidDefinitions = Arrays.asList(uuidDefinitions);
    }

    private List<UuidDefinition> uuidDefinitions = List.of();

    /**
     * If set to true, goal execution is skipped.
     */
    @Parameter(defaultValue = "false")
    boolean skip;

    /**
     * The maven project (effective pom).
     */
    @Parameter(defaultValue = "${project}", readonly = true)
    MavenProject project;

    @Parameter(defaultValue = "${settings}", readonly = true)
    Settings settings;

    @Parameter(required = true, readonly = true, defaultValue = "${project.basedir}")
    File basedir;

    /**
     * Timestamp for reproducible output archive entries, either formatted as ISO 8601
     * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
     * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
     */
    @Parameter(defaultValue = "${project.build.outputTimestamp}")
    String outputTimestamp;


    @Inject
    public void setMacroMap(Map<String, MacroType> macroMap) {
        this.macroMap = ImmutableMap.copyOf(macroMap);
    }

    private Map<String, MacroType> macroMap = Map.of();

    // internal mojo state.
    private boolean isSnapshot;
    private InterpolatorFactory interpolatorFactory;
    private TransformerRegistry transformerRegistry;

    private Map<String, FieldDefinition<?>> fieldDefinitions = Map.of();
    private List<NumberField> numberFields = List.of();
    private Map<String, String> values = Map.of();

    private Random random;

    @Override
    public void execute() throws MojoExecutionException {
        this.isSnapshot = project.getArtifact().isSnapshot();
        this.interpolatorFactory = new InterpolatorFactory(project.getModel());
        this.transformerRegistry = TransformerRegistry.INSTANCE;

        LOG.atFine().log("Current build is a %s project.", isSnapshot ? "snapshot" : "release");
        LOG.atFiner().log("On duplicate field definitions: %s", onDuplicateField);

        try {
            if (skip) {
                LOG.atFine().log("skipping plugin execution!");
            } else {
                this.random = RandomUtil.createRandomFromSeed(outputTimestamp);

                doExecute();
            }
        } catch (IOException e) {
            throw new MojoExecutionException("While running mojo: ", e);
        }
    }

    @Override
    public MavenProject getProject() {
        return project;
    }

    @Override
    public File getBasedir() {
        return basedir;
    }

    @Override
    public Settings getSettings() {
        return settings;
    }

    @Override
    public Map<String, MacroType> getMacros() {
        return macroMap;
    }

    @Override
    public Properties getProjectProperties() {
        return project.getProperties();
    }

    @Override
    public InterpolatorFactory getInterpolatorFactory() {
        return interpolatorFactory;
    }

    @Override
    public TransformerRegistry getTransformerRegistry() {
        return transformerRegistry;
    }

    @Override
    public Random getRandom() {
        return random;
    }

    protected List<NumberField> getNumbers() {
        return numberFields;
    }

    /**
     * Subclasses need to implement this method.
     */
    protected abstract void doExecute() throws IOException, MojoExecutionException;

    private void addDefinitions(ImmutableMap.Builder<String, FieldDefinition<?>> builder, List<? extends FieldDefinition<?>> newDefinitions) {
        Map<String, FieldDefinition<?>> existingDefinitions = builder.build();

        for (FieldDefinition<?> definition : newDefinitions) {
            if (definition.isSkip()) {
                continue;
            }

            String propertyName = definition.getId();

            if (checkIgnoreWarnFailState(!existingDefinitions.containsKey(propertyName), onDuplicateField,
                () -> format("field definition '%s' does not exist", propertyName),
                () -> format("field definition '%s' already exists!", propertyName))) {
                builder.put(propertyName, definition);
            }
        }
    }

    protected void createFieldDefinitions() {

        ImmutableMap.Builder<String, FieldDefinition<?>> builder = ImmutableMap.builder();
        addDefinitions(builder, numberDefinitions);
        addDefinitions(builder, stringDefinitions);
        addDefinitions(builder, macroDefinitions);
        addDefinitions(builder, dateDefinitions);
        addDefinitions(builder, uuidDefinitions);

        this.fieldDefinitions = builder.build();
    }

    protected void createFields() throws MojoExecutionException, IOException {
        ImmutableList.Builder<NumberField> numberFields = ImmutableList.builder();

        var builder = ImmutableMap.<String, String>builder();

        for (FieldDefinition<?> definition : fieldDefinitions.values()) {
            Field<?, ?> field = definition.createField(this, valueCache);

            if (field instanceof NumberField) {
                numberFields.add((NumberField) field);
            }

            var fieldValue = field.getValue();
            builder.put(field.getFieldName(), fieldValue);

            if (field.isExposeAsProperty()) {
                project.getProperties().setProperty(field.getFieldName(), fieldValue);
                LOG.atFine().log("Exporting Property name: %s, value: %s", field.getFieldName(), fieldValue);
            } else {
                LOG.atFine().log("Property name: %s, value: %s", field.getFieldName(), fieldValue);
            }
        }

        this.numberFields = numberFields.build();
        this.values = builder.build();
    }

    // generates the property groups.
    @SuppressFBWarnings(value = "WMI_WRONG_MAP_ITERATOR")
    public void createGroups() {
        var groupMapBuilder = ImmutableMap.<String, PropertyGroup>builder();
        var resultMapBuilder = ImmutableMap.<String, Set<PropertyResult>>builder();

        Set<String> exportedFields = fieldDefinitions.values().stream()
            .filter(FieldDefinition::isExport)
            .map(FieldDefinition::getId).collect(ImmutableSet.toImmutableSet());

        Set<String> propertyNames = new LinkedHashSet<>(exportedFields);

        propertyGroupDefinitions.forEach(propertyGroupDefinition -> {
            PropertyGroup propertyGroup = propertyGroupDefinition.createGroup(this);
            Set<PropertyResult> propertyResults = propertyGroup.createProperties(values);
            groupMapBuilder.put(propertyGroup.getId(), propertyGroup);
            resultMapBuilder.put(propertyGroup.getId(), propertyResults);
        });

        var groupMap = groupMapBuilder.build();
        var resultMap = resultMapBuilder.build();

        var groupsToAdd = this.activeGroups == null
            ? groupMap.keySet()
            : this.activeGroups;

        for (String groupToAdd : groupsToAdd) {

            var activeGroup = groupMap.get(groupToAdd);
            checkState(activeGroup != null, "activated group '%s' does not exist", groupToAdd);
            var activeResult = resultMap.get(groupToAdd);
            checkState(activeResult != null, "activated group '%s' has no result", groupToAdd);

            if (activeGroup.checkActive(isSnapshot)) {
                for (PropertyResult propertyResult : activeResult) {
                    String propertyName = propertyResult.getPropertyName();

                    if (checkIgnoreWarnFailState(!propertyNames.contains(propertyName), activeGroup.getOnDuplicateProperty(),
                        () -> format("property '%s' is not exposed", propertyName),
                        () -> format("property '%s' is already exposed!", propertyName))) {

                        project.getProperties().setProperty(propertyName, propertyResult.getPropertyValue());
                    }
                }
            } else {
                LOG.atFine().log("Skipping property group %s, not active", activeGroup);
            }
        }
    }


}