| /******************************************************************************* |
| * Copyright (c) 2010, 2018 BestSolution.at and others. |
| * |
| * 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: |
| * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 430075, 430080, 431464, 433336, 472654 |
| * René Brandstetter - Bug 419749 |
| * Brian de Alwis (MTI) - Bug 433053 |
| * Alexandra Buzila - Refactoring, Bug 475934 |
| ******************************************************************************/ |
| |
| package org.eclipse.e4.ui.internal.workbench; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import javax.inject.Inject; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IContributor; |
| import org.eclipse.core.runtime.IExtension; |
| import org.eclipse.core.runtime.IExtensionPoint; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.e4.core.contexts.ContextInjectionFactory; |
| import org.eclipse.e4.core.contexts.EclipseContextFactory; |
| import org.eclipse.e4.core.contexts.IEclipseContext; |
| import org.eclipse.e4.core.di.annotations.Execute; |
| import org.eclipse.e4.core.services.contributions.IContributionFactory; |
| import org.eclipse.e4.core.services.log.Logger; |
| import org.eclipse.e4.ui.model.application.MApplication; |
| import org.eclipse.e4.ui.model.application.MApplicationElement; |
| import org.eclipse.e4.ui.model.fragment.MModelFragment; |
| import org.eclipse.e4.ui.model.fragment.MModelFragments; |
| import org.eclipse.e4.ui.model.fragment.MStringModelFragment; |
| import org.eclipse.e4.ui.model.fragment.impl.FragmentPackageImpl; |
| import org.eclipse.e4.ui.model.internal.ModelUtils; |
| import org.eclipse.emf.common.util.Diagnostic; |
| import org.eclipse.emf.common.util.EList; |
| 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; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.emf.ecore.util.Diagnostician; |
| import org.eclipse.emf.ecore.util.EContentsEList; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| |
| /** |
| * The ModelAssembler is responsible for adding {@link MModelFragment fragments} |
| * and {@link MApplicationElement} imports to the application model and running |
| * pre- and post-processors on the model. |
| */ |
| public class ModelAssembler { |
| |
| private class Bucket { |
| SortedSet<ModelFragmentWrapper> wrapper = new TreeSet<>(new ModelFragmentComparator(application)); |
| Bucket dependentOn; |
| Set<Bucket> dependencies = new LinkedHashSet<>(); |
| Set<String> containedElementIds = new LinkedHashSet<>(); |
| } |
| |
| @Inject |
| private Logger logger; |
| |
| @Inject |
| private MApplication application; |
| |
| @Inject |
| private IEclipseContext context; |
| |
| @Inject |
| private IExtensionRegistry registry; |
| |
| private static final String EXTENSION_POINT_ID = "org.eclipse.e4.workbench.model"; //$NON-NLS-1$ |
| |
| private static final String INITIAL = "initial"; //$NON-NLS-1$ |
| private static final String NOTEXISTS = "notexists"; //$NON-NLS-1$ |
| |
| /** |
| * Processes the application model. This will run pre-processors, process the |
| * fragments, resolve imports and run post-processors, in this order. <br> |
| * The <strong>org.eclipse.e4.workbench.model</strong> extension point will be |
| * used to retrieve the contributed fragments (with imports) and processors.<br> |
| * Extension points will be sorted based on the dependencies of their |
| * contributors. |
| * |
| * @param initial <code>true</code> if running from a non-persisted state |
| */ |
| public void processModel(boolean initial) { |
| IExtensionPoint extPoint = registry.getExtensionPoint(EXTENSION_POINT_ID); |
| IExtension[] extensions = new ExtensionsSort().sort(extPoint.getExtensions()); |
| |
| // run processors which are marked to run before fragments |
| runProcessors(extensions, initial, false); |
| // process fragments (and resolve imports) |
| processFragments(extensions, initial); |
| // run processors which are marked to run after fragments |
| runProcessors(extensions, initial, true); |
| } |
| |
| /** |
| * Adds the {@link MApplicationElement model elements} contributed by the |
| * {@link IExtension extensions} to the {@link MApplication application model}. |
| * |
| * @param extensions the list of {@link IExtension} extension elements |
| * @param initial <code>true</code> if running from a non-persisted state |
| * |
| */ |
| private void processFragments(IExtension[] extensions, boolean initial) { |
| List<ModelFragmentWrapper> wrappers = new ArrayList<>(); |
| for (IExtension extension : extensions) { |
| IConfigurationElement[] ces = extension.getConfigurationElements(); |
| for (IConfigurationElement ce : ces) { |
| if ("fragment".equals(ce.getName()) && (initial || !INITIAL.equals(ce.getAttribute("apply")))) { //$NON-NLS-1$ //$NON-NLS-2$ |
| MModelFragments fragmentsContainer = getFragmentsContainer(ce); |
| if (fragmentsContainer == null) { |
| continue; |
| } |
| for (MModelFragment fragment : fragmentsContainer.getFragments()) { |
| boolean checkExist = !initial && NOTEXISTS.equals(ce.getAttribute("apply")); //$NON-NLS-1$ |
| wrappers.add(new ModelFragmentWrapper(fragmentsContainer, fragment, |
| ce.getContributor().getName(), URIHelper.constructPlatformURI(ce.getContributor()), |
| checkExist)); // $NON-NLS-1$ |
| } |
| } |
| } |
| } |
| |
| processFragmentWrappers(wrappers); |
| } |
| |
| /** |
| * Processes the given list of fragments wrapped in {@link ModelFragmentWrapper} |
| * elements. |
| * |
| * @param wrappers the list of fragments |
| */ |
| public void processFragmentWrappers(Collection<ModelFragmentWrapper> wrappers) { |
| Map<String, Bucket> elementIdToBucket = new LinkedHashMap<>(); |
| Map<String, Bucket> parentIdToBuckets = new LinkedHashMap<>(); |
| for (ModelFragmentWrapper fragmentWrapper : wrappers) { |
| MModelFragment fragment = fragmentWrapper.getModelFragment(); |
| String parentId = MStringModelFragment.class.cast(fragment).getParentElementId(); |
| if (!parentIdToBuckets.containsKey(parentId)) { |
| parentIdToBuckets.put(parentId, new Bucket()); |
| } |
| Bucket b = parentIdToBuckets.get(parentId); |
| if (elementIdToBucket.containsKey(parentId)) { |
| Bucket parentBucket = elementIdToBucket.get(parentId); |
| parentBucket.dependencies.add(b); |
| b.dependentOn = parentBucket; |
| } |
| b.wrapper.add(fragmentWrapper); // $NON-NLS-1$ |
| |
| for (MApplicationElement e : fragment.getElements()) { |
| // Error case -> clean up and ignore |
| if (parentId == e.getElementId()) { |
| continue; |
| } |
| elementIdToBucket.put(e.getElementId(), b); |
| b.containedElementIds.add(e.getElementId()); |
| if (parentIdToBuckets.containsKey(e.getElementId())) { |
| Bucket childBucket = parentIdToBuckets.get(e.getElementId()); |
| b.dependencies.add(childBucket); |
| childBucket.dependentOn = b; |
| } |
| } |
| } |
| processFragments(createUnifiedFragmentList(elementIdToBucket)); |
| } |
| |
| private List<ModelFragmentWrapper> createUnifiedFragmentList(Map<String, Bucket> elementIdToBucket) { |
| List<ModelFragmentWrapper> fragmentList = new ArrayList<>(); |
| Set<String> checkedElementIds = new LinkedHashSet<>(); |
| for (Entry<String, Bucket> entry : elementIdToBucket.entrySet()) { |
| if (checkedElementIds.contains(entry.getKey())) { |
| continue; |
| } |
| Bucket bucket = entry.getValue(); |
| while (bucket.dependentOn != null) { |
| bucket = bucket.dependentOn; |
| } |
| addAllBucketFragmentWrapper(bucket, fragmentList, checkedElementIds); |
| } |
| return fragmentList; |
| } |
| |
| private void addAllBucketFragmentWrapper(Bucket bucket, List<ModelFragmentWrapper> fragmentList, |
| Set<String> checkedElementIds) { |
| for (ModelFragmentWrapper wrapper : bucket.wrapper) { |
| fragmentList.add(wrapper); |
| } |
| checkedElementIds.addAll(bucket.containedElementIds); |
| for (Bucket child : bucket.dependencies) { |
| addAllBucketFragmentWrapper(child, fragmentList, checkedElementIds); |
| } |
| } |
| |
| public void processFragments(Collection<ModelFragmentWrapper> fragmentList) { |
| for (ModelFragmentWrapper fragmentWrapper : fragmentList) { |
| processFragment(fragmentWrapper.getFragmentContainer(), fragmentWrapper.getModelFragment(), |
| fragmentWrapper.getContributorName(), fragmentWrapper.getContributorURI(), |
| fragmentWrapper.isCheckExists()); |
| } |
| } |
| |
| /** |
| * Adds the {@link MApplicationElement model elements} contributed by the |
| * {@link IConfigurationElement} to the application model and resolves any |
| * fragment imports along the way. |
| * |
| * @param fragmentsContainer the {@link MModelFragments} |
| * @param fragment the {@link MModelFragment} |
| * @param contributorName the name of the element contributing the fragment |
| * @param contributorURI the URI of the element contribution the fragment |
| * @param checkExist specifies whether we should check that the |
| * application model doesn't already contain the |
| * elements contributed by the fragment before merging |
| * them |
| */ |
| public void processFragment(MModelFragments fragmentsContainer, MModelFragment fragment, String contributorName, |
| String contributorURI, boolean checkExist) { |
| /** |
| * The application elements that were added by the given |
| * IConfigurationElement to the application model |
| */ |
| List<MApplicationElement> addedElements = new ArrayList<>(); |
| |
| if (fragmentsContainer == null) { |
| return; |
| } |
| boolean evalImports = false; |
| Diagnostic validationResult = Diagnostician.INSTANCE.validate((EObject) fragment); |
| int severity = validationResult.getSeverity(); |
| if (severity == Diagnostic.ERROR) { |
| logger.error( |
| "Fragment from \"{0}\" of \"{1}\" could not be validated and was not merged: " //$NON-NLS-1$ |
| + fragment.toString(), contributorURI, contributorName); |
| } |
| |
| List<MApplicationElement> merged = processModelFragment(fragment, contributorURI, checkExist); |
| if (!merged.isEmpty()) { |
| evalImports = true; |
| addedElements.addAll(merged); |
| } else { |
| logger.debug("Nothing to merge for fragment \"{0}\" of \"{1}\"", contributorURI, //$NON-NLS-1$ |
| contributorName); |
| } |
| if (evalImports && fragmentsContainer.getImports().size() > 0) { |
| resolveImports(fragmentsContainer.getImports(), addedElements); |
| } |
| } |
| |
| private MModelFragments getFragmentsContainer(IConfigurationElement ce) { |
| E4XMIResource applicationResource = (E4XMIResource) ((EObject) application).eResource(); |
| ResourceSet resourceSet = applicationResource.getResourceSet(); |
| IContributor contributor = ce.getContributor(); |
| String attrURI = ce.getAttribute("uri"); //$NON-NLS-1$ |
| String bundleName = contributor.getName(); |
| if (attrURI == null) { |
| logger.warn("Unable to find location for the model extension \"{0}\"", bundleName); //$NON-NLS-1$ |
| return null; |
| } |
| |
| URI uri; |
| try { |
| // check if the attrURI is already a platform URI |
| if (URIHelper.isPlatformURI(attrURI)) { |
| uri = URI.createURI(attrURI); |
| } else { |
| String path = bundleName + '/' + attrURI; |
| uri = URI.createPlatformPluginURI(path, false); |
| } |
| } catch (RuntimeException e) { |
| logger.warn(e, "Invalid location \"" + attrURI + "\" of model extension \"" + bundleName + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| return null; |
| } |
| |
| Resource resource; |
| try { |
| resource = resourceSet.getResource(uri, true); |
| } catch (RuntimeException e) { |
| logger.warn(e, "Unable to read model extension from \"" + uri.toString() + "\" of \"" + bundleName + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| return null; |
| } |
| |
| EList<?> contents = resource.getContents(); |
| if (contents.isEmpty()) { |
| return null; |
| } |
| |
| Object extensionRoot = contents.get(0); |
| |
| if (!(extensionRoot instanceof MModelFragments)) { |
| logger.warn("Unable to create model extension \"{0}\"", bundleName); //$NON-NLS-1$ |
| return null; |
| } |
| return (MModelFragments) extensionRoot; |
| } |
| |
| /** |
| * Contributes the given {@link MModelFragment} to the application model. |
| * |
| * @param fragment the fragment to add to the application model |
| * @param contributorURI the URI of the element that contributes this fragment |
| * @param checkExist specifies whether we should check that the application |
| * model doesn't already contain the elements contributed |
| * by the fragment before merging them |
| * @return a list of the {@link MApplicationElement} elements that were merged |
| * into the application model by the fragment |
| */ |
| public List<MApplicationElement> processModelFragment(MModelFragment fragment, String contributorURI, |
| boolean checkExist) { |
| |
| E4XMIResource applicationResource = (E4XMIResource) ((EObject) application).eResource(); |
| |
| List<MApplicationElement> elements = fragment.getElements(); |
| if (elements.isEmpty()) { |
| return new ArrayList<>(); |
| } |
| |
| for (MApplicationElement el : elements) { |
| EObject o = (EObject) el; |
| |
| E4XMIResource r = (E4XMIResource) o.eResource(); |
| |
| if (checkExist && applicationResource.getIDToEObjectMap().containsKey(r.getID(o))) { |
| continue; |
| } |
| |
| applicationResource.setID(o, r.getID(o)); |
| |
| if (contributorURI != null) { |
| el.setContributorURI(contributorURI); |
| } |
| |
| // Remember IDs of subitems |
| TreeIterator<EObject> treeIt = EcoreUtil.getAllContents(o, true); |
| while (treeIt.hasNext()) { |
| EObject eObj = treeIt.next(); |
| r = (E4XMIResource) eObj.eResource(); |
| if (contributorURI != null && (eObj instanceof MApplicationElement)) { |
| ((MApplicationElement) eObj).setContributorURI(contributorURI); |
| } |
| applicationResource.setID(eObj, r.getInternalId(eObj)); |
| } |
| } |
| |
| return fragment.merge(application); |
| } |
| |
| /** |
| * Executes the processors as declared in provided {@link IExtension extensions} |
| * array. |
| * |
| * @param extensions the array of {@link IExtension} extensions containing |
| * the processors |
| * @param initial <code>true</code> if the application is running from a |
| * non-persisted state |
| * @param afterFragments <code>true</code> if the processors that should be run |
| * before model fragments are merged are to be executed, |
| * <code>false</code> otherwise |
| */ |
| public void runProcessors(IExtension[] extensions, boolean initial, boolean afterFragments) { |
| for (IExtension extension : extensions) { |
| IConfigurationElement[] ces = extension.getConfigurationElements(); |
| for (IConfigurationElement ce : ces) { |
| boolean parseBoolean = Boolean.parseBoolean(ce.getAttribute("beforefragment")); //$NON-NLS-1$ |
| if ("processor".equals(ce.getName()) && afterFragments != parseBoolean) { //$NON-NLS-1$ |
| if (initial || !INITIAL.equals(ce.getAttribute("apply"))) { //$NON-NLS-1$ |
| runProcessor(ce); |
| } |
| } |
| } |
| } |
| } |
| |
| private void runProcessor(IConfigurationElement ce) { |
| IEclipseContext localContext = EclipseContextFactory.create(); |
| IContributionFactory factory = context.get(IContributionFactory.class); |
| |
| for (IConfigurationElement ceEl : ce.getChildren("element")) { //$NON-NLS-1$ |
| String id = ceEl.getAttribute("id"); //$NON-NLS-1$ |
| |
| if (id == null) { |
| logger.warn("No element id given"); //$NON-NLS-1$ |
| continue; |
| } |
| |
| String key = ceEl.getAttribute("contextKey"); //$NON-NLS-1$ |
| if (key == null) { |
| key = id; |
| } |
| |
| MApplicationElement el = ModelUtils.findElementById(application, id); |
| if (el == null) { |
| logger.warn("Could not find element with id '" + id + "'"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| localContext.set(key, el); |
| } |
| |
| try { |
| Object o = factory.create("bundleclass://" + ce.getContributor().getName() + "/" + ce.getAttribute("class"), //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ |
| context, localContext); |
| if (o == null) { |
| logger.warn("Unable to create processor " + ce.getAttribute("class") + " from " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| + ce.getContributor().getName()); |
| } else { |
| ContextInjectionFactory.invoke(o, Execute.class, context, localContext); |
| } |
| } catch (Exception e) { |
| logger.warn(e, "Could not run processor"); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Resolves the given list of imports used by the specified |
| * <code>addedElements</code> in the application model. |
| * |
| * @param imports the list of elements that were imported by fragments and |
| * should be resolved in the application model |
| * @param addedElements the list of elements contributed by the fragments to the |
| * application model |
| */ |
| public void resolveImports(List<MApplicationElement> imports, List<MApplicationElement> addedElements) { |
| if (imports.isEmpty()) { |
| return; |
| } |
| // now that we have all components loaded, resolve imports |
| Map<MApplicationElement, MApplicationElement> importMaps = new HashMap<>(); |
| for (MApplicationElement importedElement : imports) { |
| MApplicationElement realElement = ModelUtils.findElementById(application, importedElement.getElementId()); |
| if (realElement == null) { |
| logger.warn("Could not resolve an import element for '" + importedElement.getElementId() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| importMaps.put(importedElement, realElement); |
| } |
| |
| TreeIterator<EObject> it = EcoreUtil.getAllContents(addedElements); |
| List<Runnable> commands = new ArrayList<>(); |
| |
| while (it.hasNext()) { |
| EObject o = it.next(); |
| |
| EContentsEList.FeatureIterator<EObject> featureIterator = (EContentsEList.FeatureIterator<EObject>) o |
| .eCrossReferences().iterator(); |
| while (featureIterator.hasNext()) { |
| EObject importObject = featureIterator.next(); |
| if (importObject.eContainmentFeature() == FragmentPackageImpl.Literals.MODEL_FRAGMENTS__IMPORTS) { |
| EStructuralFeature feature = featureIterator.feature(); |
| |
| MApplicationElement el = null; |
| if (importObject instanceof MApplicationElement) { |
| el = importMaps.get((MApplicationElement) importObject); |
| } |
| |
| if (el == null) { |
| logger.warn("Could not resolve import for " + el); //$NON-NLS-1$ |
| } |
| |
| final EObject interalTarget = o; |
| final EStructuralFeature internalFeature = feature; |
| final MApplicationElement internalElment = el; |
| final EObject internalImportObject = importObject; |
| |
| commands.add(() -> { |
| if (internalFeature.isMany()) { |
| logger.error("Replacing"); //$NON-NLS-1$ |
| @SuppressWarnings("unchecked") |
| List<Object> l = (List<Object>) interalTarget.eGet(internalFeature); |
| int index = l.indexOf(internalImportObject); |
| if (index >= 0) { |
| l.set(index, internalElment); |
| } |
| } else { |
| interalTarget.eSet(internalFeature, internalElment); |
| } |
| }); |
| } |
| } |
| } |
| |
| for (Runnable cmd : commands) { |
| cmd.run(); |
| } |
| } |
| } |