001/*
002 * Copyright (C) 2022 - 2023, the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.github.ascopes.jct.compilers;
017
018import static io.github.ascopes.jct.utils.IterableUtils.requireNonNullValues;
019import static java.util.Objects.requireNonNull;
020
021import io.github.ascopes.jct.compilers.impl.JctCompilationFactoryImpl;
022import io.github.ascopes.jct.compilers.impl.JctCompilationImpl;
023import io.github.ascopes.jct.ex.JctCompilerException;
024import io.github.ascopes.jct.filemanagers.AnnotationProcessorDiscovery;
025import io.github.ascopes.jct.filemanagers.JctFileManagerFactory;
026import io.github.ascopes.jct.filemanagers.LoggingMode;
027import io.github.ascopes.jct.workspaces.Workspace;
028import java.io.IOException;
029import java.nio.charset.Charset;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.List;
033import java.util.Locale;
034import javax.annotation.processing.Processor;
035import org.apiguardian.api.API;
036import org.apiguardian.api.API.Status;
037import org.jspecify.annotations.Nullable;
038
039/**
040 * Common functionality for a compiler that can be overridden and that produces a
041 * {@link JctCompilationImpl} as the compilation result.
042 *
043 * <p>Implementations should extend this class and override anything they require.
044 * In most cases, you should not need to override anything other than the constructor.
045 *
046 * <p>This class is <strong>not</strong> thread-safe.
047 *
048 * <p>If you wish to create a common set of configuration settings for instances of
049 * this class, you should consider writing a custom {@link JctCompilerConfigurer} object to apply
050 * the desired operations, and then apply it to instances of this class using
051 * {@link #configure(JctCompilerConfigurer)}.
052 *
053 * @param <A> the type of the class extending this class.
054 * @author Ashley Scopes
055 * @since 0.0.1
056 */
057@API(since = "0.0.1", status = Status.STABLE)
058public abstract class AbstractJctCompiler<A extends AbstractJctCompiler<A>>
059    implements JctCompiler<A, JctCompilation> {
060
061  private final List<Processor> annotationProcessors;
062  private final List<String> annotationProcessorOptions;
063  private final List<String> compilerOptions;
064  private String name;
065  private boolean showWarnings;
066  private boolean showDeprecationWarnings;
067  private boolean failOnWarnings;
068  private CompilationMode compilationMode;
069  private Locale locale;
070  private Charset logCharset;
071  private boolean verbose;
072  private boolean previewFeatures;
073  private @Nullable String release;
074  private @Nullable String source;
075  private @Nullable String target;
076  private LoggingMode diagnosticLoggingMode;
077  private boolean fixJvmModulePathMismatch;
078  private boolean inheritClassPath;
079  private boolean inheritModulePath;
080  private boolean inheritPlatformClassPath;
081  private boolean inheritSystemModulePath;
082  private LoggingMode fileManagerLoggingMode;
083  private AnnotationProcessorDiscovery annotationProcessorDiscovery;
084
085  /**
086   * Initialize this compiler.
087   *
088   * @param defaultName the printable default name to use for the compiler.
089   */
090  protected AbstractJctCompiler(String defaultName) {
091    name = requireNonNull(defaultName, "name");
092    annotationProcessors = new ArrayList<>();
093    annotationProcessorOptions = new ArrayList<>();
094    compilerOptions = new ArrayList<>();
095    showWarnings = JctCompiler.DEFAULT_SHOW_WARNINGS;
096    showDeprecationWarnings = JctCompiler.DEFAULT_SHOW_DEPRECATION_WARNINGS;
097    failOnWarnings = JctCompiler.DEFAULT_FAIL_ON_WARNINGS;
098    compilationMode = JctCompiler.DEFAULT_COMPILATION_MODE;
099    locale = JctCompiler.DEFAULT_LOCALE;
100    logCharset = JctCompiler.DEFAULT_LOG_CHARSET;
101    previewFeatures = JctCompiler.DEFAULT_PREVIEW_FEATURES;
102    release = null;
103    source = null;
104    target = null;
105    verbose = JctCompiler.DEFAULT_VERBOSE;
106    diagnosticLoggingMode = JctCompiler.DEFAULT_DIAGNOSTIC_LOGGING_MODE;
107    fixJvmModulePathMismatch = JctCompiler.DEFAULT_FIX_JVM_MODULE_PATH_MISMATCH;
108    inheritClassPath = JctCompiler.DEFAULT_INHERIT_CLASS_PATH;
109    inheritModulePath = JctCompiler.DEFAULT_INHERIT_MODULE_PATH;
110    inheritPlatformClassPath = JctCompiler.DEFAULT_INHERIT_PLATFORM_CLASS_PATH;
111    inheritSystemModulePath = JctCompiler.DEFAULT_INHERIT_SYSTEM_MODULE_PATH;
112    fileManagerLoggingMode = JctCompiler.DEFAULT_FILE_MANAGER_LOGGING_MODE;
113    annotationProcessorDiscovery = JctCompiler.DEFAULT_ANNOTATION_PROCESSOR_DISCOVERY;
114  }
115
116  @Override
117  public JctCompilation compile(Workspace workspace) {
118    return compileInternal(workspace, null);
119  }
120
121  @Override
122  public JctCompilation compile(Workspace workspace, Collection<String> classNames) {
123    return compileInternal(workspace, classNames);
124  }
125
126  @Override
127  public final <E extends Exception> A configure(JctCompilerConfigurer<E> configurer) throws E {
128    requireNonNull(configurer, "configurer");
129    configurer.configure(this);
130    return myself();
131  }
132
133  @Override
134  public String getName() {
135    return name;
136  }
137
138  @Override
139  public A name(String name) {
140    this.name = requireNonNull(name, "name");
141    return myself();
142  }
143
144  @Override
145  public boolean isVerbose() {
146    return verbose;
147  }
148
149  @Override
150  public A verbose(boolean enabled) {
151    verbose = enabled;
152    return myself();
153  }
154
155  @Override
156  public boolean isPreviewFeatures() {
157    return previewFeatures;
158  }
159
160  @Override
161  public A previewFeatures(boolean enabled) {
162    previewFeatures = enabled;
163    return myself();
164  }
165
166  @Override
167  public boolean isShowWarnings() {
168    return showWarnings;
169  }
170
171  @Override
172  public A showWarnings(boolean enabled) {
173    showWarnings = enabled;
174    return myself();
175  }
176
177  @Override
178  public boolean isShowDeprecationWarnings() {
179    return showDeprecationWarnings;
180  }
181
182  @Override
183  public A showDeprecationWarnings(boolean enabled) {
184    showDeprecationWarnings = enabled;
185    return myself();
186  }
187
188  @Override
189  public boolean isFailOnWarnings() {
190    return failOnWarnings;
191  }
192
193  @Override
194  public A failOnWarnings(boolean enabled) {
195    failOnWarnings = enabled;
196    return myself();
197  }
198
199  @Override
200  public CompilationMode getCompilationMode() {
201    return compilationMode;
202  }
203
204  @Override
205  public A compilationMode(CompilationMode compilationMode) {
206    this.compilationMode = compilationMode;
207    return myself();
208  }
209
210  @Override
211  public List<String> getAnnotationProcessorOptions() {
212    return List.copyOf(annotationProcessorOptions);
213  }
214
215  @Override
216  public A addAnnotationProcessorOptions(Iterable<String> annotationProcessorOptions) {
217    requireNonNullValues(annotationProcessorOptions, "annotationProcessorOptions");
218    annotationProcessorOptions.forEach(this.annotationProcessorOptions::add);
219    return myself();
220  }
221
222  @Override
223  public List<Processor> getAnnotationProcessors() {
224    return List.copyOf(annotationProcessors);
225  }
226
227  @Override
228  public A addAnnotationProcessors(Iterable<? extends Processor> annotationProcessors) {
229    requireNonNullValues(annotationProcessors, "annotationProcessors");
230    annotationProcessors.forEach(this.annotationProcessors::add);
231
232    return myself();
233  }
234
235  @Override
236  public List<String> getCompilerOptions() {
237    return List.copyOf(compilerOptions);
238  }
239
240  @Override
241  public A addCompilerOptions(Iterable<String> compilerOptions) {
242    requireNonNullValues(compilerOptions, "compilerOptions");
243    compilerOptions.forEach(this.compilerOptions::add);
244    return myself();
245  }
246
247  @Override
248  public String getEffectiveRelease() {
249    if (release != null) {
250      return release;
251    }
252
253    if (target != null) {
254      return target;
255    }
256
257    return getDefaultRelease();
258  }
259
260  @Override
261  public String getRelease() {
262    return release;
263  }
264
265  @Override
266  public A release(@Nullable String release) {
267    this.release = release;
268
269    if (release != null) {
270      source = null;
271      target = null;
272    }
273
274    return myself();
275  }
276
277  @Override
278  public String getSource() {
279    return source;
280  }
281
282  @Override
283  public A source(@Nullable String source) {
284    this.source = source;
285    if (source != null) {
286      release = null;
287    }
288    return myself();
289  }
290
291  @Override
292  public String getTarget() {
293    return target;
294  }
295
296  @Override
297  public A target(@Nullable String target) {
298    this.target = target;
299    if (target != null) {
300      release = null;
301    }
302    return myself();
303  }
304
305  @Override
306  public boolean isFixJvmModulePathMismatch() {
307    return fixJvmModulePathMismatch;
308  }
309
310  @Override
311  public A fixJvmModulePathMismatch(boolean fixJvmModulePathMismatch) {
312    this.fixJvmModulePathMismatch = fixJvmModulePathMismatch;
313    return myself();
314  }
315
316  @Override
317  public boolean isInheritClassPath() {
318    return inheritClassPath;
319  }
320
321  @Override
322  public A inheritClassPath(boolean inheritClassPath) {
323    this.inheritClassPath = inheritClassPath;
324    return myself();
325  }
326
327  @Override
328  public boolean isInheritModulePath() {
329    return inheritModulePath;
330  }
331
332  @Override
333  public A inheritModulePath(boolean inheritModulePath) {
334    this.inheritModulePath = inheritModulePath;
335    return myself();
336  }
337
338  @Override
339  public boolean isInheritPlatformClassPath() {
340    return inheritPlatformClassPath;
341  }
342
343  @Override
344  public A inheritPlatformClassPath(boolean inheritPlatformClassPath) {
345    this.inheritPlatformClassPath = inheritPlatformClassPath;
346    return myself();
347  }
348
349  @Override
350  public boolean isInheritSystemModulePath() {
351    return inheritSystemModulePath;
352  }
353
354  @Override
355  public A inheritSystemModulePath(boolean inheritSystemModulePath) {
356    this.inheritSystemModulePath = inheritSystemModulePath;
357    return myself();
358  }
359
360  @Override
361  public Locale getLocale() {
362    return locale;
363  }
364
365  @Override
366  public A locale(Locale locale) {
367    requireNonNull(locale, "locale");
368    this.locale = locale;
369    return myself();
370  }
371
372  @Override
373  public Charset getLogCharset() {
374    return logCharset;
375  }
376
377  @Override
378  public A logCharset(Charset logCharset) {
379    requireNonNull(logCharset, "logCharset");
380    this.logCharset = logCharset;
381    return myself();
382  }
383
384  @Override
385  public LoggingMode getFileManagerLoggingMode() {
386    return fileManagerLoggingMode;
387  }
388
389  @Override
390  public A fileManagerLoggingMode(LoggingMode fileManagerLoggingMode) {
391    this.fileManagerLoggingMode = requireNonNull(fileManagerLoggingMode, "fileManagerLoggingMode");
392    return myself();
393  }
394
395  @Override
396  public LoggingMode getDiagnosticLoggingMode() {
397    return diagnosticLoggingMode;
398  }
399
400  @Override
401  public A diagnosticLoggingMode(LoggingMode diagnosticLoggingMode) {
402    requireNonNull(diagnosticLoggingMode, "diagnosticLoggingMode");
403    this.diagnosticLoggingMode = diagnosticLoggingMode;
404    return myself();
405  }
406
407  @Override
408  public AnnotationProcessorDiscovery getAnnotationProcessorDiscovery() {
409    return annotationProcessorDiscovery;
410  }
411
412  @Override
413  public A annotationProcessorDiscovery(AnnotationProcessorDiscovery annotationProcessorDiscovery) {
414    this.annotationProcessorDiscovery = requireNonNull(
415        annotationProcessorDiscovery,
416        "annotationProcessorDiscovery"
417    );
418    return myself();
419  }
420
421  /**
422   * Get the compiler name.
423   *
424   * @return the compiler name.
425   * @see #getName()
426   * @see #name(String)
427   * @deprecated Use {@link #getName()} instead.
428   */
429  @Deprecated
430  @Override
431  public final String toString() {
432    return name;
433  }
434
435  /**
436   * Get the flag builder factory to use for building flags.
437   *
438   * @return the factory.
439   */
440  public abstract JctFlagBuilderFactory getFlagBuilderFactory();
441
442  /**
443   * Get the JSR-199 compiler factory to use for initialising an internal compiler.
444   *
445   * @return the factory.
446   */
447  public abstract Jsr199CompilerFactory getCompilerFactory();
448
449  /**
450   * Get the file manager factory to use for building a file manager during compilation.
451   *
452   * @return the factory.
453   */
454  public abstract JctFileManagerFactory getFileManagerFactory();
455
456  /**
457   * Get the compilation factory to use for building a compilation.
458   *
459   * <p>By default, this uses a common internal implementation that is designed to work with
460   * compilers that have interfaces the same as, and behave the same as Javac.
461   *
462   * <p>Some obscure compiler implementations with potentially satanic rituals for initialising
463   * and configuring components correctly may need to provide a custom implementation here instead.
464   * In this case, this method should be overridden.
465   *
466   * @return the compilation factory.
467   */
468  public JctCompilationFactory getCompilationFactory() {
469    return new JctCompilationFactoryImpl(this);
470  }
471
472  /**
473   * {@inheritDoc}
474   *
475   * @return the default release version to use when no version is specified by the user.
476   */
477  @Override
478  public abstract String getDefaultRelease();
479
480  /**
481   * Get this implementation of {@link AbstractJctCompiler}, cast to the type parameter {@link A}.
482   *
483   * @return this implementation of {@link AbstractJctCompiler}, cast to {@link A}.
484   */
485  protected final A myself() {
486    @SuppressWarnings("unchecked")
487    var me = (A) this;
488
489    return me;
490  }
491
492  /**
493   * Build the list of flags from this compiler object using the flag builder.
494   *
495   * <p>Implementations should not need to override this unless there is a special edge case
496   * that needs configuring differently. This is exposed to assist in these kinds of cases.
497   *
498   * @param flagBuilder the flag builder to apply the flag configuration to.
499   * @return the string flags to use.
500   */
501  protected List<String> buildFlags(JctFlagBuilder flagBuilder) {
502    return flagBuilder
503        .annotationProcessorOptions(annotationProcessorOptions)
504        .showDeprecationWarnings(showDeprecationWarnings)
505        .failOnWarnings(failOnWarnings)
506        .compilerOptions(compilerOptions)
507        .previewFeatures(previewFeatures)
508        .release(release)
509        .source(source)
510        .target(target)
511        .verbose(verbose)
512        .showWarnings(showWarnings)
513        .build();
514  }
515
516  @SuppressWarnings("ThrowFromFinallyBlock")
517  private JctCompilation compileInternal(Workspace workspace, Collection<String> classNames) {
518    var fileManagerFactory = getFileManagerFactory();
519    var flagBuilderFactory = getFlagBuilderFactory();
520    var compilerFactory = getCompilerFactory();
521    var compilationFactory = getCompilationFactory();
522    var flags = buildFlags(flagBuilderFactory.createFlagBuilder());
523    var compiler = compilerFactory.createCompiler();
524    var fileManager = fileManagerFactory.createFileManager(workspace);
525
526    // Any internal exceptions should be rethrown as a JctCompilerException by the
527    // compilation factory, so there is nothing else to worry about here.
528    // Likewise, do not catch IOException on the compilation process, as it may hide
529    // bugs. Hence, we have to use a try-finally-try-catch-rethrow manually rather than a nice
530    // try-with-resources. This is kinda crap code, but it prevents reporting errors incorrectly.
531
532    try {
533      return compilationFactory.createCompilation(flags, fileManager, compiler, classNames);
534    } finally {
535      try {
536        fileManager.close();
537      } catch (IOException ex) {
538        throw new JctCompilerException(
539            "Failed to close file manager. This is probably a bug, so please report it.",
540            ex
541        );
542      }
543    }
544  }
545}