/**
* Copyright (C) 2006-2017 INRIA and contributors
* Spoon - http://spoon.gforge.inria.fr/
*
* This software is governed by the CeCILL-C License under French law and
* abiding by the rules of distribution of free software. You can use, modify
* and/or redistribute the software under the terms of the CeCILL-C license as
* circulated by CEA, CNRS and INRIA at http://www.cecill.info.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL-C license and that you accept its terms.
*/
package spoon.support.compiler.jdt;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Level;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import spoon.Launcher;
import spoon.OutputType;
import spoon.SpoonException;
import spoon.compiler.Environment;
import spoon.compiler.ModelBuildingException;
import spoon.compiler.SpoonFile;
import spoon.compiler.SpoonFolder;
import spoon.compiler.SpoonResource;
import spoon.compiler.SpoonResourceHelper;
import spoon.compiler.builder.AdvancedOptions;
import spoon.compiler.builder.AnnotationProcessingOptions;
import spoon.compiler.builder.ClasspathOptions;
import spoon.compiler.builder.ComplianceOptions;
import spoon.compiler.builder.JDTBuilder;
import spoon.compiler.builder.JDTBuilderImpl;
import spoon.compiler.builder.SourceOptions;
import spoon.processing.ProcessingManager;
import spoon.processing.Processor;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtType;
import spoon.reflect.factory.Factory;
import spoon.reflect.visitor.AstParentConsistencyChecker;
import spoon.reflect.visitor.DefaultJavaPrettyPrinter;
import spoon.reflect.visitor.Filter;
import spoon.reflect.visitor.PrettyPrinter;
import spoon.reflect.visitor.Query;
import spoon.support.QueueProcessingManager;
import spoon.support.compiler.VirtualFolder;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Main class of Spoon to build the model.
* Highly depends on {@link JDTBatchCompiler} for performing the job.
*/
public class JDTBasedSpoonCompiler implements spoon.SpoonModelBuilder {
protected INameEnvironment environment = null;
protected final List<CategorizedProblem> probs = new ArrayList<>();
protected final TreeBuilderRequestor requestor = new TreeBuilderRequestor(this);
protected Factory factory;
protected int javaCompliance = 7;
protected boolean build = false;
//list of java files or folders with java files which represents source of the CtModel
protected SpoonFolder sources = new VirtualFolder();
//list of java files or folders with java files which represents templates. Templates are added to CtModel too.
protected SpoonFolder templates = new VirtualFolder();
//The classpath used to build templates
protected String[] templateClasspath = new String[0];
protected boolean buildOnlyOutdatedFiles = false;
protected File outputDirectory = new File(Launcher.OUTPUTDIR);
protected List<SpoonResource> forceBuildList = new ArrayList<>();
protected String encoding;
protected List<CompilationUnitFilter> compilationUnitFilters = new ArrayList<>();
/**
* Default constructor
*/
public JDTBasedSpoonCompiler(Factory factory) {
this.factory = factory;
}
@Override
public boolean build() {
return build(null);
}
@Override
public boolean build(JDTBuilder builder) {
if (factory == null) {
throw new SpoonException("Factory not initialized");
}
if (build) {
throw new SpoonException("Model already built");
}
build = true;
boolean srcSuccess, templateSuccess;
factory.getEnvironment().debugMessage("building sources: " + sources.getAllJavaFiles());
long t = System.currentTimeMillis();
javaCompliance = factory.getEnvironment().getComplianceLevel();
srcSuccess = buildSources(builder);
reportProblems(factory.getEnvironment());
factory.getEnvironment().debugMessage("built in " + (System.currentTimeMillis() - t) + " ms");
factory.getEnvironment().debugMessage("building templates: " + templates.getAllJavaFiles());
t = System.currentTimeMillis();
templateSuccess = buildTemplates(builder);
factory.getEnvironment().debugMessage("built in " + (System.currentTimeMillis() - t) + " ms");
checkModel();
return srcSuccess && templateSuccess;
}
private void checkModel() {
if (!factory.getEnvironment().checksAreSkipped()) {
factory.getModel().getRootPackage().accept(new AstParentConsistencyChecker());
}
}
@Override
public boolean compile(InputType... types) {
factory.getEnvironment().debugMessage("compiling sources: " + factory.CompilationUnit().getMap().keySet());
long t = System.currentTimeMillis();
javaCompliance = factory.getEnvironment().getComplianceLevel();
JDTBatchCompiler batchCompiler = createBatchCompiler(types);
final String[] args = new JDTBuilderImpl() //
.classpathOptions(new ClasspathOptions().encoding(this.encoding).classpath(getSourceClasspath()).binaries(getBinaryOutputDirectory())) //
.complianceOptions(new ComplianceOptions().compliance(javaCompliance)) //
.annotationProcessingOptions(new AnnotationProcessingOptions().compileProcessors()) //
.advancedOptions(new AdvancedOptions().preserveUnusedVars().continueExecution().enableJavadoc()) //
.sources(new SourceOptions().sources(sources.getAllJavaFiles())) // no sources, handled by the JDTBatchCompiler
.build();
getFactory().getEnvironment().debugMessage("compile args: " + Arrays.toString(args));
System.setProperty("jdt.compiler.useSingleThread", "true");
batchCompiler.compile(args);
reportProblems(factory.getEnvironment());
factory.getEnvironment().debugMessage("compiled in " + (System.currentTimeMillis() - t) + " ms");
return probs.size() == 0;
}
@Override
public void instantiateAndProcess(List<String> processors) {
// processing (consume all the processors)
ProcessingManager processing = new QueueProcessingManager(factory);
for (String processorName : processors) {
processing.addProcessor(processorName);
factory.getEnvironment().debugMessage("Loaded processor " + processorName + ".");
}
processing.process(factory.Package().getRootPackage());
}
@Override
public void process(Collection<Processor<? extends CtElement>> processors) {
// processing (consume all the processors)
ProcessingManager processing = new QueueProcessingManager(factory);
for (Processor<? extends CtElement> processorName : processors) {
processing.addProcessor(processorName);
factory.getEnvironment().debugMessage("Loaded processor " + processorName + ".");
}
processing.process(factory.Package().getRootPackage());
}
@Override
public void generateProcessedSourceFiles(OutputType outputType) {
generateProcessedSourceFiles(outputType, null);
}
@Override
public void generateProcessedSourceFiles(OutputType outputType, Filter<CtType<?>> typeFilter) {
switch (outputType) {
case CLASSES:
generateProcessedSourceFilesUsingTypes(typeFilter);
break;
case COMPILATION_UNITS:
generateProcessedSourceFilesUsingCUs();
break;
case NO_OUTPUT:
}
}
@Override
public void addInputSource(File source) {
try {
if (SpoonResourceHelper.isFile(source)) {
this.sources.addFile(SpoonResourceHelper.createFile(source));
} else {
this.sources.addFolder(SpoonResourceHelper.createFolder(source));
}
} catch (Exception e) {
throw new SpoonException(e);
}
}
@Override
public void addInputSource(SpoonResource source) {
if (source.isFile()) {
this.sources.addFile((SpoonFile) source);
} else {
this.sources.addFolder((SpoonFolder) source);
}
}
@Override
public void addInputSources(List<SpoonResource> resources) {
for (SpoonResource r : resources) {
addInputSource(r);
}
}
@Override
public Set<File> getInputSources() {
Set<File> files = new HashSet<>();
for (SpoonFolder file : getSource().getSubFolders()) {
files.add(new File(file.getPath()));
}
return files;
}
@Override
public void addTemplateSource(SpoonResource source) {
if (source.isFile()) {
this.templates.addFile((SpoonFile) source);
} else {
this.templates.addFolder((SpoonFolder) source);
}
}
@Override
public void addTemplateSource(File source) {
try {
if (SpoonResourceHelper.isFile(source)) {
this.templates.addFile(SpoonResourceHelper.createFile(source));
} else {
this.templates.addFolder(SpoonResourceHelper.createFolder(source));
}
} catch (Exception e) {
throw new SpoonException(e);
}
}
@Override
public void addTemplateSources(List<SpoonResource> resources) {
for (SpoonResource r : resources) {
addTemplateSource(r);
}
}
@Override
public Set<File> getTemplateSources() {
Set<File> files = new HashSet<>();
for (SpoonFolder file : getTemplates().getSubFolders()) {
files.add(new File(file.getPath()));
}
return files;
}
@Override
public void setSourceOutputDirectory(File outputDirectory) {
this.outputDirectory = outputDirectory;
}
@Override
public File getSourceOutputDirectory() {
return outputDirectory;
}
@Override
public void setBinaryOutputDirectory(File binaryOutputDirectory) {
this.getEnvironment().setBinaryOutputDirectory(binaryOutputDirectory.getAbsolutePath());
}
@Override
public File getBinaryOutputDirectory() {
return new File(getEnvironment().getBinaryOutputDirectory());
}
@Override
public String[] getSourceClasspath() {
return getEnvironment().getSourceClasspath();
}
@Override
public void setSourceClasspath(String... classpath) {
getEnvironment().setSourceClasspath(classpath);
}
@Override
public String[] getTemplateClasspath() {
return templateClasspath;
}
@Override
public void setTemplateClasspath(String... classpath) {
this.templateClasspath = classpath;
}
@Override
public void setBuildOnlyOutdatedFiles(boolean buildOnlyOutdatedFiles) {
this.buildOnlyOutdatedFiles = buildOnlyOutdatedFiles;
}
@Override
public void forceBuild(SpoonResource source) {
forceBuildList.add(source);
}
@Override
public String getEncoding() {
return encoding;
}
@Override
public void setEncoding(String encoding) {
this.encoding = encoding;
}
@Override
public Factory getFactory() {
return factory;
}
protected boolean buildSources(JDTBuilder jdtBuilder) {
return buildUnitsAndModel(jdtBuilder, sources, getSourceClasspath(), "", buildOnlyOutdatedFiles);
}
protected JDTBatchCompiler createBatchCompiler() {
return new JDTBatchCompiler(this);
}
protected JDTBatchCompiler createBatchCompiler(InputType... types) {
JDTBatchCompiler batchCompiler = createBatchCompiler();
// backward compatible
if (types.length == 0) {
types = new InputType[]{InputType.CTTYPES};
}
for (InputType inputType : types) {
inputType.initializeCompiler(batchCompiler);
}
return batchCompiler;
}
protected boolean buildTemplates(JDTBuilder jdtBuilder) {
return buildUnitsAndModel(jdtBuilder, templates, getTemplateClasspath(), "template ", false);
}
protected boolean buildUnitsAndModel(JDTBuilder jdtBuilder, SpoonFolder sourcesFolder, String[] classpath, String debugMessagePrefix, boolean buildOnlyOutdatedFiles) {
CompilationUnitDeclaration[] units = buildUnits(jdtBuilder, sourcesFolder, classpath, debugMessagePrefix, buildOnlyOutdatedFiles);
// here we build the model in the template factory
buildModel(units);
return probs.size() == 0;
}
private static final CompilationUnitDeclaration[] EMPTY_RESULT = new CompilationUnitDeclaration[0];
protected CompilationUnitDeclaration[] buildUnits(JDTBuilder jdtBuilder, SpoonFolder sourcesFolder, String[] classpath, String debugMessagePrefix, boolean buildOnlyOutdatedFiles) {
List<SpoonFile> sourceFiles = sourcesFolder.getAllJavaFiles();
if (sourceFiles.isEmpty()) {
return EMPTY_RESULT;
}
JDTBatchCompiler batchCompiler = createBatchCompiler(new FileCompilerConfig(sourcesFolder));
String[] args;
if (jdtBuilder == null) {
args = new JDTBuilderImpl() //
.classpathOptions(new ClasspathOptions().encoding(this.encoding).classpath(classpath)) //
.complianceOptions(new ComplianceOptions().compliance(javaCompliance)) //
.advancedOptions(new AdvancedOptions().preserveUnusedVars().continueExecution().enableJavadoc()) //
.sources(new SourceOptions().sources(sourcesFolder.getAllJavaFiles())) // no sources, handled by the JDTBatchCompiler
.build();
} else {
args = jdtBuilder.build();
}
getFactory().getEnvironment().debugMessage(debugMessagePrefix + "build args: " + Arrays.toString(args));
batchCompiler.configure(args);
if (buildOnlyOutdatedFiles && outputDirectory.exists()) {
@SuppressWarnings("unchecked") Collection<File> outputFiles = FileUtils.listFiles(outputDirectory, new String[] { "java" }, true);
keepOutdatedFiles(sourceFiles, outputFiles);
}
CompilationUnitDeclaration[] units = batchCompiler.getUnits();
return units;
}
protected void buildModel(CompilationUnitDeclaration[] units) {
JDTTreeBuilder builder = new JDTTreeBuilder(factory);
unitLoop:
for (CompilationUnitDeclaration unit : units) {
if (!unit.isEmpty()) {
final String unitPath = new String(unit.getFileName());
for (final CompilationUnitFilter cuf : compilationUnitFilters) {
if (cuf.exclude(unitPath)) {
// do not traverse this unit
continue unitLoop;
}
}
unit.traverse(builder, unit.scope);
if (getFactory().getEnvironment().isCommentsEnabled()) {
new JDTCommentBuilder(unit, factory).build();
}
}
}
}
protected void generateProcessedSourceFilesUsingTypes(Filter<CtType<?>> typeFilter) {
if (factory.getEnvironment().getDefaultFileGenerator() != null) {
factory.getEnvironment().debugMessage("Generating source using types...");
ProcessingManager processing = new QueueProcessingManager(factory);
processing.addProcessor(factory.getEnvironment().getDefaultFileGenerator());
if (typeFilter != null) {
processing.process(Query.getElements(factory.Package().getRootPackage(), typeFilter));
} else {
processing.process(factory.Package().getRootPackage());
}
}
}
protected void generateProcessedSourceFilesUsingCUs() {
factory.getEnvironment().debugMessage("Generating source using compilation units...");
// Check output directory
if (outputDirectory == null) {
throw new RuntimeException("You should set output directory before generating source files");
}
// Create spooned directory
if (outputDirectory.isFile()) {
throw new RuntimeException("Output must be a directory");
}
if (!outputDirectory.exists()) {
if (!outputDirectory.mkdirs()) {
throw new RuntimeException("Error creating output directory");
}
}
try {
outputDirectory = outputDirectory.getCanonicalFile();
} catch (IOException e1) {
throw new SpoonException(e1);
}
factory.getEnvironment().debugMessage("Generating source files to: " + outputDirectory);
List<File> printedFiles = new ArrayList<>();
for (spoon.reflect.cu.CompilationUnit cu : factory.CompilationUnit().getMap().values()) {
factory.getEnvironment().debugMessage("Generating source for compilation unit: " + cu.getFile());
CtType<?> element = cu.getMainType();
CtPackage pack = element.getPackage();
// create package directory
File packageDir;
if (pack.isUnnamedPackage()) {
packageDir = new File(outputDirectory.getAbsolutePath());
} else {
// Create current package directory
packageDir = new File(outputDirectory.getAbsolutePath() + File.separatorChar + pack.getQualifiedName().replace('.', File.separatorChar));
}
if (!packageDir.exists()) {
if (!packageDir.mkdirs()) {
throw new RuntimeException("Error creating output directory");
}
}
// print type
try {
File file = new File(packageDir.getAbsolutePath() + File.separatorChar + element.getSimpleName() + DefaultJavaPrettyPrinter.JAVA_FILE_EXTENSION);
file.createNewFile();
// the path must be given relatively to to the working directory
InputStream is = getCompilationUnitInputStream(cu.getFile().getPath());
IOUtils.copy(is, new FileOutputStream(file));
if (!printedFiles.contains(file)) {
printedFiles.add(file);
}
} catch (Exception e) {
Launcher.LOGGER.error(e.getMessage(), e);
}
}
}
protected void keepOutdatedFiles(List<SpoonFile> files, Collection<File> outputFiles) {
int offset = outputDirectory.getAbsolutePath().length() + 1;
Collection<String> relativeOutputPaths = new ArrayList<>();
for (File f : outputFiles) {
relativeOutputPaths.add(f.getAbsolutePath().substring(offset));
}
for (SpoonFile sf : new ArrayList<>(files)) {
if (forceBuildList.contains(sf)) {
continue;
}
File f = sf.toFile();
for (String s : relativeOutputPaths) {
if (f.getAbsolutePath().endsWith(s)) {
if (f.lastModified() <= new File(outputDirectory, s).lastModified()) {
files.remove(sf);
}
}
}
}
}
public void setEnvironment(INameEnvironment environment) {
this.environment = environment;
}
/**
* report a compilation problem (callback for JDT)
*/
public void reportProblem(CategorizedProblem pb) {
if (pb == null) {
return;
}
// we can not accept this problem, even in noclasspath mode
// otherwise a nasty null pointer exception occurs later
if (pb.getID() == IProblem.DuplicateTypes) {
throw new ModelBuildingException(pb.getMessage());
}
probs.add(pb);
}
public void reportProblems(Environment environment) {
if (getProblems().size() > 0) {
for (CategorizedProblem problem : getProblems()) {
if (problem != null) {
report(environment, problem);
}
}
}
}
protected void report(Environment environment, CategorizedProblem problem) {
if (problem == null) {
throw new IllegalArgumentException("problem cannot be null");
}
File file = new File(new String(problem.getOriginatingFileName()));
String filename = file.getAbsolutePath();
String message = problem.getMessage() + " at " + filename + ":" + problem.getSourceLineNumber();
if (problem.isError()) {
if (!environment.getNoClasspath()) {
// by default, compilation errors are notified as exception
throw new ModelBuildingException(message);
} else {
// in noclasspath mode, errors are only reported
// but undefined import, type, and name errors are irrelevant
int problemId = problem.getID();
if (problemId != IProblem.UndefinedType && problemId != IProblem.UndefinedName
&& problemId != IProblem.ImportNotFound) {
environment.report(null, Level.WARN, message);
}
}
}
}
/**
* returns the list of current problems
*/
public List<CategorizedProblem> getProblems() {
return Collections.unmodifiableList(this.probs);
}
public SpoonFolder getSource() {
return sources;
}
public SpoonFolder getTemplates() {
return templates;
}
protected InputStream getCompilationUnitInputStream(String path) {
Environment env = factory.getEnvironment();
spoon.reflect.cu.CompilationUnit cu = factory.CompilationUnit().getMap().get(path);
List<CtType<?>> toBePrinted = cu.getDeclaredTypes();
PrettyPrinter printer = new DefaultJavaPrettyPrinter(env);
printer.calculate(cu, toBePrinted);
return new ByteArrayInputStream(printer.getResult().toString().getBytes());
}
protected Environment getEnvironment() {
return getFactory().getEnvironment();
}
@Override
public void addCompilationUnitFilter(final CompilationUnitFilter filter) {
compilationUnitFilters.add(filter);
}
@Override
public void removeCompilationUnitFilter(CompilationUnitFilter filter) {
compilationUnitFilters.remove(filter);
}
@Override
public List<CompilationUnitFilter> getCompilationUnitFilter() {
return new ArrayList<>(compilationUnitFilters);
}
}