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