blob: f9e348f2c74251ed4ffe8c5bcacfdbd407889ad0 [file] [log] [blame]
/*******************************************************************************
* 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 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* 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.model.application.ui.basic.MPart;
import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainer;
import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainerElement;
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.osbp.vaaclipse.addons.common.api.resource.ICustomizedModelResourceHandler;
import org.eclipse.osgi.service.datalocation.Location;
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);
EObject result = EcoreUtil.copy((EObject) perspective);
resource.unload();
return (MPerspective) result;
}
/* (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) {
// 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);
// remove dangling refs
removeDanglingRefs((MPerspective) 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;
}
private void removeDanglingRefs(MPerspective perspective) {
perspective.setParent(null);
for(MUIElement child:perspective.getChildren()) {
if(child instanceof MPartSashContainer) {
iterateSash((MPartSashContainer)child);
}
if(child instanceof MPart) {
clearPart((MPart)child);
}
}
}
private void iterateSash(MPartSashContainer sash) {
for(MPartSashContainerElement element:((MPartSashContainer)sash).getChildren()) {
if(element instanceof MPart) {
clearPart((MPart)element);
}
if(element instanceof MPartSashContainer) {
iterateSash((MPartSashContainer) element);
}
}
}
private void clearPart(MPart part) {
if(part.getBindingContexts() != null) {
part.getBindingContexts().clear();
}
if(part.getToolbar() != null) {
part.setToolbar(null);
}
}
}