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.Set;
41  import javax.inject.Inject;
42  
43  import com.google.common.collect.ImmutableList;
44  import com.google.common.collect.ImmutableMap;
45  import com.google.common.collect.ImmutableSet;
46  import com.google.common.flogger.FluentLogger;
47  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
48  import org.apache.maven.plugin.AbstractMojo;
49  import org.apache.maven.plugin.MojoExecutionException;
50  import org.apache.maven.plugins.annotations.Parameter;
51  import org.apache.maven.project.MavenProject;
52  import org.apache.maven.settings.Settings;
53  
54  public abstract class AbstractPropertyHelperMojo extends AbstractMojo implements FieldContext {
55  
56      private static final FluentLogger LOG = FluentLogger.forEnclosingClass();
57  
58      protected final ValueCache valueCache = new ValueCache();
59      private IgnoreWarnFail onDuplicateField = IgnoreWarnFail.FAIL;
60  
61      /**
62       * Defines the action to take if a field is defined multiple times (e.g. as a number and a string).
63       * <br>
64       * Options are
65       * <ul>
66       *     <li><code>ignore</code> - ignore multiple definitions silently, retain just the first one found</li>
67       *     <li><code>warn</code> - like ignore, but also log a warning message</li>
68       *     <li><code>fail</code> - fail the build with an exception</li>
69       * </ul>
70       */
71      @Parameter(defaultValue = "fail", alias = "onDuplicateProperty")
72      public void setOnDuplicateField(String onDuplicateField) {
73          this.onDuplicateField = IgnoreWarnFail.forString(onDuplicateField);
74      }
75  
76      private List<String> activeGroups = List.of();
77  
78      /**
79       * The property groups to activate. If none are given, all property groups are activated.
80       * <pre>{@code
81       * <activeGroups>
82       *     <activeGroup>group1</activeGroup>
83       *     <activeGroup>group2</activeGroup>
84       *     ...
85       * </activeGroups>
86       * }</pre>
87       */
88      @Parameter
89      public void setActiveGroups(String... activeGroups) {
90          this.activeGroups = Arrays.asList(activeGroups);
91      }
92  
93      /**
94       * Define property groups. A property group contains one or more property definitions. Property groups are active by default
95       * unless they are explicitly listed with {@code <activeGroups>...</activeGroups}.
96       * <pre>{@code
97       * <propertyGroups>
98       *     <propertyGroup>
99       *         <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 }