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()); } }