/**
* Copyright 2010-2017 Evgeny Gryaznov
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package org.textmapper.jps.build;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.ModuleChunk;
import org.jetbrains.jps.builders.DirtyFilesHolder;
import org.jetbrains.jps.builders.java.JavaSourceRootDescriptor;
import org.jetbrains.jps.incremental.*;
import org.jetbrains.jps.incremental.messages.*;
import org.jetbrains.jps.incremental.messages.BuildMessage.Kind;
import org.jetbrains.jps.model.java.JpsJavaExtensionService;
import org.jetbrains.jps.model.java.compiler.JpsJavaCompilerConfiguration;
import org.textmapper.idea.compiler.*;
import org.textmapper.jps.model.JpsTmExtensionService;
import org.textmapper.jps.model.JpsTmModuleExtension;
import org.textmapper.lapg.api.DerivedSourceElement;
import org.textmapper.lapg.api.ParserConflict;
import org.textmapper.lapg.api.SourceElement;
import org.textmapper.lapg.api.TextSourceElement;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* evgeny, 11/26/12
*/
public class TextmapperModuleLevelBuilder extends ModuleLevelBuilder {
private static final String TM_EXTENSION = ".tm";
private static final FileFilter TM_SOURCES_FILTER =
SystemInfo.isFileSystemCaseSensitive ?
file -> file.getPath().endsWith(TM_EXTENSION) :
file -> StringUtil.endsWithIgnoreCase(file.getPath(), TM_EXTENSION);
private TmIdeaRefreshComponent refreshComponent = new TmIdeaRefreshComponent();
protected TextmapperModuleLevelBuilder() {
super(BuilderCategory.SOURCE_GENERATOR);
}
@Override
public void buildStarted(final CompileContext context) {
context.addBuildListener(new BuildListener() {
@Override
public void filesGenerated(FileGeneratedEvent event) {
}
@Override
public void filesDeleted(FileDeletedEvent event) {
refreshComponent.filesRemoved(event.getFilePaths());
}
});
}
@Override
public void buildFinished(CompileContext context) {
Collection<String> filesToRefresh = refreshComponent.getFilesToRefresh();
for (String file : filesToRefresh) {
context.processMessage(new CustomBuilderMessage(TmCompilerUtil.BUILDER_ID, TmBuilderMessages.MSG_CHANGED, file));
}
context.processMessage(new CustomBuilderMessage(TmCompilerUtil.BUILDER_ID, TmBuilderMessages.MSG_REFRESH, ""));
}
@NotNull
@Override
public String getPresentableName() {
return TmCompilerUtil.BUILDER_ID;
}
@Override
public ExitCode build(final CompileContext compileContext,
ModuleChunk moduleChunk,
final DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder,
final OutputConsumer outputConsumer) throws ProjectBuildException, IOException {
ExitCode status = ExitCode.NOTHING_DONE;
try {
final List<Pair<TmCompilerTask, ModuleBuildTarget>> toCompile = collectChangedFiles(compileContext, dirtyFilesHolder);
if (toCompile.isEmpty()) {
return status;
}
for (final Pair<TmCompilerTask, ModuleBuildTarget> entry : toCompile) {
final TmCompilerTask task = entry.getFirst();
refreshComponent.addOutputRoot(task.getOutputDir().getPath());
boolean success = TmCompilerUtil.compileFile(task, new TmCompilerContext() {
@Override
public TmProcessingStatus createProcessingStatus() {
return new JpsTmProcessingStatus(task.getFile(), compileContext);
}
@Override
public void fileCreated(File newFile, boolean isUnchanged) throws IOException {
final String sourcePath = FileUtil.toSystemIndependentName(task.getFile().getPath());
if (!isUnchanged) {
// mark dirty to re-compile
FSOperations.markDirty(compileContext, newFile);
// refresh virtual file in IDEA
refreshComponent.refresh(newFile.getPath());
}
outputConsumer.registerOutputFile(entry.getSecond(), newFile, Collections.singletonList(sourcePath));
}
@Override
public void reportProgress(String message) {
compileContext.processMessage(new ProgressMessage(message));
}
});
if (success) {
status = ExitCode.OK;
}
}
} catch (Exception ex) {
throw new ProjectBuildException(ex);
}
return status;
}
private static List<Pair<TmCompilerTask, ModuleBuildTarget>> collectChangedFiles(CompileContext context,
DirtyFilesHolder<JavaSourceRootDescriptor, ModuleBuildTarget> dirtyFilesHolder) throws IOException {
final JpsJavaCompilerConfiguration configuration = JpsJavaExtensionService.getInstance().getCompilerConfiguration(context.getProjectDescriptor().getProject());
assert configuration != null;
final List<Pair<TmCompilerTask, ModuleBuildTarget>> toCompile = new ArrayList<>();
dirtyFilesHolder.processDirtyFiles((target, file, sourceRoot) -> {
if (TM_SOURCES_FILTER.accept(file) /* TODO && !configuration.isResourceFile(file, sourceRoot.root) */) {
JpsTmModuleExtension ext = JpsTmExtensionService.getInstance().getExtension(target.getModule());
if (ext != null) {
File outputDir = file.getParentFile();
if (outputDir.isDirectory()) {
toCompile.add(new Pair<>(
new TmCompilerTask(file, null, outputDir, ext.isVerbose(), ext.isExcludeDefaultTemplates(), ext.getCustomTemplatesFolder()),
target));
}
}
}
return true;
});
return toCompile;
}
private static class JpsTmProcessingStatus implements TmProcessingStatus {
private File originalFile;
private CompileContext compileContext;
private boolean hasErrors = false;
public JpsTmProcessingStatus(File originalFile, CompileContext compileContext) {
this.compileContext = compileContext;
this.originalFile = originalFile;
}
public boolean hasErrors() {
return hasErrors;
}
public void report(int kind, String message, SourceElement... anchors) {
if (kind <= KIND_ERROR) {
hasErrors = true;
}
SourceElement anchor = anchors != null && anchors.length >= 1 ? anchors[0] : null;
while (anchor instanceof DerivedSourceElement) {
anchor = ((DerivedSourceElement) anchor).getOrigin();
}
if (anchor instanceof TextSourceElement) {
TextSourceElement textElement = (TextSourceElement) anchor;
compileContext.processMessage(new CompilerMessage(TmCompilerUtil.BUILDER_ID, toIdeaKind(kind),
message, textElement.getResourceName(),
textElement.getOffset(), textElement.getEndoffset(), textElement.getOffset(),
textElement.getLine(), 1 /* TODO */));
} else {
compileContext.processMessage(new CompilerMessage(TmCompilerUtil.BUILDER_ID, toIdeaKind(kind),
message, originalFile.getPath()));
}
}
public void report(String message, Throwable th) {
hasErrors = true;
compileContext.processMessage(new CompilerMessage(TmCompilerUtil.BUILDER_ID, Kind.ERROR,
message + (th != null ? ": " + th.getMessage() : ""), originalFile.getPath()));
}
public void report(ParserConflict conflict) {
if (conflict.getKind() != ParserConflict.FIXED) {
report(KIND_ERROR, conflict.getText(), conflict.getRules());
}
}
public void debug(String info) {
// ignore
}
public boolean isDebugMode() {
return false;
}
public boolean isAnalysisMode() {
return false;
}
private Kind toIdeaKind(int kind) {
switch (kind) {
case KIND_FATAL:
case KIND_ERROR:
return Kind.ERROR;
case KIND_WARN:
return Kind.WARNING;
default:
return Kind.INFO;
}
}
}
}