View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package org.basepom.mojo.propertyhelper;
16  
17  import static com.google.common.base.Preconditions.checkState;
18  import static java.lang.String.format;
19  import static org.basepom.mojo.propertyhelper.IgnoreWarnFail.checkIgnoreWarnFailState;
20  
21  import org.basepom.mojo.propertyhelper.definitions.DateDefinition;
22  import org.basepom.mojo.propertyhelper.definitions.FieldDefinition;
23  import org.basepom.mojo.propertyhelper.definitions.MacroDefinition;
24  import org.basepom.mojo.propertyhelper.definitions.NumberDefinition;
25  import org.basepom.mojo.propertyhelper.definitions.PropertyGroupDefinition;
26  import org.basepom.mojo.propertyhelper.definitions.StringDefinition;
27  import org.basepom.mojo.propertyhelper.definitions.UuidDefinition;
28  import org.basepom.mojo.propertyhelper.fields.NumberField;
29  import org.basepom.mojo.propertyhelper.groups.PropertyGroup;
30  import org.basepom.mojo.propertyhelper.groups.PropertyResult;
31  import org.basepom.mojo.propertyhelper.macros.MacroType;
32  
33  import java.io.File;
34  import java.io.IOException;
35  import java.util.Arrays;
36  import java.util.LinkedHashSet;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Properties;
40  import java.util.Random;
41  import java.util.Set;
42  import javax.inject.Inject;
43  
44  import com.google.common.collect.ImmutableList;
45  import com.google.common.collect.ImmutableMap;
46  import com.google.common.collect.ImmutableSet;
47  import com.google.common.flogger.FluentLogger;
48  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49  import org.apache.maven.plugin.AbstractMojo;
50  import org.apache.maven.plugin.MojoExecutionException;
51  import org.apache.maven.plugins.annotations.Parameter;
52  import org.apache.maven.project.MavenProject;
53  import org.apache.maven.settings.Settings;
54  
55  public abstract class AbstractPropertyHelperMojo extends AbstractMojo implements FieldContext {
56  
57      private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
58  
59      protected final ValueCache valueCache = new ValueCache();
60      private IgnoreWarnFail onDuplicateField = IgnoreWarnFail.FAIL;
61  
62      /**
63       * Defines the action to take if a field is defined multiple times (e.g. as a number and a string).
64       * <br>
65       * Options are
66       * <ul>
67       *     <li><code>ignore</code> - ignore multiple definitions silently, retain just the first one found</li>
68       *     <li><code>warn</code> - like ignore, but also log a warning message</li>
69       *     <li><code>fail</code> - fail the build with an exception</li>
70       * </ul>
71       */
72      @Parameter(defaultValue = "fail", alias = "onDuplicateProperty")
73      public void setOnDuplicateField(String onDuplicateField) {
74          this.onDuplicateField = IgnoreWarnFail.forString(onDuplicateField);
75      }
76  
77      private List<String> activeGroups = List.of();
78  
79      /**
80       * The property groups to activate. If none are given, all property groups are activated.
81       * <pre>{@code
82       * <activeGroups>
83       *     <activeGroup>group1</activeGroup>
84       *     <activeGroup>group2</activeGroup>
85       *     ...
86       * </activeGroups>
87       * }</pre>
88       */
89      @Parameter
90      public void setActiveGroups(String... activeGroups) {
91          this.activeGroups = Arrays.asList(activeGroups);
92      }
93  
94      /**
95       * Define property groups. A property group contains one or more property definitions. Property groups are active by default unless they are explicitly
96       * listed with {@code <activeGroups>...</activeGroups}.
97       * <pre>{@code
98       * <propertyGroups>
99       *     <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 }