| /******************************************************************************* |
| * Copyright (c) 2009, 2014 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Tristan Hume - <trishume@gmail.com> - |
| * Fix for Bug 2369 [Workbench] Would like to be able to save workspace without exiting |
| * Implemented workbench auto-save to correctly restore state in case of crash. |
| * Terry Parker <tparker@google.com> - Bug 416673 |
| * Florian Pirchner - adjusted for Vaaclipse usecases - uses different ModelAssembler and ModelUtils |
| ******************************************************************************/ |
| |
| package org.eclipse.osbp.vaaclipse.addons.common.resource; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.annotation.PostConstruct; |
| import javax.inject.Inject; |
| import javax.inject.Named; |
| |
| import org.eclipse.core.internal.runtime.PlatformURLPluginConnection; |
| import org.eclipse.core.runtime.URIUtil; |
| 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.log.Logger; |
| import org.eclipse.e4.ui.internal.workbench.CommandLineOptionModelProcessor; |
| import org.eclipse.e4.ui.internal.workbench.E4Workbench; |
| import org.eclipse.e4.ui.internal.workbench.E4XMIResource; |
| import org.eclipse.e4.ui.internal.workbench.URIHelper; |
| import org.eclipse.e4.ui.model.application.MApplication; |
| import org.eclipse.e4.ui.model.application.MApplicationElement; |
| import org.eclipse.e4.ui.model.application.ui.MUIElement; |
| import org.eclipse.e4.ui.model.application.ui.advanced.MPerspective; |
| import org.eclipse.e4.ui.workbench.IWorkbench; |
| import org.eclipse.emf.common.util.TreeIterator; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EStructuralFeature.Setting; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.emf.ecore.resource.URIConverter; |
| import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| import org.eclipse.emf.ecore.xmi.XMIResource; |
| import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl; |
| import org.eclipse.osgi.service.datalocation.Location; |
| import org.eclipse.osbp.vaaclipse.addons.common.api.resource.ICustomizedModelResourceHandler; |
| import org.osgi.framework.Bundle; |
| |
| /** |
| * A common implementation for the ResourceHandler. This resource handler |
| * automatically loads user defined perspectives from a different model. So user |
| * can save, create and delete their own perspectives. |
| */ |
| @SuppressWarnings("restriction") |
| public class ResourceHandler implements ICustomizedModelResourceHandler { |
| |
| /** The resource set. */ |
| @Inject |
| private ResourceSet resourceSet; |
| |
| /** The resource. */ |
| private E4XMIResource resource; |
| |
| /** The logger. */ |
| @Inject |
| private Logger logger; |
| |
| /** The user id. */ |
| @Inject |
| @Named("userId") |
| @Optional |
| String userId; |
| |
| /** The context. */ |
| @Inject |
| private IEclipseContext context; |
| |
| /** The application definition instance. */ |
| @Inject |
| @Named(E4Workbench.INITIAL_WORKBENCH_MODEL_URI) |
| private URI applicationDefinitionInstance; |
| |
| /** The instance location. */ |
| @Inject |
| @Optional |
| @Named(E4Workbench.INSTANCE_LOCATION) |
| private Location instanceLocation; |
| |
| /** |
| * Dictates whether the model should be stored using EMF or with the merging |
| * algorithm. https://bugs.eclipse.org/bugs/show_bug.cgi?id=295524 |
| * |
| */ |
| final private boolean saveAndRestore; |
| |
| /** The clear persisted state. */ |
| private boolean clearPersistedState; |
| |
| /** |
| * Constructor. |
| * |
| * @param saveAndRestore |
| * the save and restore |
| * @param clearPersistedState |
| * the clear persisted state |
| */ |
| @Inject |
| public ResourceHandler( |
| @Named(IWorkbench.PERSIST_STATE) boolean saveAndRestore, |
| @Named(IWorkbench.CLEAR_PERSISTED_STATE) boolean clearPersistedState) { |
| this.saveAndRestore = saveAndRestore; |
| this.clearPersistedState = clearPersistedState; |
| } |
| |
| /** |
| * Inits the. |
| */ |
| @PostConstruct |
| void init() { |
| |
| } |
| |
| /** |
| * Checks for top level windows. |
| * |
| * @return {@code true} if the current application model has top-level |
| * windows. |
| */ |
| public boolean hasTopLevelWindows() { |
| return hasTopLevelWindows(resource); |
| } |
| |
| /** |
| * Checks for top level windows. |
| * |
| * @param applicationResource |
| * the application resource |
| * @return {@code true} if the specified application model has top-level |
| * windows. |
| */ |
| private boolean hasTopLevelWindows(Resource applicationResource) { |
| if (applicationResource == null |
| || applicationResource.getContents() == null) { |
| // If the application resource doesn't exist or has no contents, |
| // then it has no |
| // top-level windows (and we are in an error state). |
| return false; |
| } |
| MApplication application = (MApplication) applicationResource |
| .getContents().get(0); |
| return !application.getChildren().isEmpty(); |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.e4.ui.workbench.IModelResourceHandler#loadMostRecentModel() |
| */ |
| @Override |
| public Resource loadMostRecentModel() { |
| File workbenchData = null; |
| URI restoreLocation = null; |
| |
| if (saveAndRestore) { |
| workbenchData = getWorkbenchSaveLocation(); |
| restoreLocation = URI |
| .createFileURI(workbenchData.getAbsolutePath()); |
| } |
| |
| if (clearPersistedState && workbenchData != null |
| && workbenchData.exists()) { |
| workbenchData.delete(); |
| } |
| |
| // last stored time-stamp |
| long restoreLastModified = restoreLocation == null ? 0L : new File( |
| restoreLocation.toFileString()).lastModified(); |
| |
| // See bug 380663, bug 381219 |
| // long lastApplicationModification = getLastApplicationModification(); |
| // boolean restore = restoreLastModified > lastApplicationModification; |
| boolean restore = restoreLastModified > 0; |
| boolean initialModel; |
| |
| resource = null; |
| if (restore && saveAndRestore) { |
| resource = (E4XMIResource) loadResource(restoreLocation); |
| // If the saved model does not have any top-level windows, Eclipse |
| // will exit |
| // immediately, so throw out the persisted state and reinitialize |
| // with the defaults. |
| if (!hasTopLevelWindows(resource)) { |
| if (logger != null) { |
| logger.error( |
| new Exception(), // log a stack trace to help debug |
| // the corruption |
| "The persisted workbench has no top-level windows, so reinitializing with defaults."); //$NON-NLS-1$ |
| } |
| resource = null; |
| } |
| } |
| if (resource == null) { |
| Resource appResource = loadResource(applicationDefinitionInstance); |
| if (!hasTopLevelWindows(appResource) && logger != null) { |
| logger.error( |
| new Exception(), // log a stack trace to help debug the |
| // corruption |
| "Initializing from the application definition instance yields no top-level windows! " //$NON-NLS-1$ |
| + "Continuing execution, but the missing windows may cause other initialization failures."); //$NON-NLS-1$ |
| } |
| MApplication theApp = (MApplication) appResource.getContents().get( |
| 0); |
| resource = (E4XMIResource) createResourceWithApp(theApp); |
| context.set(E4Workbench.NO_SAVED_MODEL_FOUND, Boolean.TRUE); |
| initialModel = true; |
| |
| // remove the appResource since it was damaged by |
| // #createResourceWithApp() |
| appResource.unload(); |
| resourceSet.getResources().remove(appResource); |
| } else { |
| initialModel = false; |
| } |
| |
| // create an URI mapping from the application resource to the user |
| // resource |
| resourceSet.getURIConverter().getURIMap() |
| .put(applicationDefinitionInstance, resource.getURI()); |
| // create a relative URI mapping for the last segment of the resource |
| // URI to its full qualified URI. |
| // Necessary to load split models like an exported perspective |
| resourceSet |
| .getURIConverter() |
| .getURIMap() |
| .put(URI.createURI(resource.getURI().lastSegment()), |
| resource.getURI()); |
| |
| // Add model items described in the model extension point |
| // This has to be done before commands are put into the context |
| MApplication appElement = (MApplication) resource.getContents().get(0); |
| Map<String, String> weights = getWeights(resource, appElement); |
| this.context.set(MApplication.class, appElement); |
| VaaclipseModelAssembler contribProcessor = ContextInjectionFactory |
| .make(VaaclipseModelAssembler.class, context); |
| contribProcessor.processModel(initialModel); |
| |
| // load the customized models |
| // |
| SystemuserModelHandler handler = ContextInjectionFactory.make( |
| SystemuserModelHandler.class, context); |
| handler.mergeFragment(); |
| |
| if (!clearPersistedState) { |
| CommandLineOptionModelProcessor processor = ContextInjectionFactory |
| .make(CommandLineOptionModelProcessor.class, context); |
| processor.process(); |
| } |
| |
| // apply the weights |
| // |
| for (Map.Entry<String, String> entry : weights.entrySet()) { |
| MUIElement element = (MUIElement) resource.getIDToEObjectMap().get( |
| entry.getKey()); |
| if (element != null) { |
| element.setContainerData(entry.getValue()); |
| } |
| } |
| |
| return resource; |
| } |
| |
| /** |
| * Returns the layout weights for later restore. |
| * |
| * @param resource |
| * the resource |
| * @param appElement |
| * the app element |
| * @return the weights |
| */ |
| private Map<String, String> getWeights(E4XMIResource resource, |
| MApplication appElement) { |
| |
| Map<String, String> result = new HashMap<String, String>(); |
| TreeIterator<EObject> treeIt = EcoreUtil.getAllContents( |
| (EObject) appElement, true); |
| while (treeIt.hasNext()) { |
| EObject eObj = treeIt.next(); |
| if (eObj instanceof MUIElement) { |
| result.put(resource.getID(eObj), |
| ((MUIElement) eObj).getContainerData()); |
| } |
| } |
| return result; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.e4.ui.workbench.IModelResourceHandler#save() |
| */ |
| @Override |
| public void save() throws IOException { |
| if (saveAndRestore) { |
| MApplication mApp = (MApplication) resource.getContents().get(0); |
| Resource toSave = createResourceForSave(mApp); |
| toSave.save(null); |
| // resource.save(null); |
| } |
| } |
| |
| /** |
| * Creates a resource with an app Model, used for saving copies of the main |
| * app model. |
| * |
| * @param theApp |
| * the application model to add to the resource |
| * @return a resource with a proper save path with the model as contents |
| */ |
| @Override |
| public Resource createResourceWithApp(MApplication theApp) { |
| E4XMIResource appResource = (E4XMIResource) ((EObject) theApp) |
| .eResource(); |
| Map<EObject, String> idMap = new HashMap<EObject, String>( |
| appResource.getEObjectToIDMap()); |
| |
| E4XMIResource res = (E4XMIResource) createResource(); |
| // Removes the app model from the appResource |
| res.getContents().add((EObject) theApp); |
| |
| // replace the IDs for the different EObjects by their original values. |
| // Otherwise proxies can not be resolved. |
| TreeIterator<EObject> it = EcoreUtil.getAllContents(res.getContents()); |
| while (it.hasNext()) { |
| EObject o = it.next(); |
| res.setID(o, idMap.get(o)); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * Creates a resource for saving issues. The content MApplication will be |
| * prepared. All the elements tagged with "vaaclipse-transient" will be |
| * removed from the model before saving. |
| * |
| * @param theApp |
| * the application model to add to the resource |
| * @return a resource with a proper save path with the model as contents |
| */ |
| protected Resource createResourceForSave(MApplication theApp) { |
| E4XMIResource originalResource = (E4XMIResource) ((EObject) theApp) |
| .eResource(); |
| |
| // create a new resource with the copied application model and same URI |
| // |
| E4XMIResource toSaveResource = (E4XMIResource) createResource(); |
| toSaveResource.setURI(originalResource.getURI()); |
| |
| // copy the eObject |
| // |
| EcoreUtil.Copier copier = new EcoreUtil.Copier(); |
| EObject copyApp = copier.copy((EObject) theApp); |
| copier.copyReferences(); |
| |
| // Removes all transient objects |
| // |
| removeTransientObjects(copyApp); |
| |
| // set the copy to the new resource |
| // |
| toSaveResource.getContents().add(copyApp); |
| |
| // the xml ids to be transferred to the new resource |
| // |
| Map<EObject, String> idMap = new HashMap<EObject, String>( |
| originalResource.getEObjectToIDMap()); |
| |
| // replace the IDs for the different EObjects by their original values. |
| // Otherwise proxies can not be resolved. |
| // |
| TreeIterator<EObject> originalIterator = EcoreUtil |
| .getAllContents(originalResource.getContents()); |
| while (originalIterator.hasNext()) { |
| EObject original = originalIterator.next(); |
| |
| String id = idMap.get(original); |
| // use the copier to find the copied element for the original |
| toSaveResource.setID(copier.get(original), id); |
| } |
| |
| return toSaveResource; |
| } |
| |
| /** |
| * Remove all transient objects and its cross references. |
| * |
| * @param copyApp |
| * the copy app |
| */ |
| private void removeTransientObjects(EObject copyApp) { |
| List<EObject> toRemove = new ArrayList<>(); |
| TreeIterator<EObject> iterator = EcoreUtil.getAllContents(copyApp, |
| false); |
| while (iterator.hasNext()) { |
| EObject eObject = iterator.next(); |
| if (eObject instanceof MUIElement) { |
| MUIElement uiElement = (MUIElement) eObject; |
| if (uiElement.getTags().contains("vaaclipse-transient")) { |
| toRemove.add(eObject); |
| } |
| } |
| } |
| |
| for (EObject eObject : toRemove) { |
| // remove the eObject |
| EcoreUtil.remove(eObject); |
| |
| // remove all cross references |
| Collection<Setting> usages = EcoreUtil.UsageCrossReferencer.find( |
| eObject, copyApp); |
| for (Setting setting : usages) { |
| setting.unset(); |
| } |
| } |
| |
| } |
| |
| /** |
| * Creates the resource. |
| * |
| * @return the resource |
| */ |
| private Resource createResource() { |
| // if (saveAndRestore) { |
| URI saveLocation = URI.createFileURI(getWorkbenchSaveLocation() |
| .getAbsolutePath()); |
| return resourceSet.createResource(saveLocation); |
| // } |
| // return resourceSet.createResource(URI.createURI("workbench.xmi")); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Gets the workbench save location. |
| * |
| * @return the workbench save location |
| */ |
| private File getWorkbenchSaveLocation() { |
| File workbenchData = new File(getBaseLocation(), String.format( |
| "%s_workbench.xmi", userId)); //$NON-NLS-1$ |
| return workbenchData; |
| } |
| |
| /** |
| * Gets the base location. |
| * |
| * @return the base location |
| */ |
| private File getBaseLocation() { |
| File baseLocation; |
| try { |
| baseLocation = new File(URIUtil.toURI(instanceLocation.getURL())); |
| } catch (URISyntaxException e) { |
| throw new RuntimeException(e); |
| } |
| baseLocation = new File(baseLocation, ".metadata"); //$NON-NLS-1$ |
| baseLocation = new File(baseLocation, ".plugins"); //$NON-NLS-1$ |
| baseLocation = new File(baseLocation, "org.eclipse.e4.workbench"); //$NON-NLS-1$ |
| return baseLocation; |
| } |
| |
| /** |
| * Load resource. |
| * |
| * @param uri |
| * the uri |
| * @return the resource |
| */ |
| // Ensures that even models with error are loaded! |
| private Resource loadResource(URI uri) { |
| Resource resource; |
| try { |
| resource = getResource(uri); |
| } catch (Exception e) { |
| // TODO We could use diagnostics for better analyzing the error |
| logger.error(e, "Unable to load resource " + uri.toString()); //$NON-NLS-1$ |
| return null; |
| } |
| |
| // TODO once we switch from deltas, we only need this once on the |
| // default model? |
| String contributorURI = URIHelper.EMFtoPlatform(uri); |
| if (contributorURI != null) { |
| TreeIterator<EObject> it = EcoreUtil.getAllContents(resource |
| .getContents()); |
| while (it.hasNext()) { |
| EObject o = it.next(); |
| if (o instanceof MApplicationElement) { |
| ((MApplicationElement) o).setContributorURI(contributorURI); |
| } |
| } |
| } |
| return resource; |
| } |
| |
| /** |
| * Gets the resource. |
| * |
| * @param uri |
| * the uri |
| * @return the resource |
| * @throws Exception |
| * the exception |
| */ |
| private Resource getResource(URI uri) throws Exception { |
| Resource resource; |
| if (saveAndRestore) { |
| resource = resourceSet.getResource(uri, true); |
| } else { |
| // Workaround for java.lang.IllegalStateException: No instance data |
| // can be specified |
| // thrown by |
| // org.eclipse.core.internal.runtime.DataArea.assertLocationInitialized |
| // The DataArea.assertLocationInitialized is called by |
| // ResourceSetImpl.getResource(URI, |
| // boolean) |
| resource = resourceSet.createResource(uri); |
| resource.load(new URL(uri.toString()).openStream(), |
| resourceSet.getLoadOptions()); |
| } |
| |
| return resource; |
| } |
| |
| /** |
| * Gets the last application modification. |
| * |
| * @return the last application modification |
| */ |
| protected long getLastApplicationModification() { |
| long appLastModified = 0L; |
| ResourceSetImpl resourceSetImpl = new ResourceSetImpl(); |
| |
| Map<String, ?> attributes = resourceSetImpl |
| .getURIConverter() |
| .getAttributes( |
| applicationDefinitionInstance, |
| Collections |
| .singletonMap( |
| URIConverter.OPTION_REQUESTED_ATTRIBUTES, |
| Collections |
| .singleton(URIConverter.ATTRIBUTE_TIME_STAMP))); |
| |
| Object timestamp = attributes.get(URIConverter.ATTRIBUTE_TIME_STAMP); |
| if (timestamp instanceof Long) { |
| appLastModified = ((Long) timestamp).longValue(); |
| } else if (applicationDefinitionInstance.isPlatformPlugin()) { |
| try { |
| java.net.URL url = new java.net.URL( |
| applicationDefinitionInstance.toString()); |
| // can't just use 'url.openConnection()' as it usually returns a |
| // PlatformURLPluginConnection which doesn't expose the |
| // last-modification time. So we try to resolve the file through |
| // the bundle to obtain a BundleURLConnection instead. |
| Object[] obj = PlatformURLPluginConnection.parse(url.getFile() |
| .trim(), url); |
| Bundle b = (Bundle) obj[0]; |
| // first try to resolve as an bundle file entry, then as a |
| // resource using |
| // the bundle's classpath |
| java.net.URL resolved = b.getEntry((String) obj[1]); |
| if (resolved == null) { |
| resolved = b.getResource((String) obj[1]); |
| } |
| if (resolved != null) { |
| URLConnection openConnection = resolved.openConnection(); |
| appLastModified = openConnection.getLastModified(); |
| } |
| } catch (Exception e) { |
| // ignore |
| } |
| } |
| |
| return appLastModified; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.osbp.vaaclipse.addons.common.api.resource.ICustomizedModelResourceHandler#loadPerspective(java.io.InputStream) |
| */ |
| @Override |
| public MPerspective loadPerspective(InputStream stream) throws IOException { |
| E4XMIResource resource = new E4XMIResource(URI.createURI("perspective")); |
| resource.load(stream, null); |
| final MPerspective perspective = (MPerspective) resource.getContents() |
| .get(0); |
| resource.unload(); |
| return perspective; |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.osbp.vaaclipse.addons.common.api.resource.ICustomizedModelResourceHandler#savePerspective(org.eclipse.e4.ui.model.application.ui.advanced.MPerspective) |
| */ |
| @Override |
| public void savePerspective(MPerspective perspective) throws IOException { |
| Resource resource = createResourceForSave(perspective); |
| resource.save(null); |
| } |
| |
| /* (non-Javadoc) |
| * @see org.eclipse.osbp.vaaclipse.addons.common.api.resource.ICustomizedModelResourceHandler#savePerspective(java.io.OutputStream, org.eclipse.e4.ui.model.application.ui.advanced.MPerspective) |
| */ |
| @Override |
| public void savePerspective(OutputStream stream, MPerspective perspective) throws IOException { |
| Resource resource = createResourceForSave(perspective); |
| resource.save(stream, null); |
| } |
| |
| /** |
| * Creates a resource for saving issues. The content {@link MPerspective} |
| * will be prepared. All the elements tagged with "vaaclipse-transient" will |
| * be removed from the model before saving. |
| * |
| * @param perspective |
| * the perspective |
| * @return a resource with a proper save path with the model as contents |
| */ |
| protected Resource createResourceForSave(MPerspective perspective) { |
| E4XMIResource originalResource = (E4XMIResource) ((EObject) perspective) |
| .eResource(); |
| |
| // create a new resource with the copied application model and same URI |
| // |
| URI saveLocation = URI |
| .createFileURI(getWorkbenchSaveLocation().getAbsolutePath()) |
| .trimSegments(1).appendSegment(perspective.getElementId()) |
| .appendFileExtension("perspective"); |
| XMIResource toSaveResource = new XMIResourceImpl(saveLocation); |
| |
| // copy the eObject |
| // |
| EcoreUtil.Copier copier = new EcoreUtil.Copier(); |
| EObject copyPerspective = copier.copy((EObject) perspective); |
| copier.copyReferences(); |
| |
| // Removes all transient objects |
| // |
| removeTransientObjects(copyPerspective); |
| |
| // set the copy to the new resource |
| // |
| toSaveResource.getContents().add(copyPerspective); |
| |
| // // the xml ids to be transferred to the new resource |
| // // |
| // Map<EObject, String> idMap = new HashMap<EObject, String>( |
| // originalResource.getEObjectToIDMap()); |
| // |
| // // replace the IDs for the different EObjects by their original |
| // values. |
| // // Otherwise proxies can not be resolved. |
| // // |
| // TreeIterator<EObject> originalIterator = EcoreUtil |
| // .getAllContents(originalResource.getContents()); |
| // while (originalIterator.hasNext()) { |
| // EObject original = originalIterator.next(); |
| // |
| // String id = idMap.get(original); |
| // // use the copier to find the copied element for the original |
| // toSaveResource.setID(copier.get(original), id); |
| // } |
| // |
| // // // clear all internal ids |
| // // toSaveResource.getEObjectToIDMap().clear(); |
| // // if (toSaveResource.getIntrinsicIDToEObjectMap() != null) { |
| // // toSaveResource.getIntrinsicIDToEObjectMap().clear(); |
| // // } |
| |
| return toSaveResource; |
| } |
| } |