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.IModule;
010import gov.nist.secauto.metaschema.core.model.IModuleLoader;
011import gov.nist.secauto.metaschema.core.model.IResourceLocation;
012import gov.nist.secauto.metaschema.core.model.MetaschemaException;
013import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationException;
014import gov.nist.secauto.metaschema.core.model.constraint.ConstraintValidationFinding;
015import gov.nist.secauto.metaschema.core.model.constraint.ExternalConstraintsModulePostProcessor;
016import gov.nist.secauto.metaschema.core.model.constraint.IConstraintSet;
017import gov.nist.secauto.metaschema.core.model.validation.AbstractValidationResultProcessor;
018import gov.nist.secauto.metaschema.core.model.validation.IValidationFinding;
019import gov.nist.secauto.metaschema.core.model.validation.IValidationResult;
020import gov.nist.secauto.metaschema.core.model.validation.JsonSchemaContentValidator.JsonValidationFinding;
021import gov.nist.secauto.metaschema.core.model.validation.XmlSchemaContentValidator.XmlValidationFinding;
022import gov.nist.secauto.metaschema.core.util.CollectionUtil;
023import gov.nist.secauto.metaschema.core.util.ObjectUtils;
024import gov.nist.secauto.metaschema.databind.DefaultBindingContext;
025import gov.nist.secauto.metaschema.databind.IBindingContext;
026import gov.nist.secauto.metaschema.databind.PostProcessingModuleLoaderStrategy;
027import gov.nist.secauto.metaschema.databind.SimpleModuleLoaderStrategy;
028import gov.nist.secauto.metaschema.databind.codegen.IGeneratedClass;
029import gov.nist.secauto.metaschema.databind.codegen.IGeneratedModuleClass;
030import gov.nist.secauto.metaschema.databind.codegen.IModuleBindingGenerator;
031import gov.nist.secauto.metaschema.databind.codegen.IProduction;
032import gov.nist.secauto.metaschema.databind.codegen.JavaCompilerSupport;
033import gov.nist.secauto.metaschema.databind.codegen.JavaGenerator;
034import gov.nist.secauto.metaschema.databind.codegen.ModuleCompilerHelper;
035import gov.nist.secauto.metaschema.databind.codegen.config.DefaultBindingConfiguration;
036import gov.nist.secauto.metaschema.databind.codegen.config.IBindingConfiguration;
037import gov.nist.secauto.metaschema.databind.model.IBoundModule;
038import gov.nist.secauto.metaschema.databind.model.metaschema.BindingModuleLoader;
039import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingMetaschemaModule;
040import gov.nist.secauto.metaschema.databind.model.metaschema.IBindingModuleLoader;
041import gov.nist.secauto.metaschema.databind.model.metaschema.binding.MetaschemaModelModule;
042
043import org.apache.maven.artifact.Artifact;
044import org.apache.maven.artifact.DependencyResolutionRequiredException;
045import org.apache.maven.plugin.AbstractMojo;
046import org.apache.maven.plugin.MojoExecution;
047import org.apache.maven.plugin.MojoExecutionException;
048import org.apache.maven.plugin.logging.Log;
049import org.apache.maven.plugins.annotations.Parameter;
050
051import javax.inject.Inject;
052import org.apache.maven.project.MavenProject;
053import org.codehaus.plexus.util.DirectoryScanner;
054import org.sonatype.plexus.build.incremental.BuildContext;
055import org.xml.sax.SAXParseException;
056
057import java.io.File;
058import java.io.IOException;
059import java.io.OutputStream;
060import java.net.URI;
061import java.nio.charset.Charset;
062import java.nio.file.Files;
063import java.nio.file.Path;
064import java.nio.file.Paths;
065import java.nio.file.StandardOpenOption;
066import java.util.ArrayList;
067import java.util.Collection;
068import java.util.HashSet;
069import java.util.LinkedHashSet;
070import java.util.List;
071import java.util.Objects;
072import java.util.Set;
073import java.util.function.Function;
074import java.util.stream.Collectors;
075import java.util.stream.Stream;
076
077import javax.tools.DiagnosticCollector;
078
079import edu.umd.cs.findbugs.annotations.NonNull;
080import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
081
082public abstract class AbstractMetaschemaMojo
083    extends AbstractMojo {
084  private static final String[] DEFAULT_INCLUDES = { "**/*.xml" };
085
086  /**
087   * The Maven project context.
088   *
089   * @required
090   * @readonly
091   */
092  @Parameter(defaultValue = "${project}", required = true, readonly = true)
093  MavenProject mavenProject;
094
095  /**
096   * This will be injected if this plugin is executed as part of the standard
097   * Maven lifecycle. If the mojo is directly invoked, this parameter will not be
098   * injected.
099   */
100  @Parameter(defaultValue = "${mojoExecution}", readonly = true)
101  private MojoExecution mojoExecution;
102
103  @Inject
104  private BuildContext buildContext;
105
106  @Parameter(defaultValue = "${plugin.artifacts}", readonly = true, required = true)
107  private List<Artifact> pluginArtifacts;
108
109  /**
110   * <p>
111   * The directory where the staleFile is found. The staleFile is used to
112   * determine if re-generation of generated Java classes is needed, by recording
113   * when the last build occurred.
114   * </p>
115   * <p>
116   * This directory is expected to be located within the
117   * <code>${project.build.directory}</code>, to ensure that code (re)generation
118   * occurs after cleaning the project.
119   * </p>
120   */
121  @Parameter(defaultValue = "${project.build.directory}/metaschema", readonly = true, required = true)
122  protected File staleFileDirectory;
123
124  /**
125   * <p>
126   * Defines the encoding used for generating Java Source files.
127   * </p>
128   * <p>
129   * The algorithm for finding the encoding to use is as follows (where the first
130   * non-null value found is used for encoding):
131   * <ol>
132   * <li>If the configuration property is explicitly given within the plugin's
133   * configuration, use that value.
134   * <li>If the Maven property <code>project.build.sourceEncoding</code> is
135   * defined, use its value.
136   * <li>Otherwise use the value from the system property
137   * <code>file.encoding</code>.
138   * </ol>
139   * </p>
140   *
141   * @see #getEncoding()
142   * @since 2.0
143   */
144  @Parameter(defaultValue = "${project.build.sourceEncoding}")
145  private String encoding;
146
147  /**
148   * Location to generate Java source files in.
149   */
150  @Parameter(
151      defaultValue = "${project.build.directory}/generated-sources/metaschema",
152      required = true,
153      property = "outputDirectory")
154  private File outputDirectory;
155
156  /**
157   * The directory to read source metaschema from.
158   */
159  @Parameter(defaultValue = "${basedir}/src/main/metaschema")
160  private File metaschemaDir;
161
162  /**
163   * A list of <code>files</code> containing Metaschema module constraints files.
164   */
165  @Parameter(property = "constraints")
166  private File[] constraints;
167
168  /**
169   * A set of inclusion patterns used to select which Metaschema modules are to be
170   * processed. By default, all files are processed.
171   */
172  @Parameter
173  protected String[] includes;
174
175  /**
176   * A set of exclusion patterns used to prevent certain files from being
177   * processed. By default, this set is empty such that no files are excluded.
178   */
179  @Parameter
180  protected String[] excludes;
181
182  /**
183   * Indicate if the execution should be skipped.
184   */
185  @Parameter(property = "metaschema.skip", defaultValue = "false")
186  private boolean skip;
187
188  /**
189   * The BuildContext is used to identify which files or directories were modified
190   * since last build. This is used to determine if Module-based generation must
191   * be performed again.
192   *
193   * @return the active Plexus BuildContext.
194   */
195  protected final BuildContext getBuildContext() {
196    return buildContext;
197  }
198
199  /**
200   * Retrieve the Maven project context.
201   *
202   * @return The active MavenProject.
203   */
204  protected final MavenProject getMavenProject() {
205    return mavenProject;
206  }
207
208  protected final List<Artifact> getPluginArtifacts() {
209    return pluginArtifacts;
210  }
211
212  /**
213   * Retrieve the mojo execution context.
214   *
215   * @return The active MojoExecution.
216   */
217  @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
218  public MojoExecution getMojoExecution() {
219    return mojoExecution;
220  }
221
222  /**
223   * Retrieve the directory where generated classes will be stored.
224   *
225   * @return the directory
226   */
227  protected File getOutputDirectory() {
228    return outputDirectory;
229  }
230
231  /**
232   * Set the directory where generated classes will be stored.
233   *
234   * @param outputDirectory
235   *          the directory to use
236   */
237  protected void setOutputDirectory(File outputDirectory) {
238    Objects.requireNonNull(outputDirectory, "outputDirectory");
239    this.outputDirectory = outputDirectory;
240  }
241
242  /**
243   * Gets the file encoding to use for generated classes.
244   * <p>
245   * The algorithm for finding the encoding to use is as follows (where the first
246   * non-null value found is used for encoding):
247   * </p>
248   * <ol>
249   * <li>If the configuration property is explicitly given within the plugin's
250   * configuration, use that value.
251   * <li>If the Maven property <code>project.build.sourceEncoding</code> is
252   * defined, use its value.
253   * <li>Otherwise use the value from the system property
254   * <code>file.encoding</code>.
255   * </ol>
256   *
257   * @return The encoding to be used by this AbstractJaxbMojo and its tools.
258   */
259  protected final String getEncoding() {
260    String encoding;
261    if (this.encoding != null) {
262      // first try to use the provided encoding
263      encoding = this.encoding;
264      if (getLog().isDebugEnabled()) {
265        getLog().debug(String.format("Using configured encoding [%s].", encoding));
266      }
267    } else {
268      encoding = Charset.defaultCharset().displayName();
269      if (getLog().isWarnEnabled()) {
270        getLog().warn(String.format("Using system encoding [%s]. This build is platform dependent!", encoding));
271      }
272    }
273    return encoding;
274  }
275
276  /**
277   * Retrieve a stream of Module file sources.
278   *
279   * @return the stream
280   */
281  protected Stream<File> getModuleSources() {
282    DirectoryScanner ds = new DirectoryScanner();
283    ds.setBasedir(metaschemaDir);
284    ds.setIncludes(includes != null && includes.length > 0 ? includes : DEFAULT_INCLUDES);
285    ds.setExcludes(excludes != null && excludes.length > 0 ? excludes : null);
286    ds.addDefaultExcludes();
287    ds.setCaseSensitive(true);
288    ds.setFollowSymlinks(false);
289    ds.scan();
290    return Stream.of(ds.getIncludedFiles()).map(filename -> new File(metaschemaDir, filename)).distinct();
291  }
292
293  @NonNull
294  protected IBindingContext newBindingContext(
295      @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws IOException, MetaschemaException {
296    // generate Java sources based on provided metaschema sources
297    return new DefaultBindingContext(
298        new PostProcessingModuleLoaderStrategy(
299            // ensure that the external constraints do not apply to the built in module
300            CollectionUtil.singletonList(modulePostProcessor),
301            new SimpleModuleLoaderStrategy(
302                // this is used instead of the default generator to ensure that plugin classpath
303                // entries are used for compilation
304                new ModuleBindingGenerator(
305                    ObjectUtils.notNull(Files.createDirectories(Paths.get("target/metaschema-codegen-modules"))),
306                    new DefaultBindingConfiguration()))));
307  }
308
309  /**
310   * Get the configured collection of constraints.
311   *
312   * @return the loaded constraints
313   * @throws MojoExecutionException
314   *           if an error occurred while loading the constraints
315   */
316  @NonNull
317  protected List<IConstraintSet> getConstraints() throws MojoExecutionException {
318    IConstraintLoader loader = IBindingContext.getConstraintLoader();
319    List<IConstraintSet> constraintSets = new ArrayList<>(constraints.length);
320    for (File constraint : this.constraints) {
321      try {
322        constraintSets.addAll(loader.load(ObjectUtils.notNull(constraint)));
323      } catch (IOException | MetaschemaException ex) {
324        throw new MojoExecutionException("Loading of external constraints failed", ex);
325      }
326    }
327    return CollectionUtil.unmodifiableList(constraintSets);
328  }
329
330  /**
331   * Determine if the execution of this mojo should be skipped.
332   *
333   * @return {@code true} if the mojo execution should be skipped, or
334   *         {@code false} otherwise
335   */
336  protected boolean shouldExecutionBeSkipped() {
337    return skip;
338  }
339
340  /**
341   * Get the name of the file that is used to detect staleness.
342   *
343   * @return the name
344   */
345  protected abstract String getStaleFileName();
346
347  /**
348   * Gets the staleFile for this execution.
349   *
350   * @return the staleFile
351   */
352  protected final File getStaleFile() {
353    StringBuilder builder = new StringBuilder();
354    if (getMojoExecution() != null) {
355      builder.append(getMojoExecution().getExecutionId()).append('-');
356    }
357    builder.append(getStaleFileName());
358    return new File(staleFileDirectory, builder.toString());
359  }
360
361  /**
362   * Determine if code generation is required. This is done by comparing the last
363   * modified time of each Module source file against the stale file managed by
364   * this plugin.
365   *
366   * @return {@code true} if the code generation is needed, or {@code false}
367   *         otherwise
368   */
369  protected boolean isGenerationRequired() {
370    final File staleFile = getStaleFile();
371    boolean generate = !staleFile.exists();
372    if (generate) {
373      if (getLog().isInfoEnabled()) {
374        getLog().info(String.format("Stale file '%s' doesn't exist! Generating source files.", staleFile.getPath()));
375      }
376      generate = true;
377    } else {
378      generate = false;
379      // check for staleness
380      long staleLastModified = staleFile.lastModified();
381
382      BuildContext buildContext = getBuildContext();
383      URI metaschemaDirRelative = getMavenProject().getBasedir().toURI().relativize(metaschemaDir.toURI());
384
385      if (buildContext.isIncremental() && buildContext.hasDelta(metaschemaDirRelative.toString())) {
386        if (getLog().isInfoEnabled()) {
387          getLog().info("metaschemaDirRelative: " + metaschemaDirRelative.toString());
388        }
389        generate = true;
390      }
391
392      if (!generate) {
393        for (File sourceFile : getModuleSources().collect(Collectors.toList())) {
394          if (getLog().isInfoEnabled()) {
395            getLog().info("Source file: " + sourceFile.getPath());
396          }
397          if (sourceFile.lastModified() > staleLastModified) {
398            generate = true;
399          }
400        }
401      }
402    }
403    return generate;
404  }
405
406  protected Set<String> getClassPath() throws DependencyResolutionRequiredException {
407    Set<String> pathElements;
408    try {
409      pathElements = new LinkedHashSet<>(getMavenProject().getCompileClasspathElements());
410    } catch (DependencyResolutionRequiredException ex) {
411      getLog().warn("exception calling getCompileClasspathElements", ex);
412      throw ex;
413    }
414
415    if (pluginArtifacts != null) {
416      for (Artifact a : getPluginArtifacts()) {
417        if (a.getFile() != null) {
418          pathElements.add(a.getFile().getAbsolutePath());
419        }
420      }
421    }
422    return pathElements;
423  }
424
425  @NonNull
426  protected Set<IModule> getModulesToGenerateFor(
427      @NonNull IBindingContext bindingContext,
428      @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor)
429      throws MetaschemaException, IOException, ConstraintValidationException {
430
431    // Don't use the normal loader, since it attempts to register and compile the
432    // module.
433    // We only care about the module content for generating sources and schemas
434    IBindingModuleLoader loader = new BindingModuleLoader(bindingContext, (module, ctx) -> {
435      modulePostProcessor.processModule(module);
436    });
437    loader.allowEntityResolution();
438
439    LoggingValidationHandler validationHandler = new LoggingValidationHandler();
440
441    Set<IModule> modules = new HashSet<>();
442    for (File source : getModuleSources().collect(Collectors.toList())) {
443      assert source != null;
444      if (getLog().isInfoEnabled()) {
445        getLog().info("Using metaschema source: " + source.getPath());
446      }
447      IBindingMetaschemaModule module = loader.load(source);
448
449      IValidationResult result = bindingContext.validate(
450          module.getSourceNodeItem(),
451          loader.getBindingContext().newBoundLoader(),
452          null);
453
454      validationHandler.handleResults(result);
455
456      modules.add(module);
457    }
458    return modules;
459  }
460
461  protected void createStaleFile(@NonNull File staleFile) throws MojoExecutionException {
462    // create the stale file
463    if (!staleFileDirectory.exists() && !staleFileDirectory.mkdirs()) {
464      throw new MojoExecutionException("Unable to create output directory: " + staleFileDirectory);
465    }
466    try (OutputStream os
467        = Files.newOutputStream(staleFile.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE,
468            StandardOpenOption.TRUNCATE_EXISTING)) {
469      os.close();
470      if (getLog().isInfoEnabled()) {
471        getLog().info("Created stale file: " + staleFile);
472      }
473    } catch (IOException ex) {
474      throw new MojoExecutionException("Failed to write stale file: " + staleFile.getPath(), ex);
475    }
476  }
477
478  @SuppressWarnings("PMD.AvoidCatchingGenericException")
479  @Override
480  public void execute() throws MojoExecutionException {
481    File staleFile = getStaleFile();
482    try {
483      staleFile = ObjectUtils.notNull(staleFile.getCanonicalFile());
484    } catch (IOException ex) {
485      if (getLog().isWarnEnabled()) {
486        getLog().warn("Unable to resolve canonical path to stale file. Treating it as not existing.", ex);
487      }
488    }
489
490    boolean generate;
491    if (shouldExecutionBeSkipped()) {
492      if (getLog().isDebugEnabled()) {
493        getLog().debug(String.format("Generation is configured to be skipped. Skipping."));
494      }
495      generate = false;
496    } else if (staleFile.exists()) {
497      generate = isGenerationRequired();
498    } else {
499      if (getLog().isInfoEnabled()) {
500        getLog().info(String.format("Stale file '%s' doesn't exist! Generation is required.", staleFile.getPath()));
501      }
502      generate = true;
503    }
504
505    if (generate) {
506      List<IConstraintSet> constraints = getConstraints();
507      IModuleLoader.IModulePostProcessor modulePostProcessor
508          = new LimitedExternalConstraintsModulePostProcessor(constraints);
509
510      List<File> generatedFiles;
511      try {
512        generatedFiles = performGeneration(modulePostProcessor);
513      } finally {
514        // ensure the stale file is created to ensure that regeneration is only
515        // performed when a
516        // change is made
517        createStaleFile(staleFile);
518      }
519
520      if (getLog().isInfoEnabled()) {
521        getLog().info(String.format("Generated %d files.", generatedFiles.size()));
522      }
523
524      // for m2e
525      for (File file : generatedFiles) {
526        getBuildContext().refresh(file);
527      }
528    }
529  }
530
531  @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.ExceptionAsFlowControl" })
532  @NonNull
533  private List<File> performGeneration(
534      @NonNull IModuleLoader.IModulePostProcessor modulePostProcessor) throws MojoExecutionException {
535    File outputDir = getOutputDirectory();
536    if (getLog().isDebugEnabled()) {
537      getLog().debug(String.format("Using outputDirectory: %s", outputDir.getPath()));
538    }
539
540    if (!outputDir.exists() && !outputDir.mkdirs()) {
541      throw new MojoExecutionException("Unable to create output directory: " + outputDir);
542    }
543
544    IBindingContext bindingContext;
545    try {
546      bindingContext = newBindingContext(modulePostProcessor);
547    } catch (MetaschemaException | IOException ex) {
548      throw new MojoExecutionException("Failed to create the binding context", ex);
549    }
550
551    // generate Java sources based on provided metaschema sources
552    Set<IModule> modules;
553    try {
554      modules = getModulesToGenerateFor(bindingContext, modulePostProcessor);
555    } catch (Exception ex) {
556      throw new MojoExecutionException("Loading of metaschema modules failed", ex);
557    }
558
559    return generate(modules);
560  }
561
562  /**
563   * Perform the generation operation.
564   *
565   * @param modules
566   *          the modules to generate resources/sources for
567   *
568   * @return the files generated during the operation
569   * @throws MojoExecutionException
570   *           if an error occurred while performing the generation operation
571   */
572  @NonNull
573  protected abstract List<File> generate(@NonNull Set<IModule> modules) throws MojoExecutionException;
574
575  protected final class LoggingValidationHandler
576      extends AbstractValidationResultProcessor {
577
578    private <T extends IValidationFinding> void handleFinding(
579        @NonNull T finding,
580        @NonNull Function<T, CharSequence> formatter) {
581
582      Log log = getLog();
583
584      switch (finding.getSeverity()) {
585      case CRITICAL:
586      case ERROR:
587        if (log.isErrorEnabled()) {
588          log.error(formatter.apply(finding), finding.getCause());
589        }
590        break;
591      case WARNING:
592        if (log.isWarnEnabled()) {
593          getLog().warn(formatter.apply(finding), finding.getCause());
594        }
595        break;
596      case INFORMATIONAL:
597        if (log.isInfoEnabled()) {
598          getLog().info(formatter.apply(finding), finding.getCause());
599        }
600        break;
601      default:
602        if (log.isDebugEnabled()) {
603          getLog().debug(formatter.apply(finding), finding.getCause());
604        }
605        break;
606      }
607    }
608
609    @Override
610    protected void handleJsonValidationFinding(JsonValidationFinding finding) {
611      handleFinding(finding, this::getMessage);
612    }
613
614    @Override
615    protected void handleXmlValidationFinding(XmlValidationFinding finding) {
616      handleFinding(finding, this::getMessage);
617    }
618
619    @Override
620    protected void handleConstraintValidationFinding(ConstraintValidationFinding finding) {
621      handleFinding(finding, this::getMessage);
622    }
623
624    @NonNull
625    private CharSequence getMessage(JsonValidationFinding finding) {
626      StringBuilder builder = new StringBuilder();
627      builder.append('[')
628          .append(finding.getCause().getPointerToViolation())
629          .append("] ")
630          .append(finding.getMessage());
631
632      URI documentUri = finding.getDocumentUri();
633      if (documentUri != null) {
634        builder.append(" [")
635            .append(documentUri.toString())
636            .append(']');
637      }
638      return builder;
639    }
640
641    @NonNull
642    private CharSequence getMessage(XmlValidationFinding finding) {
643      StringBuilder builder = new StringBuilder();
644
645      builder.append(finding.getMessage())
646          .append(" [");
647
648      URI documentUri = finding.getDocumentUri();
649      if (documentUri != null) {
650        builder.append(documentUri.toString());
651      }
652
653      SAXParseException ex = finding.getCause();
654      builder.append(finding.getMessage())
655          .append('{')
656          .append(ex.getLineNumber())
657          .append(',')
658          .append(ex.getColumnNumber())
659          .append("}]");
660      return builder;
661    }
662
663    @NonNull
664    private CharSequence getMessage(@NonNull ConstraintValidationFinding finding) {
665      StringBuilder builder = new StringBuilder();
666      builder.append('[')
667          .append(finding.getTarget().getMetapath())
668          .append(']');
669
670      String id = finding.getIdentifier();
671      if (id != null) {
672        builder.append(' ')
673            .append(id);
674      }
675
676      builder.append(' ')
677          .append(finding.getMessage());
678
679      URI documentUri = finding.getTarget().getBaseUri();
680      IResourceLocation location = finding.getLocation();
681      if (documentUri != null || location != null) {
682        builder.append(" [");
683      }
684
685      if (documentUri != null) {
686        builder.append(documentUri.toString());
687      }
688
689      if (location != null) {
690        builder.append('{')
691            .append(location.getLine())
692            .append(',')
693            .append(location.getColumn())
694            .append('}');
695      }
696      if (documentUri != null || location != null) {
697        builder.append(']');
698      }
699      return builder;
700    }
701  }
702
703  public class ModuleBindingGenerator implements IModuleBindingGenerator {
704    @NonNull
705    private final Path compilePath;
706    @NonNull
707    private final ClassLoader classLoader;
708    @NonNull
709    private final IBindingConfiguration bindingConfiguration;
710
711    public ModuleBindingGenerator(
712        @NonNull Path compilePath,
713        @NonNull IBindingConfiguration bindingConfiguration) {
714      this.compilePath = compilePath;
715      this.classLoader = ModuleCompilerHelper.newClassLoader(
716          compilePath,
717          ObjectUtils.notNull(Thread.currentThread().getContextClassLoader()));
718      this.bindingConfiguration = bindingConfiguration;
719    }
720
721    @NonNull
722    public IProduction generateClasses(@NonNull IModule module) throws MetaschemaException {
723      IProduction production;
724      try {
725        production = JavaGenerator.generate(module, compilePath, bindingConfiguration);
726      } catch (IOException ex) {
727        throw new MetaschemaException(
728            String.format("Unable to generate and compile classes for module '%s'.", module.getLocation()),
729            ex);
730      }
731      return production;
732    }
733
734    private void compileClasses(@NonNull IProduction production, @NonNull Path classDir)
735        throws IOException, DependencyResolutionRequiredException {
736      List<IGeneratedClass> classesToCompile = production.getGeneratedClasses().collect(Collectors.toList());
737
738      List<Path> classes = ObjectUtils.notNull(classesToCompile.stream()
739          .map(IGeneratedClass::getClassFile)
740          .collect(Collectors.toUnmodifiableList()));
741
742      JavaCompilerSupport compiler = new JavaCompilerSupport(classDir);
743      compiler.setLogger(new JavaCompilerSupport.Logger() {
744
745        @Override
746        public boolean isDebugEnabled() {
747          return getLog().isDebugEnabled();
748        }
749
750        @Override
751        public boolean isInfoEnabled() {
752          return getLog().isInfoEnabled();
753        }
754
755        @Override
756        public void debug(String msg) {
757          getLog().debug(msg);
758        }
759
760        @Override
761        public void info(String msg) {
762          getLog().info(msg);
763        }
764      });
765
766      getClassPath().forEach(compiler::addToClassPath);
767
768      JavaCompilerSupport.CompilationResult result = compiler.compile(classes);
769
770      if (!result.isSuccessful()) {
771        DiagnosticCollector<?> diagnostics = new DiagnosticCollector<>();
772        if (getLog().isErrorEnabled()) {
773          getLog().error("diagnostics: " + diagnostics.getDiagnostics().toString());
774        }
775        throw new IllegalStateException(String.format("failed to compile classes: %s",
776            classesToCompile.stream()
777                .map(clazz -> clazz.getClassName().canonicalName())
778                .collect(Collectors.joining(","))));
779      }
780    }
781
782    @Override
783    public Class<? extends IBoundModule> generate(IModule module) throws MetaschemaException {
784      IProduction production = generateClasses(module);
785      try {
786        compileClasses(production, compilePath);
787      } catch (IOException | DependencyResolutionRequiredException ex) {
788        throw new IllegalStateException("failed to compile classes", ex);
789      }
790      IGeneratedModuleClass moduleClass = ObjectUtils.requireNonNull(production.getModuleProduction(module));
791
792      try {
793        return moduleClass.load(classLoader);
794      } catch (ClassNotFoundException ex) {
795        throw new IllegalStateException(ex);
796      }
797    }
798  }
799
800  private static class LimitedExternalConstraintsModulePostProcessor
801      extends ExternalConstraintsModulePostProcessor {
802
803    public LimitedExternalConstraintsModulePostProcessor(
804        @NonNull Collection<IConstraintSet> additionalConstraintSets) {
805      super(additionalConstraintSets);
806    }
807
808    /**
809     * This method ensures that constraints are not applied to the built-in
810     * Metaschema module module twice, when this module is selected as the source
811     * for generation.
812     */
813    @Override
814    public void processModule(IModule module) {
815      if (!(module instanceof MetaschemaModelModule)) {
816        super.processModule(module);
817      }
818    }
819  }
820}