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