package jetbrains.mps.vcs.platform.integration;
/*Generated by MPS */
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import com.intellij.notification.Notification;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SModel;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.module.SRepository;
import jetbrains.mps.project.Project;
import jetbrains.mps.internal.collections.runtime.Sequence;
import jetbrains.mps.internal.collections.runtime.IWhereFilter;
import java.util.Map;
import org.jetbrains.mps.openapi.model.SNodeReference;
import java.util.HashMap;
import jetbrains.mps.baseLanguage.closures.runtime.Wrappers;
import jetbrains.mps.internal.collections.runtime.IterableUtils;
import jetbrains.mps.internal.collections.runtime.ISelector;
import com.intellij.notification.NotificationType;
import com.intellij.notification.NotificationListener;
import org.jetbrains.annotations.NotNull;
import javax.swing.event.HyperlinkEvent;
import jetbrains.mps.openapi.navigation.EditorNavigator;
import com.intellij.notification.Notifications;
import jetbrains.mps.ide.project.ProjectHelper;
import java.util.List;
import jetbrains.mps.project.ProjectManager;
import jetbrains.mps.extapi.persistence.FileDataSource;
import org.apache.log4j.Level;
import jetbrains.mps.vfs.IFile;
import java.io.File;
import com.intellij.openapi.application.ApplicationManager;
import jetbrains.mps.ide.platform.watching.ReloadManager;
import jetbrains.mps.util.Computable;
import jetbrains.mps.extapi.module.SModuleBase;
import jetbrains.mps.extapi.model.SModelBase;
import com.intellij.openapi.application.ModalityState;
import javax.swing.JOptionPane;
import com.intellij.openapi.ui.Messages;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.smodel.persistence.def.ModelPersistence;
import jetbrains.mps.vcs.util.MergeDriverBackupUtil;
import jetbrains.mps.vcs.platform.util.MergeBackupUtil;
import java.io.IOException;
import jetbrains.mps.util.SNodeOperations;
import jetbrains.mps.vcspersistence.VCSPersistenceUtil;
import com.intellij.diff.contents.DiffContent;
import jetbrains.mps.internal.collections.runtime.ListSequence;
import java.util.ArrayList;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.diff.DiffManager;
import com.intellij.diff.DiffDialogHints;
import jetbrains.mps.vcs.util.ModelVersion;
import com.intellij.openapi.ui.TestDialog;
import com.intellij.openapi.application.Application;
public class ModelStorageProblemsListener extends SRepositoryContentAdapter {
private static final Logger LOG = LogManager.getLogger(ModelStorageProblemsListener.class);
private Notification myLastNotification;
private volatile SModelReference myLastModel;
@Override
protected void startListening(SModel model) {
model.addModelListener(this);
}
@Override
protected void stopListening(SModel model) {
model.removeModelListener(this);
}
@Override
public void modelSaved(SModel model) {
final SModelReference ref = myLastModel;
if (ref != null && ref.equals(model.getReference())) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
if (myLastModel == ref && myLastNotification != null) {
myLastNotification.expire();
myLastNotification = null;
myLastModel = null;
}
}
});
}
}
@Override
public void conflictDetected(SModel model) {
EditableSModel m = (EditableSModel) model;
assert m.isChanged() && m.needsReloading();
resolveDiskMemoryConflict(m);
}
@Override
public void problemsDetected(SModel model, Iterable<SModel.Problem> problems) {
Iterable<SModel.Problem> pr = problems;
SRepository repository = model.getRepository();
final Project project = getProjectFromUI(repository);
if (Sequence.fromIterable(pr).any(new IWhereFilter<SModel.Problem>() {
public boolean accept(SModel.Problem it) {
return it.isError();
}
})) {
final boolean isSave = Sequence.fromIterable(pr).any(new IWhereFilter<SModel.Problem>() {
public boolean accept(SModel.Problem it) {
return it.isError() && it.getKind() == SModel.Problem.Kind.Save;
}
});
final Map<String, SNodeReference> errMap = new HashMap<String, SNodeReference>();
final Wrappers._int index = new Wrappers._int(0);
String problemText = IterableUtils.join(Sequence.fromIterable(pr).where(new IWhereFilter<SModel.Problem>() {
public boolean accept(SModel.Problem it) {
return it.isError();
}
}).select(new ISelector<SModel.Problem, String>() {
public String select(SModel.Problem it) {
String link = "";
if (it.getNode() != null && project != null) {
link = " (<a href=\"" + index.value + "\">view node</a>)";
errMap.put(Integer.toString(index.value++), it.getNode().getReference());
}
return "error: " + it.getText() + link;
}
}).take(3), "<br/>");
final String message = String.format("<p>Cannot %s model %s.<br/>%s</p>", (isSave ? "save" : "load"), model.getModelName(), problemText);
final SModelReference ref = model.getReference();
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
if (myLastNotification != null) {
myLastNotification.expire();
}
myLastNotification = new Notification("Model Persistence", (isSave ? "Save Failure" : "Load Failure"), message, NotificationType.WARNING, new NotificationListener() {
public void hyperlinkUpdate(@NotNull Notification p0, @NotNull HyperlinkEvent e) {
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
return;
}
SNodeReference ref = errMap.get(e.getDescription());
assert ref != null;
new EditorNavigator(project).shallFocus(true).shallSelect(true).open(ref);
}
});
myLastModel = ref;
Notifications.Bus.notify(myLastNotification);
}
});
}
}
private Project getProjectFromUI(SRepository repository) {
Project project = ProjectHelper.getProject(repository);
if (project == null) {
// Note: the following code can be removed after proper implementation of project repositories
List<Project> openProjects = ProjectManager.getInstance().getOpenedProjects();
if (openProjects.size() == 1) {
project = openProjects.get(0);
}
}
return project;
}
private void resolveDiskMemoryConflict(final EditableSModel model) {
if (!(model.getSource() instanceof FileDataSource)) {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error(String.format("Conflicting content in memory and on disk for model %s from %s. Unfortunately, MPS does not support conflict resolution for models from multiple files yet, conflict ignored.", model.getModelName(), model.getSource().getLocation()));
}
return;
}
final IFile file = ((FileDataSource) model.getSource()).getFile();
final File backupFile = doBackup(file, model);
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
public void run() {
// do nothing if conflict was already resolved and model was saved or reloaded or unregistered
if (!(model.isChanged()) || model.getRepository() == null) {
backupFile.delete();
return;
}
assert model.getRepository() != null;
final boolean contentConflict = file.exists();
boolean needSave = ReloadManager.getInstance().computeNoReload(new Computable<Boolean>() {
public Boolean compute() {
if (contentConflict) {
return showDiskMemoryQuestion(file, model, backupFile);
} else {
return showDeletedFromDiskQuestion(model, backupFile);
}
}
});
if (needSave) {
// FIXME it used to be executeCommand (that replaced runWriteActionInCommand) here.
// as long as our modules are always loaded into global repository, model.getRepository().getModelAccess() gives
// GlobalModelAccess of MPSModuleRepository, which doesn't support commands.
// Earlier code went fine with runWriteActionInCommand() which looked up active project from UI.
// MSPL, however, listens to all repositories, and it's odd to execute a command in a project for a model that may belong to a completely different one.
// Therefore, it's better to stick to model's native repository. What we lack with runWriteAction instead of executeCommand is undo capability, perhaps.
// Is it something so vital anyone would complain of?
model.getRepository().getModelAccess().runWriteAction(new Runnable() {
public void run() {
model.updateTimestamp();
model.save();
}
});
} else {
model.getRepository().getModelAccess().runWriteAction(new Runnable() {
public void run() {
if (contentConflict) {
model.reloadFromSource();
} else {
((SModuleBase) model.getModule()).unregisterModel((SModelBase) model);
}
}
});
}
}
}, ModalityState.NON_MODAL);
}
private static boolean showDeletedFromDiskQuestion(SModel inMemory, File backupFile) {
if (isApplicationInUnitTestOrHeadless()) {
return ourTestImplementation.show("") == 0;
}
int result = JOptionPane.showConfirmDialog(null, "Model file for model \n" + inMemory + "\n was externally deleted from disk.\n" + "Backup of it was saved to \"" + backupFile.getAbsolutePath() + "\"\nDo you wish to restore it?", "Model Deleted Externally", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, Messages.getQuestionIcon());
return result == 0;
}
private static boolean showDiskMemoryQuestion(IFile modelFile, SModel inMemory, File backupFile) {
String message = "Changes have been made to \n" + inMemory + "\n model in memory and on disk.\n" + "Backup of both versions was saved to \"" + backupFile.getAbsolutePath() + "\"\n" + "Which version to use?";
String title = "Model Versions Conflict";
String[] options = {"Load File System Version", "Save Memory Version", "Show Difference"};
while (true) {
if (isApplicationInUnitTestOrHeadless()) {
return ourTestImplementation.show("") == 1;
}
int result = JOptionPane.showOptionDialog(null, message, title, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, Messages.getQuestionIcon(), options, null);
switch (result) {
case 0:
// disk version
return false;
case 1:
// memory version
return true;
case 2:
default:
// diff dialog or cancel
openDiffDialog(modelFile, inMemory);
}
}
}
private static File doBackup(IFile modelFile, final SModel inMemory) {
try {
File tmp = FileUtil.createTmpDir();
// as the model is already in repo, we can assume it's in supported persistence
final Wrappers._T<String> modelData = new Wrappers._T<String>();
inMemory.getRepository().getModelAccess().runReadAction(new Runnable() {
public void run() {
modelData.value = ModelPersistence.modelToString(((SModelBase) inMemory).getSModelInternal());
}
});
MergeDriverBackupUtil.writeContentsToFile(modelData.value.getBytes(FileUtil.DEFAULT_CHARSET), modelFile.getName(), tmp, ModelStorageProblemsListener.DiskMemoryConflictVersion.MEMORY.getSuffix());
if (modelFile.exists()) {
com.intellij.openapi.util.io.FileUtil.copy(new File(modelFile.getPath()), new File(tmp.getAbsolutePath(), modelFile.getName() + "." + ModelStorageProblemsListener.DiskMemoryConflictVersion.FILE_SYSTEM.getSuffix()));
}
File zipfile = MergeBackupUtil.chooseZipFileForModelFile(modelFile);
zipfile.getParentFile().mkdirs();
FileUtil.zip(tmp, zipfile);
FileUtil.delete(tmp);
return zipfile;
} catch (IOException e) {
if (LOG.isEnabledFor(Level.ERROR)) {
LOG.error("Cannot create backup during resolving disk-memory conflict for " + SNodeOperations.getModelLongName(inMemory), e);
}
throw new RuntimeException(e);
}
}
private static void openDiffDialog(IFile modelFile, SModel inMemory) {
SModel onDisk = VCSPersistenceUtil.loadModel(modelFile);
com.intellij.openapi.project.Project project = com.intellij.openapi.project.ProjectManager.getInstance().getOpenProjects()[0];
List<DiffContent> contents = ListSequence.fromListAndArray(new ArrayList<DiffContent>(), new ModelDiffContent(onDisk), new ModelDiffContent(inMemory));
List<String> titles = ListSequence.fromListAndArray(new ArrayList<String>(), "Filesystem version (Read-Only)", "Memory Version");
DiffRequest request = new SimpleDiffRequest("Model file and model in memory differs", contents, titles);
DiffManager.getInstance().showDiff(project, request, DiffDialogHints.MODAL);
}
public enum DiskMemoryConflictVersion implements ModelVersion {
FILE_SYSTEM("filesystem"),
MEMORY("memory");
private final String mySuffix;
private DiskMemoryConflictVersion(String suffix) {
mySuffix = suffix;
}
@Override
public String getSuffix() {
return mySuffix;
}
}
private static TestDialog ourTestImplementation = TestDialog.DEFAULT;
public static TestDialog setTestDialog(TestDialog newValue) {
Application application = ApplicationManager.getApplication();
if (application != null) {
assert application.isUnitTestMode() : "This method is available for tests only";
}
TestDialog oldValue = ourTestImplementation;
ourTestImplementation = newValue;
return oldValue;
}
private static boolean isApplicationInUnitTestOrHeadless() {
final Application application = ApplicationManager.getApplication();
return application != null && (application.isUnitTestMode() || application.isHeadlessEnvironment());
}
}