001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.maven.plugin; 007 008import gov.nist.secauto.metaschema.core.model.IConstraintLoader; 009import gov.nist.secauto.metaschema.core.model.MetaschemaException; 010import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet; 011import gov.nist.secauto.metaschema.core.model.xml.ExternalConstraintsModulePostProcessor; 012import gov.nist.secauto.metaschema.core.util.CollectionUtil; 013import gov.nist.secauto.metaschema.core.util.ObjectUtils; 014import gov.nist.secauto.metaschema.databind.IBindingContext; 015import gov.nist.secauto.metaschema.databind.model.metaschema.BindingConstraintLoader; 016import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader; 017 018import org.apache.maven.plugin.AbstractMojo; 019import org.apache.maven.plugin.MojoExecution; 020import org.apache.maven.plugin.MojoExecutionException; 021import org.apache.maven.plugins.annotations.Component; 022import org.apache.maven.plugins.annotations.Parameter; 023import org.apache.maven.project.MavenProject; 024import org.codehaus.plexus.util.DirectoryScanner; 025import org.sonatype.plexus.build.incremental.BuildContext; 026 027import java.io.File; 028import java.io.IOException; 029import java.net.URI; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Objects; 033import java.util.stream.Collectors; 034import java.util.stream.Stream; 035 036import edu.umd.cs.findbugs.annotations.NonNull; 037import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 038 039public abstract class AbstractMetaschemaMojo 040 extends AbstractMojo { 041 private static final String SYSTEM_FILE_ENCODING_PROPERTY = "file.encoding"; 042 private static final String[] DEFAULT_INCLUDES = { "**/*.xml" }; 043 044 /** 045 * The Maven project context. 046 * 047 * @parameter default-value="${project}" 048 * @required 049 * @readonly 050 */ 051 @Parameter(defaultValue = "${project}", required = true, readonly = true) 052 MavenProject mavenProject; 053 054 /** 055 * This will be injected if this plugin is executed as part of the standard 056 * Maven lifecycle. If the mojo is directly invoked, this parameter will not be 057 * injected. 058 */ 059 @Parameter(defaultValue = "${mojoExecution}", readonly = true) 060 private MojoExecution mojoExecution; 061 062 @Component 063 private BuildContext buildContext; 064 065 /** 066 * <p> 067 * The directory where the staleFile is found. The staleFile is used to 068 * determine if re-generation of generated Java classes is needed, by recording 069 * when the last build occurred. 070 * </p> 071 * <p> 072 * This directory is expected to be located within the 073 * <code>${project.build.directory}</code>, to ensure that code (re)generation 074 * occurs after cleaning the project. 075 * </p> 076 */ 077 @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true) 078 protected File staleFileDirectory; 079 080 /** 081 * <p> 082 * Defines the encoding used for generating Java Source files. 083 * </p> 084 * <p> 085 * The algorithm for finding the encoding to use is as follows (where the first 086 * non-null value found is used for encoding): 087 * <ol> 088 * <li>If the configuration property is explicitly given within the plugin's 089 * configuration, use that value.</li> 090 * <li>If the Maven property <code>project.build.sourceEncoding</code> is 091 * defined, use its value.</li> 092 * <li>Otherwise use the value from the system property 093 * <code>file.encoding</code>.</li> 094 * </ol> 095 * </p> 096 * 097 * @see #getEncoding() 098 * @since 2.0 099 */ 100 @Parameter(defaultValue = "${project.build.sourceEncoding}") 101 private String encoding; 102 103 /** 104 * Location to generate Java source files in. 105 */ 106 @Parameter(defaultValue = "${project.build.directory}/generated-sources/metaschema", required = true) 107 private File outputDirectory; 108 109 /** 110 * The directory to read source metaschema from. 111 */ 112 @Parameter(defaultValue = "${basedir}/src/main/metaschema") 113 private File metaschemaDir; 114 115 /** 116 * A list of <code>files</code> containing Metaschema module constraints files. 117 */ 118 @Parameter(property = "constraints") 119 private File[] constraints; 120 121 /** 122 * A set of inclusion patterns used to select which Metaschema modules are to be 123 * processed. By default, all files are processed. 124 */ 125 126 @Parameter 127 protected String[] includes; 128 129 /** 130 * A set of exclusion patterns used to prevent certain files from being 131 * processed. By default, this set is empty such that no files are excluded. 132 */ 133 @Parameter 134 protected String[] excludes; 135 136 /** 137 * Indicate if the execution should be skipped. 138 */ 139 @Parameter(property = "metaschema.skip", defaultValue = "false") 140 private boolean skip; 141 142 /** 143 * The BuildContext is used to identify which files or directories were modified 144 * since last build. This is used to determine if Module-based generation must 145 * be performed again. 146 * 147 * @return the active Plexus BuildContext. 148 */ 149 protected final BuildContext getBuildContext() { 150 return buildContext; 151 } 152 153 /** 154 * Retrieve the Maven project context. 155 * 156 * @return The active MavenProject. 157 */ 158 protected final MavenProject getMavenProject() { 159 return mavenProject; 160 } 161 162 /** 163 * Retrieve the mojo execution context. 164 * 165 * @return The active MojoExecution. 166 */ 167 @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder") 168 public MojoExecution getMojoExecution() { 169 return mojoExecution; 170 } 171 172 /** 173 * Retrieve the directory where generated classes will be stored. 174 * 175 * @return the directory 176 */ 177 protected File getOutputDirectory() { 178 return outputDirectory; 179 } 180 181 /** 182 * Set the directory where generated classes will be stored. 183 * 184 * @param outputDirectory 185 * the directory to use 186 */ 187 protected void setOutputDirectory(File outputDirectory) { 188 Objects.requireNonNull(outputDirectory, "outputDirectory"); 189 this.outputDirectory = outputDirectory; 190 } 191 192 /** 193 * Gets the file encoding to use for generated classes. 194 * <p> 195 * The algorithm for finding the encoding to use is as follows (where the first 196 * non-null value found is used for encoding): 197 * </p> 198 * <ol> 199 * <li>If the configuration property is explicitly given within the plugin's 200 * configuration, use that value.</li> 201 * <li>If the Maven property <code>project.build.sourceEncoding</code> is 202 * defined, use its value.</li> 203 * <li>Otherwise use the value from the system property 204 * <code>file.encoding</code>.</li> 205 * </ol> 206 * 207 * @return The encoding to be used by this AbstractJaxbMojo and its tools. 208 */ 209 protected final String getEncoding() { 210 String encoding; 211 if (this.encoding != null) { 212 // first try to use the provided encoding 213 encoding = this.encoding; 214 if (getLog().isDebugEnabled()) { 215 getLog().debug(String.format("Using configured encoding [%s].", encoding)); 216 } 217 } else { 218 encoding = System.getProperty(SYSTEM_FILE_ENCODING_PROPERTY); 219 if (getLog().isWarnEnabled()) { 220 getLog().warn(String.format("Using system encoding [%s]. This build is platform dependent!", encoding)); 221 } 222 } 223 return encoding; 224 } 225 226 /** 227 * Retrieve a stream of Module file sources. 228 * 229 * @return the stream 230 */ 231 protected Stream<File> getModuleSources() { 232 DirectoryScanner ds = new DirectoryScanner(); 233 ds.setBasedir(metaschemaDir); 234 ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES); 235 ds.setExcludes(excludes != null && excludes.length > 0 ? excludes : null); 236 ds.addDefaultExcludes(); 237 ds.setCaseSensitive(true); 238 ds.setFollowSymlinks(false); 239 ds.scan(); 240 return Stream.of(ds.getIncludedFiles()).map(filename -> new File(metaschemaDir, filename)).distinct(); 241 } 242 243 /** 244 * Get the configured collection of constraints. 245 * 246 * @param bindingContext 247 * the Metaschema binding context to use when loading the constraints 248 * @return the loaded constraints 249 * @throws MetaschemaException 250 * if a binding exception occurred while loading the constraints 251 * @throws IOException 252 * if an error occurred while reading the constraints 253 */ 254 protected List<IConstraintSet> getConstraints(@NonNull IBindingContext bindingContext) 255 throws MetaschemaException, IOException { 256 IConstraintLoader loader = new BindingConstraintLoader(bindingContext); 257 List<IConstraintSet> constraintSets = new ArrayList<>(constraints.length); 258 for (File constraint : this.constraints) { 259 constraintSets.addAll(loader.load(ObjectUtils.notNull(constraint))); 260 } 261 return CollectionUtil.unmodifiableList(constraintSets); 262 } 263 264 /** 265 * Determine if the execution of this mojo should be skipped. 266 * 267 * @return {@code true} if the mojo execution should be skipped, or 268 * {@code false} otherwise 269 */ 270 protected boolean shouldExecutionBeSkipped() { 271 return skip; 272 } 273 274 /** 275 * Get the name of the file that is used to detect staleness. 276 * 277 * @return the name 278 */ 279 protected abstract String getStaleFileName(); 280 281 /** 282 * Gets the staleFile for this execution. 283 * 284 * @return the staleFile 285 */ 286 protected final File getStaleFile() { 287 StringBuilder builder = new StringBuilder(); 288 if (getMojoExecution() != null) { 289 builder.append(getMojoExecution().getExecutionId()).append('-'); 290 } 291 builder.append(getStaleFileName()); 292 return new File(staleFileDirectory, builder.toString()); 293 } 294 295 /** 296 * Determine if code generation is required. This is done by comparing the last 297 * modified time of each Module source file against the stale file managed by 298 * this plugin. 299 * 300 * @return {@code true} if the code generation is needed, or {@code false} 301 * otherwise 302 */ 303 protected boolean isGenerationRequired() { 304 final File staleFile = getStaleFile(); 305 boolean generate = !staleFile.exists(); 306 if (generate) { 307 if (getLog().isInfoEnabled()) { 308 getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath())); 309 } 310 generate = true; 311 } else { 312 generate = false; 313 // check for staleness 314 long staleLastModified = staleFile.lastModified(); 315 316 BuildContext buildContext = getBuildContext(); 317 URI metaschemaDirRelative = getMavenProject().getBasedir().toURI().relativize(metaschemaDir.toURI()); 318 319 if (buildContext.isIncremental() && buildContext.hasDelta(metaschemaDirRelative.toString())) { 320 if (getLog().isInfoEnabled()) { 321 getLog().info("metaschemaDirRelative: " + metaschemaDirRelative.toString()); 322 } 323 generate = true; 324 } 325 326 if (!generate) { 327 for (File sourceFile : getModuleSources().collect(Collectors.toList())) { 328 if (getLog().isInfoEnabled()) { 329 getLog().info("Source file: " + sourceFile.getPath()); 330 } 331 if (sourceFile.lastModified() > staleLastModified) { 332 generate = true; 333 } 334 } 335 } 336 } 337 return generate; 338 } 339 340 /** 341 * Construct a new module loader based on the provided mojo configuration. 342 * 343 * @return the module loader 344 * @throws MojoExecutionException 345 * if an error occurred while loading the configured constraints 346 */ 347 @NonNull 348 protected BindingModuleLoader newModuleLoader() throws MojoExecutionException { 349 IBindingContext bindingContext = IBindingContext.instance(); 350 351 List<IConstraintSet> constraints; 352 try { 353 constraints = getConstraints(bindingContext); 354 } catch (MetaschemaException | IOException ex) { 355 throw new MojoExecutionException("Unable to load external constraints.", ex); 356 } 357 358 // generate Java sources based on provided metaschema sources 359 BindingModuleLoader loader = constraints.isEmpty() 360 ? new BindingModuleLoader(bindingContext) 361 : new BindingModuleLoader( 362 bindingContext, 363 CollectionUtil.singletonList(new ExternalConstraintsModulePostProcessor(constraints))); 364 loader.allowEntityResolution(); 365 return loader; 366 } 367}