001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package org.basepom.mojo.propertyhelper;
016
017import static com.google.common.base.Preconditions.checkState;
018import static java.lang.String.format;
019import static org.basepom.mojo.propertyhelper.IgnoreWarnFail.checkIgnoreWarnFailState;
020
021import org.basepom.mojo.propertyhelper.definitions.DateDefinition;
022import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
023import org.basepom.mojo.propertyhelper.definitions.MacroDefinition;
024import org.basepom.mojo.propertyhelper.definitions.NumberDefinition;
025import org.basepom.mojo.propertyhelper.definitions.PropertyGroupDefinition;
026import org.basepom.mojo.propertyhelper.definitions.StringDefinition;
027import org.basepom.mojo.propertyhelper.definitions.UuidDefinition;
028import org.basepom.mojo.propertyhelper.fields.NumberField;
029import org.basepom.mojo.propertyhelper.groups.PropertyGroup;
030import org.basepom.mojo.propertyhelper.groups.PropertyResult;
031import org.basepom.mojo.propertyhelper.macros.MacroType;
032
033import java.io.File;
034import java.io.IOException;
035import java.util.Arrays;
036import java.util.LinkedHashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Properties;
040import java.util.Random;
041import java.util.Set;
042import javax.inject.Inject;
043
044import com.google.common.collect.ImmutableList;
045import com.google.common.collect.ImmutableMap;
046import com.google.common.collect.ImmutableSet;
047import com.google.common.flogger.FluentLogger;
048import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
049import org.apache.maven.plugin.AbstractMojo;
050import org.apache.maven.plugin.MojoExecutionException;
051import org.apache.maven.plugins.annotations.Parameter;
052import org.apache.maven.project.MavenProject;
053import org.apache.maven.settings.Settings;
054
055public abstract class AbstractPropertyHelperMojo extends AbstractMojo implements FieldContext {
056
057    private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
058
059    protected final ValueCache valueCache = new ValueCache();
060    private IgnoreWarnFail onDuplicateField = IgnoreWarnFail.FAIL;
061
062    /**
063     * Defines the action to take if a field is defined multiple times (e.g. as a number and a string).
064     * <br>
065     * Options are
066     * <ul>
067     *     <li><code>ignore</code> - ignore multiple definitions silently, retain just the first one found</li>
068     *     <li><code>warn</code> - like ignore, but also log a warning message</li>
069     *     <li><code>fail</code> - fail the build with an exception</li>
070     * </ul>
071     */
072    @Parameter(defaultValue = "fail", alias = "onDuplicateProperty")
073    public void setOnDuplicateField(String onDuplicateField) {
074        this.onDuplicateField = IgnoreWarnFail.forString(onDuplicateField);
075    }
076
077    private List<String> activeGroups = List.of();
078
079    /**
080     * The property groups to activate. If none are given, all property groups are activated.
081     * <pre>{@code
082     * <activeGroups>
083     *     <activeGroup>group1</activeGroup>
084     *     <activeGroup>group2</activeGroup>
085     *     ...
086     * </activeGroups>
087     * }</pre>
088     */
089    @Parameter
090    public void setActiveGroups(String... activeGroups) {
091        this.activeGroups = Arrays.asList(activeGroups);
092    }
093
094    /**
095     * Define property groups. A property group contains one or more property definitions. Property groups are active by default unless they are explicitly
096     * listed with {@code <activeGroups>...</activeGroups}.
097     * <pre>{@code
098     * <propertyGroups>
099     *     <propertyGroup>
100     *         <id>...</id>
101     *
102     *         <activeOnRelease>true|false</activeOnRelease>
103     *         <activeOnSnapshot>true|false</activeOnSnapshot>
104     *         <onDuplicateProperty>ignore|warn|fail</onDuplicateProperty>
105     *         <onMissingField>ignore|warn|fail</onMissingField>
106     *
107     *         <properties>
108     *             <property>
109     *                 <name>...</name>
110     *                 <value>...</value>
111     *                 <transformers>...</transformers>
112     *             </property>
113     *             ...
114     *         </properties>
115     *     </propertyGroup>
116     *     ...
117     * </propertyGroups>
118     * }</pre>
119     */
120    @Parameter
121    public void setPropertyGroups(PropertyGroupDefinition... propertyGroups) {
122        this.propertyGroupDefinitions = Arrays.asList(propertyGroups);
123    }
124
125    private List<PropertyGroupDefinition> propertyGroupDefinitions = List.of();
126
127    /**
128     * Number property definitions.
129     *
130     * <pre>{@code
131     * <numbers>
132     *     <number>
133     *         <id>...</id>
134     *         <skip>true|false</skip>
135     *         <export>true|false</export>
136     *
137     *         <fieldNumber>...</fieldNumber>
138     *         <increment>...</increment>
139     *         <format>...</format>
140     *         <regexp>...</regexp>
141     *         <transformers>...</transformers>
142     *
143     *         <propertyFile>...</propertyFile>
144     *         <propertyNameInFile>...</propertyNameInFile>
145     *         <initialValue>...</initialValue>
146     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
147     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
148     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
149     *     </number>
150     *     ...
151     * </numbers>
152     * }</pre>
153     */
154    @Parameter
155    public void setNumbers(NumberDefinition... numberDefinitions) {
156        this.numberDefinitions = Arrays.asList(numberDefinitions);
157    }
158
159    private List<NumberDefinition> numberDefinitions = List.of();
160
161    /**
162     * String property definitions.
163     *
164     * <pre>{@code
165     * <strings>
166     *     <string>
167     *         <id>...</id>
168     *         <skip>true|false</skip>
169     *         <export>true|false</export>
170     *
171     *         <values>
172     *             <value>...</value>
173     *             ...
174     *         </values>
175     *         <blankIsValid>true|false</blankIsValid>
176     *         <onMissingValue>ignore|warn|fail</onMissingValue
177     *         <format>...</format>
178     *         <regexp>...</regexp>
179     *         <transformers>...</transformers>
180     *
181     *         <propertyFile>...</propertyFile>
182     *         <propertyNameInFile>...</propertyNameInFile>
183     *         <initialValue>...</initialValue>
184     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
185     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
186     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
187     *     </string>
188     *     ...
189     * </strings>
190     * }</pre>
191     */
192    @Parameter
193    public void setStrings(StringDefinition... stringDefinitions) {
194        this.stringDefinitions = Arrays.asList(stringDefinitions);
195    }
196
197    private List<StringDefinition> stringDefinitions = List.of();
198
199    /**
200     * Date property definitions.
201     *
202     * <pre>{@code
203     * <dates>
204     *     <date>
205     *         <id>...</id>
206     *         <skip>true|false</skip>
207     *         <export>true|false</export>
208     *
209     *         <value>...</value>
210     *         <timezone>...</timezone>
211     *         <format>...</format>
212     *         <regexp>...</regexp>
213     *         <transformers>...</transformers>
214     *
215     *         <propertyFile>...</propertyFile>
216     *         <propertyNameInFile>...</propertyNameInFile>
217     *         <initialValue>...</initialValue>
218     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
219     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
220     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
221     *     </date>
222     *     ...
223     * </dates>
224     * }</pre>
225     */
226    @Parameter
227    public void setDates(DateDefinition... dateDefinitions) {
228        this.dateDefinitions = Arrays.asList(dateDefinitions);
229    }
230
231    private List<DateDefinition> dateDefinitions = List.of();
232
233    /**
234     * Macro definitions.
235     *
236     * <pre>{@code
237     * <macros>
238     *     <macro>
239     *         <id>...</id>
240     *         <skip>true|false</skip>
241     *         <export>true|false</export>
242     *
243     *         <macroType>...</macroType>
244     *         <macroClass>...</macroClass>
245     *         <properties>
246     *             <some-name>some-value</some-name>
247     *             ...
248     *         </properties>
249     *
250     *         <format>...</format>
251     *         <regexp>...</regexp>
252     *         <transformers>...</transformers>
253     *
254     *         <propertyFile>...</propertyFile>
255     *         <propertyNameInFile>...</propertyNameInFile>
256     *         <initialValue>...</initialValue>
257     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
258     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
259     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
260     *     </macro>
261     *     ...
262     * </macros>
263     * }</pre>
264     */
265    @Parameter
266    public void setMacros(MacroDefinition... macroDefinitions) {
267        this.macroDefinitions = Arrays.asList(macroDefinitions);
268    }
269
270    private List<MacroDefinition> macroDefinitions = List.of();
271
272    /**
273     * Uuid definitions.
274     *
275     * <pre>{@code
276     * <uuids>
277     *     <uuid>
278     *         <id>...</id>
279     *         <skip>true|false</skip>
280     *         <export>true|false</export>
281     *
282     *         <value>...</value>
283     *         <format>...</format>
284     *         <regexp>...</regexp>
285     *         <transformers>...</transformers>
286     *
287     *         <propertyFile>...</propertyFile>
288     *         <propertyNameInFile>...</propertyNameInFile>
289     *         <initialValue>...</initialValue>
290     *         <onMissingFile>ignore|warn|fail|create</onMissingFile>
291     *         <onMissingFileProperty>ignore|warn|fail|create</onMissingFileProperty>
292     *         <onMissingProperty>ignore|warn|fail</onMissingProperty>
293     *     </uuid>
294     *     ...
295     * </uuids>
296     * }</pre>
297     */
298    @Parameter
299    public void setUuids(UuidDefinition... uuidDefinitions) {
300        this.uuidDefinitions = Arrays.asList(uuidDefinitions);
301    }
302
303    private List<UuidDefinition> uuidDefinitions = List.of();
304
305    /**
306     * If set to true, goal execution is skipped.
307     */
308    @Parameter(defaultValue = "false")
309    boolean skip;
310
311    /**
312     * The maven project (effective pom).
313     */
314    @Parameter(defaultValue = "${project}", readonly = true)
315    MavenProject project;
316
317    @Parameter(defaultValue = "${settings}", readonly = true)
318    Settings settings;
319
320    @Parameter(required = true, readonly = true, defaultValue = "${project.basedir}")
321    File basedir;
322
323    /**
324     * Timestamp for reproducible output archive entries, either formatted as ISO 8601
325     * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
326     * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
327     */
328    @Parameter(defaultValue = "${project.build.outputTimestamp}")
329    String outputTimestamp;
330
331
332    @Inject
333    public void setMacroMap(Map<String, MacroType> macroMap) {
334        this.macroMap = ImmutableMap.copyOf(macroMap);
335    }
336
337    private Map<String, MacroType> macroMap = Map.of();
338
339    // internal mojo state.
340    private boolean isSnapshot;
341    private InterpolatorFactory interpolatorFactory;
342    private TransformerRegistry transformerRegistry;
343
344    private Map<String, FieldDefinition<?>> fieldDefinitions = Map.of();
345    private List<NumberField> numberFields = List.of();
346    private Map<String, String> values = Map.of();
347
348    private Random random;
349
350    @Override
351    public void execute() throws MojoExecutionException {
352        this.isSnapshot = project.getArtifact().isSnapshot();
353        this.interpolatorFactory = new InterpolatorFactory(project.getModel());
354        this.transformerRegistry = TransformerRegistry.INSTANCE;
355
356        LOG.atFine().log("Current build is a %s project.", isSnapshot ? "snapshot" : "release");
357        LOG.atFiner().log("On duplicate field definitions: %s", onDuplicateField);
358
359        try {
360            if (skip) {
361                LOG.atFine().log("skipping plugin execution!");
362            } else {
363                this.random = RandomUtil.createRandomFromSeed(outputTimestamp);
364
365                doExecute();
366            }
367        } catch (IOException e) {
368            throw new MojoExecutionException("While running mojo: ", e);
369        }
370    }
371
372    @Override
373    public MavenProject getProject() {
374        return project;
375    }
376
377    @Override
378    public File getBasedir() {
379        return basedir;
380    }
381
382    @Override
383    public Settings getSettings() {
384        return settings;
385    }
386
387    @Override
388    public Map<String, MacroType> getMacros() {
389        return macroMap;
390    }
391
392    @Override
393    public Properties getProjectProperties() {
394        return project.getProperties();
395    }
396
397    @Override
398    public InterpolatorFactory getInterpolatorFactory() {
399        return interpolatorFactory;
400    }
401
402    @Override
403    public TransformerRegistry getTransformerRegistry() {
404        return transformerRegistry;
405    }
406
407    @Override
408    public Random getRandom() {
409        return random;
410    }
411
412    protected List<NumberField> getNumbers() {
413        return numberFields;
414    }
415
416    /**
417     * Subclasses need to implement this method.
418     */
419    protected abstract void doExecute() throws IOException, MojoExecutionException;
420
421    private void addDefinitions(ImmutableMap.Builder<String, FieldDefinition<?>> builder, List<? extends FieldDefinition<?>> newDefinitions) {
422        Map<String, FieldDefinition<?>> existingDefinitions = builder.build();
423
424        for (FieldDefinition<?> definition : newDefinitions) {
425            if (definition.isSkip()) {
426                continue;
427            }
428
429            String propertyName = definition.getId();
430
431            if (checkIgnoreWarnFailState(!existingDefinitions.containsKey(propertyName), onDuplicateField,
432                () -> format("field definition '%s' does not exist", propertyName),
433                () -> format("field definition '%s' already exists!", propertyName))) {
434                builder.put(propertyName, definition);
435            }
436        }
437    }
438
439    protected void createFieldDefinitions() {
440
441        ImmutableMap.Builder<String, FieldDefinition<?>> builder = ImmutableMap.builder();
442        addDefinitions(builder, numberDefinitions);
443        addDefinitions(builder, stringDefinitions);
444        addDefinitions(builder, macroDefinitions);
445        addDefinitions(builder, dateDefinitions);
446        addDefinitions(builder, uuidDefinitions);
447
448        this.fieldDefinitions = builder.build();
449    }
450
451    protected void createFields() throws MojoExecutionException, IOException {
452        ImmutableList.Builder<NumberField> numberFields = ImmutableList.builder();
453
454        var builder = ImmutableMap.<String, String>builder();
455
456        for (FieldDefinition<?> definition : fieldDefinitions.values()) {
457            Field<?, ?> field = definition.createField(this, valueCache);
458
459            if (field instanceof NumberField) {
460                numberFields.add((NumberField) field);
461            }
462
463            var fieldValue = field.getValue();
464            builder.put(field.getFieldName(), fieldValue);
465
466            if (field.isExposeAsProperty()) {
467                project.getProperties().setProperty(field.getFieldName(), fieldValue);
468                LOG.atFine().log("Exporting Property name: %s, value: %s", field.getFieldName(), fieldValue);
469            } else {
470                LOG.atFine().log("Property name: %s, value: %s", field.getFieldName(), fieldValue);
471            }
472        }
473
474        this.numberFields = numberFields.build();
475        this.values = builder.build();
476    }
477
478    // generates the property groups.
479    @SuppressFBWarnings(value = "WMI_WRONG_MAP_ITERATOR")
480    public void createGroups() {
481        var groupMapBuilder = ImmutableMap.<String, PropertyGroup>builder();
482        var resultMapBuilder = ImmutableMap.<String, Set<PropertyResult>>builder();
483
484        Set<String> exportedFields = fieldDefinitions.values().stream()
485            .filter(FieldDefinition::isExport)
486            .map(FieldDefinition::getId).collect(ImmutableSet.toImmutableSet());
487
488        Set<String> propertyNames = new LinkedHashSet<>(exportedFields);
489
490        propertyGroupDefinitions.forEach(propertyGroupDefinition -> {
491            PropertyGroup propertyGroup = propertyGroupDefinition.createGroup(this);
492            Set<PropertyResult> propertyResults = propertyGroup.createProperties(values);
493            groupMapBuilder.put(propertyGroup.getId(), propertyGroup);
494            resultMapBuilder.put(propertyGroup.getId(), propertyResults);
495        });
496
497        var groupMap = groupMapBuilder.build();
498        var resultMap = resultMapBuilder.build();
499
500        var groupsToAdd = this.activeGroups == null
501            ? groupMap.keySet()
502            : this.activeGroups;
503
504        for (String groupToAdd : groupsToAdd) {
505
506            var activeGroup = groupMap.get(groupToAdd);
507            checkState(activeGroup != null, "activated group '%s' does not exist", groupToAdd);
508            var activeResult = resultMap.get(groupToAdd);
509            checkState(activeResult != null, "activated group '%s' has no result", groupToAdd);
510
511            if (activeGroup.checkActive(isSnapshot)) {
512                for (PropertyResult propertyResult : activeResult) {
513                    String propertyName = propertyResult.getPropertyName();
514
515                    if (checkIgnoreWarnFailState(!propertyNames.contains(propertyName), activeGroup.getOnDuplicateProperty(),
516                        () -> format("property '%s' is not exposed", propertyName),
517                        () -> format("property '%s' is already exposed!", propertyName))) {
518
519                        project.getProperties().setProperty(propertyName, propertyResult.getPropertyValue());
520                    }
521                }
522            } else {
523                LOG.atFine().log("Skipping property group %s, not active", activeGroup);
524            }
525        }
526    }
527
528
529}