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}