/*- * Copyright © 2009 Diamond Light Source Ltd. * * This file is part of GDA. * * GDA is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License version 3 as published by the Free * Software Foundation. * * GDA 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 GDA. If not, see <http://www.gnu.org/licenses/>. */ package uk.ac.gda.richbeans.editors; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.List; import org.eclipse.core.commands.operations.IOperationHistory; import org.eclipse.core.commands.operations.IUndoContext; import org.eclipse.core.commands.operations.ObjectUndoContext; import org.eclipse.core.commands.operations.OperationHistoryFactory; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IStorage; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.ui.IActionBars; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IReusableEditor; import org.eclipse.ui.IStorageEditorInput; import org.eclipse.ui.PartInitException; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.ide.FileStoreEditorInput; import org.eclipse.ui.operations.LinearUndoViolationUserApprover; import org.eclipse.ui.operations.NonLocalUndoUserApprover; import org.eclipse.ui.operations.RedoActionHandler; import org.eclipse.ui.operations.UndoActionHandler; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.ui.part.MultiPageEditorPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.InputSource; import uk.ac.gda.common.rcp.util.EclipseUtils; import uk.ac.gda.common.rcp.util.IStorageUtils; import uk.ac.gda.richbeans.editors.xml.XMLBeanEditor; import uk.ac.gda.util.beans.xml.XMLHelpers; /** * This multipage editor is designed to be extended and the resulting class declared as an extension point. * <p> * If you extend this class, you must implement the createPart0() method and this must return a RichBeanEditor implementation. This class extending * RichBeanEditor can be created entirely automatically using RCP developer and exposing the relevant, correctly named, fields within RCP developer. */ public abstract class RichBeanMultiPageEditorPart extends MultiPageEditorPart implements DirtyContainer, IReusableEditor { private boolean undoRegistered = false; /** * Creates editor and sets a property that can be used to check * if the class is a RichBeanEditorPart. Useful for checking subclasses * registered as editors from other plugins. */ public RichBeanMultiPageEditorPart() { setPartProperty("RichBeanEditorPart", "true"); } // We must ensure that bundle urls are transformed to absolute or the sax parser does not work. static { XMLHelpers.setUrlResolver(EclipseUtils.getUrlResolver()); } protected static final Logger logger = LoggerFactory.getLogger(RichBeanMultiPageEditorPart.class); /** * The bean we are currently editing */ protected Object editingBean; /** * The UI editor being used normally to define the data */ protected RichBeanEditorPart richBeanEditor; /** * The XML editor on the second tab which can be used to edit * the xml directly */ protected XMLBeanEditor xmlEditor; /** * The path to the editing bean, may be null */ protected String path; /** * An action used to undo */ protected UndoActionHandler undoAction; /** * An action used to redo */ protected RedoActionHandler redoAction; /** * The context used in the undo stack */ protected IUndoContext context; /** * @return Class */ public abstract Class<?> getBeanClass(); /** * @return URL */ public abstract URL getMappingUrl(); /** * @return URL */ public abstract URL getSchemaUrl(); /** * Please implement this method to return your RichBeanEditorPart implementation. * @param path * @param editingBean * @return RichBeanEditorPart */ protected abstract RichBeanEditorPart getRichBeanEditorPart(String path, Object editingBean); @Override public void setInput(final IEditorInput input) { try { assignInput(input); createBean(); linkUI(); // Close all other editors editing this bean. // Currently only one editor for a given bean class may be open at a time. final IEditorReference[] refs = getSite().getPage().getEditorReferences(); for (int i = 0; i < refs.length; i++) { if (refs[i].getId().equals(this.getSite().getId())) { final IEditorPart part = refs[i].getEditor(false); if (part!=this) getSite().getPage().closeEditor(part, true); } } // TODO this method should fire a property change as specified in the javadoc, but when this is implemented it will need testing } catch (Throwable th){ logger.error("Error setting input for editor from input " + input.getName(), th); } } protected void createBean() { try { // Do not validate this bean on the read, user may not have added all parameters. if (this.getEditorInput() instanceof IStorageEditorInput) { IStorage storage = ((IStorageEditorInput)getEditorInput()).getStorage(); InputSource source = new InputSource(IStorageUtils.getContents(storage)); this.editingBean = XMLHelpers.createFromXML(getMappingUrl(), getBeanClass(), getSchemaUrl(), source, false); } else { this.editingBean = XMLHelpers.createFromXML(getMappingUrl(), getBeanClass(), getSchemaUrl(), path, false); } } catch (Throwable e) { // Class not found can come through here when the classes required for castor to load the bean from file are not present. throw new RuntimeException(e.getMessage(), e); } } protected void createUndoRedoActions() { this.context = new ObjectUndoContext(this); this.undoAction = new UndoActionHandler(getSite(), context); this.redoAction = new RedoActionHandler(getSite(), context); IOperationHistory history= OperationHistoryFactory.getOperationHistory(); history.addOperationApprover(new NonLocalUndoUserApprover(context, this, new Object [] { getEditorInput() }, Object.class)); history.addOperationApprover(new LinearUndoViolationUserApprover(context, this)); } protected void assignInput(IEditorInput input) { super.setInput(input); this.path = EclipseUtils.getFilePath(input); setPartName(input.getName()); if (richBeanEditor!=null) { try { pageChangeProcessing = false; richBeanEditor.setInput(input); richBeanEditor.setPath(path); } finally { pageChangeProcessing = true; } } if (xmlEditor!=null) { xmlEditor.setInput(input); } } /** * Called at creation and after the editor input has changed. If overriding, please call super.linkUI() */ protected void linkUI() { if (richBeanEditor!=null) { try { pageChangeProcessing = false; super.setActivePage(0); richBeanEditor.setEditingBean(editingBean); richBeanEditor.linkUI(false); } finally { pageChangeProcessing = true; } } if (xmlEditor!=null) { xmlEditor.setEditingBean(editingBean); } } @Override protected void createPages() { createUndoRedoActions(); try{ richBeanEditor = createPage0(); xmlEditor = createPage1(); } catch (Throwable th){ logger.error("Error creating pages for editor ",th); Assert.isTrue(false); // this is how org.eclipse.ui objects handle this sort of problem } } /** * Creates page 1 of the multi-page editor, * which allows you to change the font used in page 2. */ protected RichBeanEditorPart createPage0() { try { RichBeanEditorPart richBeanEditor = getRichBeanEditorPart(path, editingBean); richBeanEditor.setUndoableContext(context); int index = addPage(richBeanEditor, getEditorInput()); richBeanEditor.linkUI(false); setPageText(index, richBeanEditor.getRichEditorTabText()); return richBeanEditor; } catch (PartInitException e) { ErrorDialog.openError( getSite().getShell(), "Error creating editor for "+getClass().getName(), null, e.getStatus()); } return null; } /** * Creates page 1 of the multi-page editor, * which contains a text editor. */ protected XMLBeanEditor createPage1() { try { XMLBeanEditor xmlEditor = new XMLBeanEditor(this, getMappingUrl(), getSchemaUrl(), editingBean); int index = addPage(xmlEditor, getEditorInput()); setPageText(index, "XML"); return xmlEditor; } catch (PartInitException e) { logger.error(e.getMessage(), e); ErrorDialog.openError( getSite().getShell(), "Error creating text editor for "+getClass().getName(), null, e.getStatus()); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } private boolean allowDirtyUpdates = true; private boolean isDirty = false; @Override public boolean isDirty() { if (path == null) return false; return isDirty; } @Override public void setDirty(boolean isDirty) { if (!allowDirtyUpdates) return; this.isDirty = isDirty; firePropertyChange(IEditorPart.PROP_DIRTY); } private boolean pageChangeProcessing = false; @Override protected void initializePageSwitching() { super.initializePageSwitching(); pageChangeProcessing = true; } @Override protected void pageChange(int newPageIndex) { if (!pageChangeProcessing) { if (!undoRegistered) { getSite().getShell().getDisplay().asyncExec(new Runnable() { // You have to do this on an asyncExec. @Override public void run() { setUIUndoRedo(); } }); } super.pageChange(newPageIndex); return; } // If first page going to then regenerate bean. if (newPageIndex == 1) { try { setXMLUndoRedo(); richBeanEditor.uiToBean(); xmlEditor.beanToXML(getPrivateXMLFields()); } catch (Exception e) { e.printStackTrace(); } } else if (newPageIndex == 0) { try { setUIUndoRedo(); xmlEditor.xmlToBean(); } catch (Exception e) { // XML Cannot be parsed. try { logger.debug("XML Validation stack trace.", e); final boolean isOk = MessageDialog.openQuestion(getSite().getShell(), "XML Validation Error", "The XML is not valid.\n\nDo you want to continue to change screen and lose your edits?\n\n"+ "Error message:\n"+XMLBeanEditor.getSantitizedExceptionMessage(e.getMessage())); if (!isOk) { try { pageChangeProcessing = false; super.setActivePage(1); } finally { pageChangeProcessing = true; } } } catch (Throwable ne) { ne.printStackTrace(); } } try { allowDirtyUpdates = false; richBeanEditor.linkUI(true); } catch (Exception e) { e.printStackTrace(); } finally { allowDirtyUpdates = true; } } super.pageChange(newPageIndex); } protected void setUIUndoRedo() { final IActionBars actionBars = getEditorSite().getActionBars(); actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), undoAction); actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), redoAction); actionBars.updateActionBars(); OperationHistoryFactory.getOperationHistory().dispose(context, true, true, false); undoRegistered = true; } protected void setXMLUndoRedo() { final IActionBars actionBars = getEditorSite().getActionBars(); actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(), xmlEditor.getAction(ActionFactory.UNDO.getId())); actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(), xmlEditor.getAction(ActionFactory.REDO.getId())); actionBars.updateActionBars(); OperationHistoryFactory.getOperationHistory().dispose(context, true, true, false); undoRegistered = true; } /** * NOTE Can save both to this project, in which case add as IFile or * to any other location, in which case add as external resource. */ @Override public void doSaveAs() { final IFile currentiFile = EclipseUtils.getIFile(getEditorInput()); final IFolder folder = (currentiFile != null) ? (IFolder) currentiFile.getParent() : null; final FileDialog dialog = new FileDialog(getSite().getShell(), SWT.SAVE); dialog.setText("Save as XML"); dialog.setFilterExtensions(new String[] { "*.xml" }); final File currentFile = new File(this.path); dialog.setFilterPath(currentFile.getParentFile().getAbsolutePath()); dialog.setFileName(currentFile.getName()); String newFile = dialog.open(); if (newFile!=null) { if (!newFile.endsWith(".xml")) newFile = newFile+".xml"; newFile = validateFileName(newFile); if (newFile==null) return; final File file = new File(newFile); if (file.exists()) { final boolean ovr = MessageDialog.openQuestion(getSite().getShell(), "Confirm File Overwrite", "The file '"+file.getName()+"' exists in '"+file.getParentFile().getName()+"'.\n\n"+ "Would you like to overwrite it?"); if (!ovr) return; } try { file.createNewFile(); } catch (IOException e) { MessageDialog.openError(getSite().getShell(), "Cannot save file.", "The file '"+file.getName()+"' cannot be created in '"+file.getParentFile().getName()+"'.\n\n"+ e.getMessage()); return; } try { if (!confirmFileNameChange(currentFile, file)) { file.delete(); return; } } catch (Exception ne) { logger.error("Cannot confirm name change", ne); return; } IEditorInput input; if (folder!=null&&folder.getLocation().toFile().equals(file.getParentFile())) { final IFile ifile = folder.getFile(file.getName()); try { ifile.refreshLocal(IResource.DEPTH_ZERO, null); } catch (CoreException e) { logger.error("Cannot refresh "+ifile, e); } input = new FileEditorInput(ifile); } else { input = new FileStoreEditorInput(EFS.getLocalFileSystem().fromLocalFile(file)); } assignInput(input); doSave(new NullProgressMonitor()); setDirty(false); } } @Override public void doSave(IProgressMonitor monitor) { final int index = getActivePage(); if (index == 0) { richBeanEditor.doSave(monitor); } else if (index == 1) { xmlEditor.doSave(monitor); } } /** * Please override if work should be done when save as changes the active file name. * @param oldName * @param newName */ @SuppressWarnings("unused") protected boolean confirmFileNameChange(final File oldName, final File newName) throws Exception { return true; } /** * Optional file name validation. Returns null if name invalid. */ protected String validateFileName(final String newFile) { return newFile; } @Override public boolean isSaveAsAllowed() { return true; } /** * * @return the editor if it is already in existance. */ public RichBeanEditorPart getRichBeanEditor() { return richBeanEditor; } /** * Override to provide a list of field names which * cannot be edited in the XML editor. * @return fields. */ public List<String> getPrivateXMLFields() { return null; } @Override public void dispose() { editingBean = null; if (richBeanEditor != null) { richBeanEditor.dispose(); richBeanEditor = null; } super.dispose(); } }