/*
* Copyright 2015 Nokia Solutions and Networks
* Licensed under the Apache License, Version 2.0,
* see license.txt file for details.
*/
package org.robotframework.ide.eclipse.main.plugin.tableeditor;
import static com.google.common.collect.Lists.newArrayList;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import javax.inject.Inject;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.content.IContentDescriber;
import org.eclipse.debug.ui.actions.IToggleBreakpointsTarget;
import org.eclipse.e4.core.contexts.ContextFunction;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.di.UIEventTopic;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.IDocument;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorRegistry;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IPerspectiveListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.forms.editor.FormEditor;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import org.rf.ide.core.executor.RedSystemProperties;
import org.rf.ide.core.executor.RobotRuntimeEnvironment;
import org.rf.ide.core.testdata.DumpContext;
import org.rf.ide.core.testdata.DumpedResultBuilder.DumpedResult;
import org.rf.ide.core.testdata.RobotFileDumper;
import org.rf.ide.core.testdata.mapping.QuickTokenListenerBaseTwoModelReferencesLinker;
import org.rf.ide.core.testdata.mapping.TwoModelReferencesLinker;
import org.rf.ide.core.testdata.model.RobotFile;
import org.rf.ide.core.testdata.model.RobotFileOutput;
import org.rf.ide.core.testdata.text.read.separators.TokenSeparatorBuilder.FileFormat;
import org.robotframework.ide.eclipse.main.plugin.RedImages;
import org.robotframework.ide.eclipse.main.plugin.RedPlugin;
import org.robotframework.ide.eclipse.main.plugin.model.RobotElement;
import org.robotframework.ide.eclipse.main.plugin.model.RobotElementChange;
import org.robotframework.ide.eclipse.main.plugin.model.RobotElementChange.Kind;
import org.robotframework.ide.eclipse.main.plugin.model.RobotFileInternalElement.ElementOpenMode;
import org.robotframework.ide.eclipse.main.plugin.model.RobotFolder;
import org.robotframework.ide.eclipse.main.plugin.model.RobotModelEvents;
import org.robotframework.ide.eclipse.main.plugin.model.RobotProject;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFile;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFileSection;
import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteStreamFile;
import org.robotframework.ide.eclipse.main.plugin.preferences.InstalledRobotsPreferencesPage;
import org.robotframework.ide.eclipse.main.plugin.project.ASuiteFileDescriber;
import org.robotframework.ide.eclipse.main.plugin.project.RobotSuiteFileDescriber;
import org.robotframework.ide.eclipse.main.plugin.project.TsvRobotSuiteFileDescriber;
import org.robotframework.ide.eclipse.main.plugin.project.build.RobotArtifactsValidator;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.cases.CasesEditorPart;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.dnd.RedClipboard;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.keywords.KeywordsEditorPart;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.settings.SettingsEditorPart;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.source.SuiteSourceEditor;
import org.robotframework.ide.eclipse.main.plugin.tableeditor.variables.VariablesEditorPart;
import org.robotframework.red.graphics.ImagesManager;
import org.robotframework.red.jface.dialogs.ErrorDialogWithLinkToPreferences;
import org.robotframework.red.swt.SwtThread;
public class RobotFormEditor extends FormEditor {
private static final String EDITOR_CONTEXT_ID = "org.robotframework.ide.eclipse.tableeditor.context";
public static final String ID = "org.robotframework.ide.tableditor";
private static IPartListener robotFormEditorPartListener;
private static IPerspectiveListener workbenchWindowPerspectiveListener;
private final List<IEditorPart> parts = newArrayList();
private RedClipboard clipboard;
private RobotSuiteFile suiteModel;
private boolean isEditable;
private SuiteFileValidationListener validationListener;
private final OnSaveLibrariesAutodiscoveryTrigger saveLibDiscoveryTrigger = new OnSaveLibrariesAutodiscoveryTrigger();
public RedClipboard getClipboard() {
return clipboard;
}
@Override
public void init(final IEditorSite site, final IEditorInput input) throws PartInitException {
try {
super.init(site, input);
clipboard = new RedClipboard(site.getShell().getDisplay());
validationListener = new SuiteFileValidationListener();
prepareEclipseContext();
validationListener.init();
ResourcesPlugin.getWorkspace().addResourceChangeListener(validationListener,
IResourceChangeEvent.POST_CHANGE);
site.getService(ICommandService.class).addExecutionListener(saveLibDiscoveryTrigger);
initRobotFormEditorPartListener(site.getPage());
initWorkbenchWindowPerspectiveListener(site.getWorkbenchWindow());
} catch (final IllegalRobotEditorInputException e) {
throw new PartInitException("Unable to open editor", e);
}
}
private void prepareEclipseContext() {
final IEclipseContext parentContext = getSite().getService(IEclipseContext.class);
final IEclipseContext eclipseContext = parentContext.getActiveLeaf();
eclipseContext.set(RobotEditorSources.SUITE_FILE_MODEL, new ContextFunction() {
@Override
public Object compute(final IEclipseContext context, final String contextKey) {
return provideSuiteModel();
}
});
eclipseContext.set(RedClipboard.class, clipboard);
eclipseContext.set(SuiteFileMarkersContainer.class, validationListener);
ContextInjectionFactory.inject(this, eclipseContext);
ContextInjectionFactory.inject(validationListener, eclipseContext);
}
private void initRobotFormEditorPartListener(final IWorkbenchPage page) {
if (robotFormEditorPartListener == null) {
robotFormEditorPartListener = new RobotFormEditorPartListener();
page.addPartListener(robotFormEditorPartListener);
}
}
private void initWorkbenchWindowPerspectiveListener(final IWorkbenchWindow window) {
if (workbenchWindowPerspectiveListener == null) {
workbenchWindowPerspectiveListener = new RobotFormEditorPerspectiveListener();
window.addPerspectiveListener(workbenchWindowPerspectiveListener);
}
}
@Override
protected void setInput(final IEditorInput input) {
if (input instanceof FileEditorInput) {
isEditable = !((FileEditorInput) input).getFile().isReadOnly();
setPartName(input.getName());
} else {
final IStorage storage = input.getAdapter(IStorage.class);
if (storage != null) {
isEditable = !storage.isReadOnly();
setPartName(storage.getName() + " [" + storage.getFullPath() + "]");
} else {
throw new IllegalRobotEditorInputException(
"Unable to open editor: unrecognized input of class: " + input.getClass().getName());
}
}
super.setInput(input);
}
public boolean isEditable() {
return isEditable;
}
@Override
protected void addPages() {
try {
prepareCommandsContext();
if (provideSuiteModel().isSuiteFile()) {
addEditorPart(new CasesEditorPart(), "Test Cases");
}
addEditorPart(new KeywordsEditorPart(), "Keywords");
addEditorPart(new SettingsEditorPart(), "Settings");
addEditorPart(new VariablesEditorPart(), "Variables");
addEditorPart(new SuiteSourceEditor(), "Source", ImagesManager.getImage(RedImages.getSourceImage()));
activateProperPage();
} catch (final Exception e) {
throw new RobotEditorOpeningException("Unable to initialize Suite editor", e);
}
}
private void activateProperPage() {
final String pageToActivate = RobotFormEditorActivePageSaver.getLastActivePageId(getEditorInput());
if (pageToActivate == null) {
final ElementOpenMode openMode = RedPlugin.getDefault().getPreferences().getElementOpenMode();
if (openMode == ElementOpenMode.OPEN_IN_SOURCE) {
setActivePart("");
} else {
activateFirstPage();
}
} else {
setActivePart(pageToActivate);
}
}
private void setActivePart(final String pageIdToActivate) {
for (int i = 0; i < getPageCount(); i++) {
final IEditorPart editorPart = getEditor(i);
if (editorPart instanceof ISectionEditorPart
&& ((ISectionEditorPart) editorPart).getId().equals(pageIdToActivate)) {
setActivePage(i);
return;
}
}
setActivePage(getPageCount() - 1);
}
private void addEditorPart(final IEditorPart editorPart, final String partName) throws PartInitException {
addEditorPart(editorPart, partName, editorPart.getTitleImage());
}
private void addEditorPart(final IEditorPart editorPart, final String partName, final Image image)
throws PartInitException {
parts.add(editorPart);
final IEclipseContext parentContext = getSite().getService(IEclipseContext.class);
final IEclipseContext eclipseContext = parentContext.getActiveLeaf();
ContextInjectionFactory.inject(editorPart, eclipseContext);
final int newVariablesPart = addPage(editorPart, getEditorInput());
setPageImage(newVariablesPart, image);
setPageText(newVariablesPart, partName);
}
private void prepareCommandsContext() {
final IContextService commandsContext = getSite().getService(IContextService.class);
commandsContext.activateContext(EDITOR_CONTEXT_ID);
}
@Override
public void doSave(final IProgressMonitor monitor) {
waitForPendingEditorJobs();
boolean shouldSave = true;
boolean shouldClose = false;
final RobotSuiteFile currentModel = provideSuiteModel();
if (!(getActiveEditor() instanceof SuiteSourceEditor)) {
updateSourceFromModel();
}
final int description = determineContentDescription();
if (currentModel.isSuiteFile() && description == IContentDescriber.INVALID) {
shouldSave = MessageDialog.openConfirm(getSite().getShell(), "File content mismatch",
"The file " + currentModel.getFile().getName() + " is a Suite file, but after "
+ "changes there is no Test Cases section. From now on this file will be recognized as "
+ "Resource file.\n\nClick OK to save and reopen editor or cancel saving");
shouldClose = true;
} else if (currentModel.isResourceFile() && description == IContentDescriber.VALID) {
shouldSave = MessageDialog.openConfirm(getSite().getShell(), "File content mismatch",
"The file " + currentModel.getFile().getName() + " is a Resource file, but after "
+ "changes there is a Test Cases section defined. From now on this file will be recognized "
+ "as Suite file.\n\nClick OK to save and reopen editor or cancel saving");
shouldClose = true;
}
if (!shouldSave) {
monitor.setCanceled(true);
return;
}
for (final IEditorPart dirtyEditor : getDirtyEditors()) {
dirtyEditor.doSave(monitor);
}
updateActivePage();
saveLibDiscoveryTrigger.startLibrariesAutoDiscoveryIfRequired(currentModel);
if (shouldClose) {
reopenEditor();
}
}
private int determineContentDescription() {
try {
final StringReader reader = new StringReader(getSourceEditor().getDocument().get());
final String fileExt = suiteModel.getFileExtension();
final ASuiteFileDescriber desc = fileExt != null && fileExt.toLowerCase().equals("tsv")
? new TsvRobotSuiteFileDescriber() : new RobotSuiteFileDescriber();
return desc.describe(reader, null);
} catch (final IOException e) {
// nothing to do
}
return IContentDescriber.INDETERMINATE;
}
private void waitForPendingEditorJobs() {
// jobs are sending model modification events, so it has to be done before dumping model to
// source
for (final IEditorPart part : parts) {
if (part instanceof ISectionEditorPart) {
((ISectionEditorPart) part).waitForPendingJobs();
}
}
}
private List<? extends IEditorPart> getDirtyEditors() {
final List<IEditorPart> dirtyEditors = newArrayList();
for (int i = 0; i < getPageCount(); i++) {
final IEditorPart editorPart = getEditor(i);
if (editorPart != null && editorPart.isDirty()) {
dirtyEditors.add(editorPart);
}
}
return dirtyEditors;
}
private void reopenEditor() {
close(false);
getSite().getShell().getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
final IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();
final IEditorDescriptor desc = editorRegistry.findEditor(RobotFormEditor.ID);
final IWorkbenchPage page = RobotFormEditor.this.getSite().getPage();
try {
page.openEditor(new FileEditorInput(suiteModel.getFile()), desc.getId());
} catch (final PartInitException e) {
throw new IllegalStateException("Unable to reopen editor", e);
}
}
});
}
@Override
public boolean isSaveAsAllowed() {
return false;
}
@Override
public void doSaveAs() {
// it is not allowed currently
}
@Override
public void dispose() {
final IEclipseContext parentContext = getSite().getService(IEclipseContext.class);
final IEclipseContext context = parentContext.getActiveLeaf();
ContextInjectionFactory.uninject(this, context);
ContextInjectionFactory.uninject(validationListener, context);
for (final IEditorPart part : parts) {
ContextInjectionFactory.uninject(part, context);
}
super.dispose();
ResourcesPlugin.getWorkspace().removeResourceChangeListener(validationListener);
getSite().getService(ICommandService.class).removeExecutionListener(saveLibDiscoveryTrigger);
clipboard.dispose();
suiteModel.dispose();
final IEventBroker eventBroker = PlatformUI.getWorkbench().getService(IEventBroker.class);
eventBroker.post(RobotModelEvents.SUITE_MODEL_DISPOSED, RobotElementChange.createChangedElement(suiteModel));
RobotArtifactsValidator.revalidate(suiteModel);
}
public SelectionLayerAccessor getSelectionLayerAccessor() {
final IEditorPart activeEditor = getActiveEditor();
if (activeEditor instanceof ISectionEditorPart) {
return ((ISectionEditorPart) activeEditor).getSelectionLayerAccessor();
}
return null;
}
public java.util.Optional<TreeLayerAccessor> getTreeLayerAccessor() {
final IEditorPart activeEditor = getActiveEditor();
if (activeEditor instanceof ISectionEditorPart) {
return ((ISectionEditorPart) activeEditor).getTreeLayerAccessor();
}
return java.util.Optional.empty();
}
public RobotSuiteFile provideSuiteModel() {
if (suiteModel != null) {
return suiteModel;
}
if (getEditorInput() instanceof FileEditorInput) {
suiteModel = RedPlugin.getModelManager().createSuiteFile(((FileEditorInput) getEditorInput()).getFile());
checkRuntimeEnvironment(suiteModel);
} else {
final IStorage storage = getEditorInput().getAdapter(IStorage.class);
try {
suiteModel = new RobotSuiteStreamFile(storage.getName(), storage.getContents(), storage.isReadOnly());
} catch (final CoreException e) {
throw new IllegalRobotEditorInputException("Unable to provide model for given input", e);
}
}
return suiteModel;
}
public SuiteSourceEditor getSourceEditor() {
for (int i = 0; i < getPageCount(); i++) {
final IEditorPart editorPart = getEditor(i);
if (editorPart instanceof SuiteSourceEditor) {
return (SuiteSourceEditor) editorPart;
}
}
return null;
}
@Override
protected void pageChange(final int newPageIndex) {
if (newPageIndex != getCurrentPage() && getActiveEditor() instanceof ISectionEditorPart) {
((ISectionEditorPart) getActiveEditor()).aboutToChangeToOtherPage();
}
super.pageChange(newPageIndex);
updateActivePage();
final IEditorPart activeEditor = getActiveEditor();
final String activePageId = activeEditor instanceof ISectionEditorPart
? ((ISectionEditorPart) activeEditor).getId() : "";
RobotFormEditorActivePageSaver.saveActivePageId(getEditorInput(), activePageId);
}
private void updateActivePage() {
if (getActiveEditor() instanceof ISectionEditorPart) {
final ISectionEditorPart page = (ISectionEditorPart) getActiveEditor();
page.updateOnActivation();
if (isDirty()) {
SwtThread.asyncExec(new Runnable() {
// there are some locking threads involved which results in blocking
// main thread for hundreds of milliseconds thus giving stops when switching
// from source part to some section editor part
@Override
public void run() {
getSourceEditor().disableReconcilation();
}
});
} else {
getSourceEditor().disableReconcilation();
}
} else if (getActiveEditor() instanceof SuiteSourceEditor) {
getSourceEditor().enableReconcilation();
updateSourceFromModel();
}
}
private void updateSourceFromModel() {
waitForPendingEditorJobs();
final SuiteSourceEditor editor = getSourceEditor();
if (!getDirtyEditors().isEmpty()) {
final IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput());
final RobotFile model = provideSuiteModel().getLinkedElement();
final RobotFileOutput currentRobotOutputFile = model.getParent();
final String separatorFromPreference = RedPlugin.getDefault()
.getPreferences()
.getSeparatorToUse(currentRobotOutputFile.getFileFormat() == FileFormat.TSV);
final DumpContext ctx = new DumpContext();
ctx.setPreferedSeparator(separatorFromPreference);
ctx.setDirtyFlag(true);
final RobotFileDumper dumper = new RobotFileDumper();
dumper.setContext(ctx);
final String content;
if (RedSystemProperties.shouldUseOldReparsedLinkMode()) {
content = dumper.dump(currentRobotOutputFile);
RobotFileOutput alreadyDumpedContent = suiteModel.getProject()
.getRobotParser()
.parseEditorContent(content, currentRobotOutputFile.getProcessedFile());
new TwoModelReferencesLinker().update(currentRobotOutputFile, alreadyDumpedContent);
alreadyDumpedContent = null;
} else {
final DumpedResult dumpResult = dumper.dumpToResultObject(currentRobotOutputFile);
content = dumpResult.newContent();
new QuickTokenListenerBaseTwoModelReferencesLinker().update(currentRobotOutputFile, dumpResult);
}
document.set(content);
}
}
public SuiteSourceEditor activateSourcePage() {
if (getActiveEditor() instanceof SuiteSourceEditor) {
return (SuiteSourceEditor) getActiveEditor();
}
final SuiteSourceEditor editor = getSourceEditor();
setActiveEditor(editor);
return editor;
}
public void activateFirstPage() {
setActivePage(0);
}
public ISectionEditorPart activatePage(final RobotSuiteFileSection section) {
int index = -1;
for (int i = 0; i < getPageCount(); i++) {
final IEditorPart part = (IEditorPart) pages.get(i);
if (part instanceof ISectionEditorPart && ((ISectionEditorPart) part).isPartFor(section)) {
index = i;
break;
}
}
if (index >= 0) {
setActivePage(index);
return (ISectionEditorPart) pages.get(index);
}
return null;
}
public static void activateSourcePageInActiveEditor(final IWorkbenchWindow window) {
if (window != null) {
final IEditorPart activeEditor = window.getActivePage().getActiveEditor();
if (activeEditor instanceof RobotFormEditor) {
((RobotFormEditor) activeEditor).activateSourcePage();
}
}
}
private void checkRuntimeEnvironment(final RobotSuiteFile suiteFile) {
if (suiteFile != null) {
final RobotProject robotProject = suiteFile.getProject();
if (robotProject != null) {
final RobotRuntimeEnvironment runtimeEnvironment = robotProject.getRuntimeEnvironment();
if (runtimeEnvironment == null || !runtimeEnvironment.isValidPythonInstallation()
|| !runtimeEnvironment.hasRobotInstalled()) {
final Shell shell = getSite().getShell();
if (shell != null && shell.isVisible()) {
new ErrorDialogWithLinkToPreferences(shell, "Runtime Environment Error",
"Unable to provide valid RED runtime environment. Check python/robot installation and set it in Preferences.",
InstalledRobotsPreferencesPage.ID, "Installed Robot Frameworks").open();
}
}
}
}
}
@SuppressWarnings("unchecked")
@Override
public Object getAdapter(@SuppressWarnings("rawtypes") final Class adapter) {
if (adapter == IContentOutlinePage.class) {
return new RobotOutlinePage(this, suiteModel);
} else if (adapter == IToggleBreakpointsTarget.class) {
return new ToggleBreakpointTarget();
}
return super.getAdapter(adapter);
}
@Inject
@Optional
private void closeEditorWhenResourceBecomesNotAvailable(
@UIEventTopic(RobotModelEvents.EXTERNAL_MODEL_CHANGE) final RobotElementChange change) {
if (change.getKind() == Kind.REMOVED && getEditorInput() instanceof FileEditorInput) {
final IFile file = ((FileEditorInput) getEditorInput()).getFile();
final RobotElement element = change.getElement();
if (element instanceof RobotProject && ((RobotProject) element).getProject().equals(file.getProject())) {
close(true);
} else if (element instanceof RobotFolder
&& ((RobotFolder) element).getFolder().getLocation().isPrefixOf(file.getLocation())) {
close(true);
} else if (element instanceof RobotSuiteFile && ((RobotSuiteFile) element).getFile().equals(file)) {
close(true);
}
}
}
@Inject
@Optional
private void whenSectionIsCreated(
@UIEventTopic(RobotModelEvents.ROBOT_SUITE_SECTION_ADDED) final RobotSuiteFile file) {
if (suiteModel == file) {
updateActivePage();
}
}
@Inject
@Optional
private void whenSectionIsRemoved(
@UIEventTopic(RobotModelEvents.ROBOT_SUITE_SECTION_REMOVED) final RobotSuiteFile file) {
if (suiteModel == file) {
updateActivePage();
}
}
private static class IllegalRobotEditorInputException extends RuntimeException {
private static final long serialVersionUID = 1L;
public IllegalRobotEditorInputException(final String message) {
super(message);
}
public IllegalRobotEditorInputException(final String message, final Throwable cause) {
super(message, cause);
}
}
public static class RobotEditorOpeningException extends RuntimeException {
private static final long serialVersionUID = 1L;
public RobotEditorOpeningException(final String message, final Throwable cause) {
super(message, cause);
}
}
}