Bug 571866 - Add possibility to register fragments and processors
via DS

Change-Id: I16c4054174945273f421c008e48505dd47ebfaa8
Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>
diff --git a/bundles/org.eclipse.e4.ui.workbench/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.workbench/META-INF/MANIFEST.MF
index 93ccc50..15115d0 100644
--- a/bundles/org.eclipse.e4.ui.workbench/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.e4.ui.workbench/META-INF/MANIFEST.MF
@@ -1,7 +1,7 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-SymbolicName: org.eclipse.e4.ui.workbench;singleton:=true
-Bundle-Version: 1.12.100.qualifier
+Bundle-Version: 1.13.0.qualifier
 Bundle-Name: %pluginName
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
@@ -41,7 +41,8 @@
 Bundle-Activator: org.eclipse.e4.ui.internal.workbench.Activator
 Service-Component: OSGI-INF/org.eclipse.e4.ui.internal.LocaleChangeServiceContextFunction.xml,
  OSGI-INF/org.eclipse.e4.ui.internal.workbench.PartServiceCreationFunction.xml,
- OSGI-INF/org.eclipse.e4.ui.internal.workbench.ProgressMonitorFunction.xml
+ OSGI-INF/org.eclipse.e4.ui.internal.workbench.ProgressMonitorFunction.xml,
+ OSGI-INF/org.eclipse.e4.ui.internal.workbench.ModelAssembler.xml
 Import-Package: javax.annotation,
  javax.inject;version="1.0.0",
  org.apache.commons.jxpath;version="1.3.0",
diff --git a/bundles/org.eclipse.e4.ui.workbench/OSGI-INF/org.eclipse.e4.ui.internal.workbench.ModelAssembler.xml b/bundles/org.eclipse.e4.ui.workbench/OSGI-INF/org.eclipse.e4.ui.internal.workbench.ModelAssembler.xml
new file mode 100644
index 0000000..80ba725
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.workbench/OSGI-INF/org.eclipse.e4.ui.internal.workbench.ModelAssembler.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="activate" deactivate="deactivate" immediate="true" name="org.eclipse.e4.ui.internal.workbench.ModelAssembler">
+   <service>
+      <provide interface="org.eclipse.e4.ui.internal.workbench.ModelAssembler"/>
+   </service>
+   <reference bind="setExtensionRegistry" interface="org.eclipse.core.runtime.IExtensionRegistry" name="ExtensionRegistry"/>
+   <reference bind="setLogger" cardinality="0..1" interface="org.osgi.service.log.LoggerFactory" name="Logger" policy="dynamic" unbind="unsetLogger"/>
+   <reference bind="setModelProcessorContribution" cardinality="0..n" interface="org.eclipse.e4.ui.workbench.modeling.IModelProcessorContribution" name="ModelProcessorContribution" policy="dynamic" unbind="unsetModelProcessorContribution"/>
+   <implementation class="org.eclipse.e4.ui.internal.workbench.ModelAssembler"/>
+</scr:component>
\ No newline at end of file
diff --git a/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ModelAssembler.java b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ModelAssembler.java
index 744c26e..239b660 100644
--- a/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ModelAssembler.java
+++ b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ModelAssembler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2010, 2020 BestSolution.at and others.
+ * Copyright (c) 2010, 2021 BestSolution.at and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -22,6 +22,7 @@
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Dictionary;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -31,9 +32,11 @@
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import javax.inject.Inject;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
 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;
@@ -42,14 +45,17 @@
 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.di.UISynchronize;
 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.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.e4.ui.workbench.modeling.IModelProcessorContribution;
+import org.eclipse.e4.ui.workbench.modeling.IModelProcessorContribution.ModelElement;
 import org.eclipse.emf.common.util.Diagnostic;
 import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.common.util.TreeIterator;
@@ -61,12 +67,29 @@
 import org.eclipse.emf.ecore.util.Diagnostician;
 import org.eclipse.emf.ecore.util.EContentsEList;
 import org.eclipse.emf.ecore.util.EcoreUtil;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.log.LogLevel;
+import org.osgi.service.log.Logger;
+import org.osgi.service.log.LoggerFactory;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
 
 /**
  * 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.
  */
+@Component(service = ModelAssembler.class, immediate = true)
 public class ModelAssembler {
 
 	private class Bucket {
@@ -76,23 +99,200 @@
 		Set<String> containedElementIds = new LinkedHashSet<>();
 	}
 
-	@Inject
-	private Logger logger;
+	private class FragmentWrapperElementMapping {
+		ModelFragmentWrapper wrapper;
+		List<MApplicationElement> elements;
+	}
 
-	@Inject
+	private class ModelFragmentBundleTracker implements BundleTrackerCustomizer<List<FragmentWrapperElementMapping>> {
+
+		@Override
+		public List<FragmentWrapperElementMapping> addingBundle(Bundle bundle, BundleEvent event) {
+			// only react on bundles with Model-Fragment header
+			if (bundle.getHeaders("").get(MODEL_FRAGMENT_HEADER) != null) { //$NON-NLS-1$
+				// add the fragment to the application model
+				List<ModelFragmentWrapper> wrappers = getModelFragmentWrapperFromBundle(bundle,
+						ModelAssembler.this.initial);
+
+				List<FragmentWrapperElementMapping> mappings = wrappers.stream().map(w -> {
+					FragmentWrapperElementMapping mapping = new FragmentWrapperElementMapping();
+					mapping.wrapper = w;
+					mapping.elements = new ArrayList<>(w.getModelFragment().getElements());
+					return mapping;
+				}).collect(Collectors.toList());
+
+				// we skip direct processing in case the startup model processing is not done
+				// yet
+				if (processModelExecuted) {
+					uiSync.asyncExec(() -> processFragmentWrappers(wrappers));
+				}
+
+				return mappings;
+			}
+			return null;
+		}
+
+		@Override
+		public void modifiedBundle(Bundle bundle, BundleEvent event, List<FragmentWrapperElementMapping> mapping) {
+			// do nothing
+		}
+
+		@Override
+		public void removedBundle(Bundle bundle, BundleEvent event, List<FragmentWrapperElementMapping> mappings) {
+			// remove fragment elements from application model
+			uiSync.asyncExec(() -> {
+				if (mappings != null) {
+					mappings.stream().flatMap(m -> m.elements.stream()).forEach(appElement -> {
+						// TODO implement removal of contributions, e.g. MenuContributions
+
+						if (appElement instanceof MUIElement) {
+							MUIElement element = (MUIElement) appElement;
+							element.setToBeRendered(false);
+							if (element.getParent() != null) {
+								element.getParent().getChildren().remove(element);
+							}
+						}
+					});
+
+					// unload resource
+					String bundleName = bundle.getSymbolicName();
+					String fragmentHeader = bundle.getHeaders("").get(MODEL_FRAGMENT_HEADER); //$NON-NLS-1$
+					String[] fr = fragmentHeader.split(";"); //$NON-NLS-1$
+					if (fr.length > 0) {
+						String attrURI = fr[0];
+						E4XMIResource applicationResource = (E4XMIResource) ((EObject) application).eResource();
+						ResourceSet resourceSet = applicationResource.getResourceSet();
+						if (attrURI == null) {
+							log(LogLevel.WARN, "Unable to find location for the model extension \"{0}\"", bundleName); //$NON-NLS-1$
+							return;
+						}
+
+						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) {
+							log(LogLevel.WARN, "Invalid location \"{0}\" of model extension \"\"", attrURI, bundleName, //$NON-NLS-1$
+									e);
+							return;
+						}
+
+						try {
+							Resource resource = resourceSet.getResource(uri, true);
+							resource.unload();
+						} catch (RuntimeException e) {
+							log(LogLevel.WARN, "Unable to read model extension from \"{0}\" of \"{1}\"", uri, //$NON-NLS-1$
+									bundleName);
+						}
+					}
+
+				}
+			});
+		}
+	}
+
 	private MApplication application;
-
-	@Inject
 	private IEclipseContext context;
-
-	@Inject
-	private IExtensionRegistry registry;
+	private UISynchronize uiSync;
+	private boolean initial;
 
 	private static final String EXTENSION_POINT_ID = "org.eclipse.e4.workbench.model"; //$NON-NLS-1$
+	private static final String MODEL_FRAGMENT_HEADER = "Model-Fragment"; //$NON-NLS-1$
 
+	private static final String ALWAYS = "always"; //$NON-NLS-1$
 	private static final String INITIAL = "initial"; //$NON-NLS-1$
 	private static final String NOTEXISTS = "notexists"; //$NON-NLS-1$
 
+	LoggerFactory factory;
+	Logger logger;
+
+	private IExtensionRegistry registry;
+
+	private CopyOnWriteArrayList<ServiceReference<IModelProcessorContribution>> processorContributions = new CopyOnWriteArrayList<>();
+
+	BundleContext bundleContext;
+
+	BundleTracker<List<FragmentWrapperElementMapping>> tracker;
+
+	private boolean processModelExecuted = false;
+
+	@Activate
+	void activate(BundleContext bundleContext) {
+		this.bundleContext = bundleContext;
+
+		this.tracker = new BundleTracker<>(bundleContext,
+				Bundle.STARTING | Bundle.ACTIVE | Bundle.STOPPING, new ModelFragmentBundleTracker());
+	}
+
+	@Deactivate
+	void deactivate() {
+		if (this.tracker != null) {
+			this.tracker.close();
+		}
+	}
+
+	/**
+	 * Setter for the {@link IExtensionRegistry}. Use public method binding instead
+	 * of field binding to enable testing.
+	 *
+	 * @param registry The {@link IExtensionRegistry} to use.
+	 */
+	@Reference
+	public void setExtensionRegistry(IExtensionRegistry registry) {
+		this.registry = registry;
+	}
+
+	@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
+	void setModelProcessorContribution(ServiceReference<IModelProcessorContribution> contrib) {
+		this.processorContributions.add(contrib);
+
+		// we skip direct processing in case the startup model processing is not done
+		// yet
+		if (processModelExecuted) {
+			uiSync.asyncExec(() -> {
+				IModelProcessorContribution service = bundleContext.getService(contrib);
+				runProcessor(service);
+			});
+		}
+	}
+
+	void unsetModelProcessorContribution(ServiceReference<IModelProcessorContribution> contrib) {
+		this.processorContributions.remove(contrib);
+		IModelProcessorContribution service = bundleContext.getService(contrib);
+
+		if (this.context != null) {
+			uiSync.asyncExec(() -> {
+				try {
+					ContextInjectionFactory.invoke(service, PreDestroy.class, context);
+				} catch (Exception e) {
+					log(LogLevel.WARN, "Could not run PreDestroy on processor {0}: {1}", contrib.getClass().getName(), //$NON-NLS-1$
+							e);
+				}
+			});
+		}
+	}
+
+	/**
+	 * Initialize this {@link ModelAssembler} with references from the Eclipse
+	 * layer. Used via CIF#invoke instead of CIF#inject or CIF#make to avoid that
+	 * this instance is managed by the Eclipse injection.
+	 *
+	 * @param application the application model
+	 * @param context     the workbench context needed for injection
+	 * @param sync        to dynamically add/remove UI elements in the UI thread
+	 */
+	@PostConstruct
+	public void init(MApplication application, IEclipseContext context, UISynchronize sync) {
+		this.application = application;
+		this.context = context;
+		this.uiSync = sync;
+	}
+
 	/**
 	 * Processes the application model. This will run pre-processors, process the
 	 * fragments, resolve imports and run post-processors, in this order. <br>
@@ -101,9 +301,12 @@
 	 * Extension points will be sorted based on the dependencies of their
 	 * contributors.
 	 *
-	 * @param initial <code>true</code> if running from a non-persisted state
+	 * @param initial     <code>true</code> if running from a non-persisted state
 	 */
+	@Execute
 	public void processModel(boolean initial) {
+		this.initial = initial;
+
 		IExtensionPoint extPoint = registry.getExtensionPoint(EXTENSION_POINT_ID);
 		IExtension[] extensions = new ExtensionsSort().sort(extPoint.getExtensions());
 
@@ -113,6 +316,11 @@
 		processFragments(extensions, initial);
 		// run processors which are marked to run after fragments
 		runProcessors(extensions, initial, true);
+
+		// once we are done, any further handling in the tracker can't be initial
+		// anymore
+		this.initial = false;
+		this.processModelExecuted = true;
 	}
 
 	/**
@@ -129,7 +337,8 @@
 			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);
+					MModelFragments fragmentsContainer = getFragmentsContainer(ce.getAttribute("uri"), //$NON-NLS-1$
+							ce.getContributor().getName());
 					if (fragmentsContainer == null) {
 						continue;
 					}
@@ -143,9 +352,57 @@
 			}
 		}
 
+		if (this.tracker != null) {
+			// this triggers initial bundle tracking in the current thread
+			// for startup reasons we do not process each fragment on initial tracking by
+			// its own, instead we will process the initially tracked bundles together once
+			// the initial tracking is done
+			this.tracker.open();
+
+			// once the initial tracking is done we process the tracked bundles
+			// this is for performance optimization on initial loading to avoid multiple
+			// fragment merge operations
+			List<ModelFragmentWrapper> collect = this.tracker.getTracked().values().stream()
+					.flatMap(List::stream).map(w -> w.wrapper).collect(Collectors.toList());
+			wrappers.addAll(collect);
+		}
+
 		processFragmentWrappers(wrappers);
 	}
 
+	private List<ModelFragmentWrapper> getModelFragmentWrapperFromBundle(Bundle bundle, boolean initial) {
+		List<ModelFragmentWrapper> wrappers = new ArrayList<>();
+		String fragmentHeader = bundle.getHeaders("").get(MODEL_FRAGMENT_HEADER); //$NON-NLS-1$
+		String[] fr = fragmentHeader.split(";"); //$NON-NLS-1$
+		if (fr.length > 0) {
+			String uri = fr[0];
+			String apply = fr.length > 1 ? fr[1].split("=")[1] : "always"; //$NON-NLS-1$ //$NON-NLS-2$
+
+			// check if the value for apply is valid
+			if (!ALWAYS.equals(apply) && !INITIAL.equals(apply) && !NOTEXISTS.equals(apply)) {
+				log(LogLevel.WARN, "Model-Fragment header apply attribute \"{0}\" is invalid, falling back to always", //$NON-NLS-1$
+						apply);
+				apply = ALWAYS;
+			}
+
+			if (initial || !INITIAL.equals(apply)) {
+				MModelFragments fragmentsContainer = getFragmentsContainer(uri, bundle.getSymbolicName());
+				if (fragmentsContainer != null) {
+					for (MModelFragment fragment : fragmentsContainer.getFragments()) {
+						boolean checkExist = !initial && NOTEXISTS.equals(apply);
+						wrappers.add(new ModelFragmentWrapper(fragmentsContainer, fragment, bundle.getSymbolicName(),
+								URIHelper.constructPlatformURI(bundle), checkExist)); // $NON-NLS-1$
+					}
+				}
+			}
+		} else {
+			log(LogLevel.ERROR, "Model-Fragment header value \"{0}\" in bundle \\\"{1}\\\" is invalid", //$NON-NLS-1$
+					fragmentHeader, bundle.getSymbolicName());
+		}
+
+		return wrappers;
+	}
+
 	/**
 	 * Processes the given list of fragments wrapped in {@link ModelFragmentWrapper}
 	 * elements.
@@ -248,7 +505,7 @@
 		Diagnostic validationResult = Diagnostician.INSTANCE.validate((EObject) fragment);
 		int severity = validationResult.getSeverity();
 		if (severity == Diagnostic.ERROR) {
-			logger.error(
+			log(LogLevel.ERROR,
 					"Fragment from \"{0}\" of \"{1}\" could not be validated and was not merged: " //$NON-NLS-1$
 							+ fragment, contributorURI, contributorName);
 		}
@@ -258,7 +515,7 @@
 			evalImports = true;
 			addedElements.addAll(merged);
 		} else {
-			logger.debug("Nothing to merge for fragment \"{0}\" of \"{1}\"", contributorURI, //$NON-NLS-1$
+			log(LogLevel.DEBUG, "Nothing to merge for fragment \"{0}\" of \"{1}\"", contributorURI, //$NON-NLS-1$
 					contributorName);
 		}
 		if (evalImports && fragmentsContainer.getImports().size() > 0) {
@@ -266,14 +523,11 @@
 		}
 	}
 
-	private MModelFragments getFragmentsContainer(IConfigurationElement ce) {
+	private MModelFragments getFragmentsContainer(String attrURI, String bundleName) {
 		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$
+			log(LogLevel.WARN, "Unable to find location for the model extension \"{0}\"", bundleName); //$NON-NLS-1$
 			return null;
 		}
 
@@ -287,7 +541,7 @@
 				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$
+			log(LogLevel.WARN, "Invalid location \"{0}\" of model extension \"\"", attrURI, bundleName, e); //$NON-NLS-1$
 			return null;
 		}
 
@@ -295,7 +549,7 @@
 		try {
 			resource = resourceSet.getResource(uri, true);
 		} catch (RuntimeException e) {
-			logger.warn(e, "Unable to read model extension from \"" + uri + "\" of \"" + bundleName + "\""); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+			log(LogLevel.WARN, "Unable to read model extension from \"{0}\" of \"{1}\"", uri, bundleName); //$NON-NLS-1$
 			return null;
 		}
 
@@ -307,7 +561,7 @@
 		Object extensionRoot = contents.get(0);
 
 		if (!(extensionRoot instanceof MModelFragments)) {
-			logger.warn("Unable to create model extension \"{0}\"", bundleName); //$NON-NLS-1$
+			log(LogLevel.WARN, "Unable to create model extension \"{0}\"", bundleName); //$NON-NLS-1$
 			return null;
 		}
 		return (MModelFragments) extensionRoot;
@@ -388,6 +642,33 @@
 				}
 			}
 		}
+
+		this.processorContributions.stream().filter(sr -> {
+			Dictionary<String, Object> dict = sr.getProperties();
+
+			Object before = dict.get(IModelProcessorContribution.BEFORE_FRAGMENT_PROPERTY_KEY);
+			boolean beforeFragments = true;
+			if (before instanceof Boolean) {
+				beforeFragments = (Boolean) before;
+			} else if (before instanceof String) {
+				beforeFragments = Boolean.parseBoolean((String) before);
+			}
+
+			Object applyObject = dict.get(IModelProcessorContribution.APPLY_PROPERTY_KEY);
+			String apply = applyObject instanceof String ? (String) applyObject
+					: IModelProcessorContribution.APPLY_ALWAYS;
+
+			// check if the value for apply is valid
+			if (!ALWAYS.equals(apply) && !INITIAL.equals(apply)) {
+				log(LogLevel.WARN,
+						"IModelProcessorContribution apply property value \"{0}\" is invalid, falling back to always", //$NON-NLS-1$
+						apply);
+				apply = IModelProcessorContribution.APPLY_ALWAYS;
+			}
+
+			return ((afterFragments != beforeFragments)
+					&& (initial || IModelProcessorContribution.APPLY_ALWAYS.equals(apply)));
+		}).map(sr -> bundleContext.getService(sr)).forEach(ModelAssembler.this::runProcessor);
 	}
 
 	private void runProcessor(IConfigurationElement ce) {
@@ -398,7 +679,7 @@
 			String id = ceEl.getAttribute("id"); //$NON-NLS-1$
 
 			if (id == null) {
-				logger.warn("No element id given"); //$NON-NLS-1$
+				log(LogLevel.WARN, "No element id given"); //$NON-NLS-1$
 				continue;
 			}
 
@@ -409,7 +690,7 @@
 
 			MApplicationElement el = ModelUtils.findElementById(application, id);
 			if (el == null) {
-				logger.warn("Could not find element with id '" + id + "'"); //$NON-NLS-1$ //$NON-NLS-2$
+				log(LogLevel.WARN, "Could not find element with id '{0}'", id); //$NON-NLS-1$
 			}
 			localContext.set(key, el);
 		}
@@ -418,13 +699,56 @@
 			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());
+				log(LogLevel.WARN, "Unable to create processor {0} from {1}", //$NON-NLS-1$
+						ce.getAttribute("class"), //$NON-NLS-1$
+						ce.getContributor().getName());
 			} else {
-				ContextInjectionFactory.invoke(o, Execute.class, context, localContext);
+				ContextInjectionFactory.invoke(o, Execute.class, context, localContext, null);
 			}
 		} catch (Exception e) {
-			logger.warn(e, "Could not run processor"); //$NON-NLS-1$
+			log(LogLevel.WARN, "Could not run processor: {0}", e); //$NON-NLS-1$
+		}
+	}
+
+	private void runProcessor(IModelProcessorContribution processor) {
+		IEclipseContext localContext = EclipseContextFactory.create();
+
+		for (ModelElement element : processor.getModelElements()) {
+			String id = element.getId();
+
+			if (id == null) {
+				log(LogLevel.WARN, "No element id given"); //$NON-NLS-1$
+				continue;
+			}
+
+			String key = element.getContextKey();
+			if (key == null) {
+				key = id;
+			}
+
+			MApplicationElement el = ModelUtils.findElementById(application, id);
+			if (el == null) {
+				log(LogLevel.WARN, "Could not find element with id '{0}'", id); //$NON-NLS-1$
+			}
+			localContext.set(key, el);
+		}
+
+		try {
+			Object o = null;
+			if (processor.getProcessorClass() != null) {
+				o = ContextInjectionFactory.make(processor.getProcessorClass(), localContext);
+			} else {
+				o = processor;
+			}
+			if (o == null) {
+				log(LogLevel.WARN, "Unable to create processor {0} from {1}", //$NON-NLS-1$
+						processor.getProcessorClass().getName(),
+						FrameworkUtil.getBundle(processor.getProcessorClass()).getSymbolicName());
+			} else {
+				ContextInjectionFactory.invoke(o, Execute.class, context, localContext, null);
+			}
+		} catch (Exception e) {
+			log(LogLevel.WARN, "Could not run processor: {0}", e); //$NON-NLS-1$
 		}
 	}
 
@@ -466,8 +790,8 @@
 						el = importMaps.get((MApplicationElement) importObject);
 
 						if (el == null) {
-							logger.warn("Could not resolve import for " //$NON-NLS-1$
-									+ ((MApplicationElement) importObject).getElementId());
+							log(LogLevel.WARN, "Could not resolve import for {0}", //$NON-NLS-1$
+									((MApplicationElement) importObject).getElementId());
 						}
 					}
 
@@ -478,7 +802,7 @@
 
 					commands.add(() -> {
 						if (internalFeature.isMany()) {
-							logger.error(MessageFormat.format(
+							log(LogLevel.ERROR, MessageFormat.format(
 									"Replacing in {0}.\n\nFeature={1}.\n\nInternalElement={2} contributed by {3}.\n\nImportObject={4}", //$NON-NLS-1$
 									interalTarget, internalFeature.getName(), internalElement.getElementId(),
 									internalElement.getContributorURI(), internalImportObject));
@@ -500,4 +824,59 @@
 			cmd.run();
 		}
 	}
+
+	void log(LogLevel level, String message, Object... args) {
+		Logger log = this.logger;
+		if (log != null) {
+			switch (level) {
+			case ERROR:
+				log.error(message, args);
+				break;
+			case WARN:
+				log.warn(message, args);
+				break;
+			case INFO:
+				log.info(message, args);
+				break;
+			case DEBUG:
+				log.debug(message, args);
+				break;
+			case AUDIT:
+				log.audit(message, args);
+				break;
+			case TRACE:
+				log.trace(message, args);
+				break;
+			}
+		} else {
+			// fallback if no LogService is available
+			if (LogLevel.ERROR == level) {
+				System.err.println(MessageFormat.format(message, args));
+			} else {
+				System.out.println(MessageFormat.format(message, args));
+			}
+		}
+	}
+
+	/**
+	 *
+	 * @param factory The {@link LoggerFactory} to retrieve the {@link Logger} from.
+	 */
+	@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
+	public void setLogger(LoggerFactory factory) {
+		this.factory = factory;
+		this.logger = factory.getLogger(getClass());
+	}
+
+	/**
+	 *
+	 * @param loggerFactory The {@link LoggerFactory} that was used to retrieve the
+	 *                      {@link Logger}.
+	 */
+	public void unsetLogger(LoggerFactory loggerFactory) {
+		if (this.factory == loggerFactory) {
+			this.factory = null;
+			this.logger = null;
+		}
+	}
 }
diff --git a/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ResourceHandler.java b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ResourceHandler.java
index 947a8c7..ce5d396 100644
--- a/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ResourceHandler.java
+++ b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/internal/workbench/ResourceHandler.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2009, 2016 IBM Corporation and others.
+ * Copyright (c) 2009, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -192,9 +192,12 @@
 		MApplication appElement = (MApplication) resource.getContents().get(0);
 
 		this.context.set(MApplication.class, appElement);
-		ModelAssembler contribProcessor = ContextInjectionFactory.make(ModelAssembler.class,
-				context);
-		contribProcessor.processModel(initialModel);
+
+		ModelAssembler mac = context.get(ModelAssembler.class);
+		if (mac != null) {
+			ContextInjectionFactory.invoke(mac, PostConstruct.class, context);
+			mac.processModel(initialModel);
+		}
 
 		if (!hasTopLevelWindows(resource) && logger != null) {
 			logger.error(new Exception(), // log a stack trace to help debug the
diff --git a/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/workbench/modeling/IModelProcessorContribution.java b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/workbench/modeling/IModelProcessorContribution.java
new file mode 100644
index 0000000..f5d3b8f
--- /dev/null
+++ b/bundles/org.eclipse.e4.ui.workbench/src/org/eclipse/e4/ui/workbench/modeling/IModelProcessorContribution.java
@@ -0,0 +1,140 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Dirk Fauth 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:
+ *     Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
+ ******************************************************************************/
+
+package org.eclipse.e4.ui.workbench.modeling;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Service component interface to be able to register model processors via OSGi
+ * services.
+ * <p>
+ * Programmatic processor that gets called with the intention of modifying UI
+ * model.
+ * </p>
+ *
+ * @since 1.13
+ */
+public interface IModelProcessorContribution {
+
+	/**
+	 * Service property key for specifying the beforeFragment attribute, which
+	 * specifies if the processor has to be invoked before model fragments are
+	 * added. If not specified it defaults to <code>true</code>.
+	 */
+	String BEFORE_FRAGMENT_PROPERTY_KEY = "beforefragment"; //$NON-NLS-1$
+	/**
+	 * Service property key for specifying the beforeFragment attribute, which
+	 * specifies if the processor has to be invoked before model fragments are
+	 * added. If not specified it defaults to <code>true</code>.
+	 * <p>
+	 * This constant can be used to simplify the property definition in the
+	 * Component annotation:<br>
+	 * <code>@Component(property = { IModelProcessorContribution.BEFORE_FRAGMENT_PROPERTY_PREFIX + "false" })</code>
+	 * </p>
+	 */
+	String BEFORE_FRAGMENT_PROPERTY_PREFIX = "beforefragment:Boolean="; //$NON-NLS-1$
+	/**
+	 * Service property key for specifying the apply attribute, which defines in
+	 * which case a processor is run. If not specified it defaults to <i>always</i>.
+	 */
+	String APPLY_PROPERTY_KEY = "apply"; //$NON-NLS-1$
+	/**
+	 * Service property key for specifying the apply attribute, which defines in
+	 * which case a processor is run. If not specified it defaults to <i>always</i>.
+	 * <p>
+	 * This constant can be used to simplify the property definition in the
+	 * Component annotation:<br>
+	 * <code>@Component(property = { IModelProcessorContribution.APPLY_PROPERTY_PREFIX + "initial" })</code>
+	 * </p>
+	 */
+	String APPLY_PROPERTY_PREFIX = "apply="; //$NON-NLS-1$
+	/**
+	 * Value for the <code>apply</code> attribute. If set the processor is executed
+	 * each time the application is started.
+	 */
+	String APPLY_ALWAYS = "always"; //$NON-NLS-1$
+	/**
+	 * Value for the <code>apply</code> attribute. If set the processor is executed
+	 * only when coming from a none persistent state.
+	 */
+	String APPLY_INITIAL = "initial"; //$NON-NLS-1$
+
+	/**
+	 *
+	 * @return Java class containing model processor. A class method with the
+	 *         qualifier <code>"org.eclipse.e4.core.di.annotations.Execute"</code>,
+	 *         will be invoked as a part of the model processing. If this method
+	 *         returns <code>null</code> it is expected that this
+	 *         {@link IModelProcessorContribution} contains a method with the
+	 *         qualifier <code>"org.eclipse.e4.core.di.annotations.Execute"</code>.
+	 */
+	default Class<?> getProcessorClass() {
+		return null;
+	}
+
+	/**
+	 *
+	 * @return model elements to be added to the context used to invoke the
+	 *         processor.
+	 */
+	default List<ModelElement> getModelElements() {
+		return Collections.emptyList();
+	}
+
+	/**
+	 * A model element to be added to the context used to invoke the processor.
+	 */
+	class ModelElement {
+		private final String id;
+		private final String contextKey;
+
+		/**
+		 *
+		 * @param id Identifier of a model element to be added to the context.
+		 */
+		public ModelElement(String id) {
+			this(id, null);
+		}
+
+		/**
+		 *
+		 * @param id         Identifier of a model element to be added to the context.
+		 * @param contextKey An optional key under which to store the model element in
+		 *                   the context. The value of "id" is used if this value is not
+		 *                   specified.
+		 */
+		public ModelElement(String id, String contextKey) {
+			this.id = id;
+			this.contextKey = contextKey;
+		}
+
+		/**
+		 * @return Identifier of a model element to be added to the context.
+		 */
+		public String getId() {
+			return id;
+		}
+
+		/**
+		 * @return An optional key under which to store the model element in the
+		 *         context. The value of "id" is used if this value is not specified.
+		 */
+		public String getContextKey() {
+			return contextKey;
+		}
+
+	}
+}
diff --git a/tests/org.eclipse.e4.ui.tests/pom.xml b/tests/org.eclipse.e4.ui.tests/pom.xml
index 78aebe4..49a0a26 100644
--- a/tests/org.eclipse.e4.ui.tests/pom.xml
+++ b/tests/org.eclipse.e4.ui.tests/pom.xml
@@ -39,5 +39,5 @@
       </plugin>
     </plugins>
   </build>
-  
+
 </project>
diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/application/ResourceHandlerTest.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/application/ResourceHandlerTest.java
index 214aa4f..3c6488f 100644
--- a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/application/ResourceHandlerTest.java
+++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/application/ResourceHandlerTest.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import org.eclipse.e4.core.contexts.ContextInjectionFactory;
 import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.ui.di.UISynchronize;
 import org.eclipse.e4.ui.internal.workbench.E4Workbench;
 import org.eclipse.e4.ui.internal.workbench.E4XMIResource;
 import org.eclipse.e4.ui.internal.workbench.ResourceHandler;
@@ -38,10 +39,12 @@
 import org.eclipse.e4.ui.model.application.ui.menu.MMenu;
 import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement;
 import org.eclipse.e4.ui.workbench.IWorkbench;
+import org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.eclipse.osgi.service.datalocation.Location;
+import org.eclipse.swt.widgets.Display;
 import org.junit.Before;
 import org.junit.Test;
 import org.osgi.framework.BundleContext;
@@ -82,6 +85,8 @@
 
 		localContext.set(E4Workbench.INITIAL_WORKBENCH_MODEL_URI, uri);
 
+		localContext.set(UISynchronize.class, new DisplayUISynchronize(Display.getDefault()));
+
 		return ContextInjectionFactory.make(ResourceHandler.class, localContext);
 
 	}
diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerFragmentOrderingTests.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerFragmentOrderingTests.java
index 1b3ea76..ce4c846 100644
--- a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerFragmentOrderingTests.java
+++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerFragmentOrderingTests.java
@@ -17,7 +17,6 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
 
 import java.util.Arrays;
 import java.util.HashSet;
@@ -25,9 +24,10 @@
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
+import javax.annotation.PostConstruct;
 import org.eclipse.e4.core.contexts.ContextInjectionFactory;
 import org.eclipse.e4.core.contexts.IEclipseContext;
-import org.eclipse.e4.core.services.log.Logger;
+import org.eclipse.e4.ui.di.UISynchronize;
 import org.eclipse.e4.ui.internal.workbench.E4XMIResource;
 import org.eclipse.e4.ui.internal.workbench.E4XMIResourceFactory;
 import org.eclipse.e4.ui.internal.workbench.ModelAssembler;
@@ -49,10 +49,12 @@
 import org.eclipse.e4.ui.model.fragment.MFragmentFactory;
 import org.eclipse.e4.ui.model.fragment.MModelFragments;
 import org.eclipse.e4.ui.model.fragment.MStringModelFragment;
+import org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.swt.widgets.Display;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -69,7 +71,6 @@
 	private ResourceSetImpl resourceSet;
 	private E4XMIResource appResource;
 	private ModelAssembler assembler;
-	private Logger logger;
 	private MToolBar toolBar;
 	private static final String MAIN_TOOLBAR_ID = "org.eclipse.e4.ui.tests.modelassembler.fragmentordering.mainWindow.mainToolBar";
 	private static final String MAIN_WINDOW_ID = "org.eclipse.e4.ui.tests.modelassembler.fragmentordering.mainWindow";
@@ -81,10 +82,8 @@
 		application.setElementId(APPLICATION_ID);
 		application.setContext(appContext);
 
-		logger = mock(Logger.class);
-
-		appContext.set(Logger.class, logger);
 		appContext.set(MApplication.class, application);
+		appContext.set(UISynchronize.class, new DisplayUISynchronize(Display.getDefault()));
 
 		factory = new E4XMIResourceFactory();
 		appResource = (E4XMIResource) factory.createResource(URI.createURI("virtualuri"));
@@ -92,7 +91,7 @@
 		resourceSet.getResources().add(appResource);
 		appResource.getContents().add((EObject) application);
 		assembler = new ModelAssembler();
-		ContextInjectionFactory.inject(assembler, appContext);
+		ContextInjectionFactory.invoke(assembler, PostConstruct.class, appContext);
 		createWindowWithToolbar();
 	}
 
diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerTests.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerTests.java
index 6e5376d..af4fb5d 100644
--- a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerTests.java
+++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ModelAssemblerTests.java
@@ -21,11 +21,13 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
+import javax.annotation.PostConstruct;
 import org.eclipse.core.internal.registry.ExtensionRegistry;
 import org.eclipse.core.runtime.ContributorFactorySimple;
 import org.eclipse.core.runtime.IContributor;
@@ -35,7 +37,7 @@
 import org.eclipse.core.runtime.RegistryFactory;
 import org.eclipse.e4.core.contexts.ContextInjectionFactory;
 import org.eclipse.e4.core.contexts.IEclipseContext;
-import org.eclipse.e4.core.services.log.Logger;
+import org.eclipse.e4.ui.di.UISynchronize;
 import org.eclipse.e4.ui.internal.workbench.E4XMIResource;
 import org.eclipse.e4.ui.internal.workbench.E4XMIResourceFactory;
 import org.eclipse.e4.ui.internal.workbench.ExtensionsSort;
@@ -55,14 +57,19 @@
 import org.eclipse.e4.ui.model.fragment.MModelFragments;
 import org.eclipse.e4.ui.model.fragment.MStringModelFragment;
 import org.eclipse.e4.ui.workbench.modeling.EModelService;
+import org.eclipse.e4.ui.workbench.swt.DisplayUISynchronize;
 import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.EObject;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.eclipse.emf.ecore.resource.URIConverter;
 import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
+import org.eclipse.swt.widgets.Display;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.osgi.service.log.Logger;
+import org.osgi.service.log.LoggerFactory;
 
 @SuppressWarnings("nls")
 public class ModelAssemblerTests {
@@ -86,17 +93,25 @@
 		application.setContext(appContext);
 
 		logger = mock(Logger.class);
+		LoggerFactory logFactory = mock(LoggerFactory.class);
+		when(logFactory.getLogger((Class<?>) ArgumentMatchers.any())).thenReturn(logger);
 
 		appContext.set(Logger.class, logger);
 		appContext.set(MApplication.class, application);
+		appContext.set(UISynchronize.class, new DisplayUISynchronize(Display.getDefault()));
 
 		factory = new E4XMIResourceFactory();
 		appResource = (E4XMIResource) factory.createResource(URI.createURI("virtualuri"));
 		resourceSet = new ResourceSetImpl();
 		resourceSet.getResources().add(appResource);
 		appResource.getContents().add((EObject) application);
+
+		// FIXME set references programmatically
 		assembler = new ModelAssembler();
-		ContextInjectionFactory.inject(assembler, appContext);
+		ContextInjectionFactory.invoke(assembler, PostConstruct.class, appContext);
+
+		assembler.setLogger(logFactory);
+
 		modelService = application.getContext().get(EModelService.class);
 	}
 
@@ -295,7 +310,7 @@
 
 		assembler.resolveImports(imports, addedElements);
 		assertEquals(null, placeholder.getRef());
-		verify(logger).warn("Could not resolve import for null");
+		verify(logger).warn("Could not resolve import for {0}", new Object[] { null });
 		verifyZeroInteractions(logger);
 	}
 
@@ -325,6 +340,8 @@
 		/* contribute fragment with imports and post-processor */
 		IContributor contributor = ContributorFactorySimple.createContributor(BUNDLE_SYMBOLIC_NAME);
 		IExtensionRegistry registry = createTestExtensionRegistry();
+		assembler.setExtensionRegistry(registry);
+
 		assertEquals(0, registry.getConfigurationElementsFor(EXTENSION_POINT_ID).length);
 		// The fragment contributes a Placeholder to the application's Area. The
 		// Placeholder references the Part that we created above.
@@ -463,7 +480,8 @@
 	@Test
 	public void testProcessor_noProcessor() throws Exception {
 		testProcessor("org.eclipse.e4.ui.tests/data/ModelAssembler/processor_null.xml", true, false);
-		verify(logger).warn("Unable to create processor null from org.eclipse.e4.ui.tests");
+		verify(logger).warn("Unable to create processor {0} from {1}",
+				new Object[] { null, "org.eclipse.e4.ui.tests" });
 		assertEquals(0, application.getDescriptors().size());
 		verifyZeroInteractions(logger);
 	}
@@ -477,8 +495,9 @@
 	@Test
 	public void testProcessor_processorNotFound() throws Exception {
 		testProcessor("org.eclipse.e4.ui.tests/data/ModelAssembler/processor_wrongProcessorClass.xml", true, false);
-		verify(logger).warn(
-				"Unable to create processor org.eclipse.e4.ui.tests.workbench.SimplePreProcessor_NotFound from org.eclipse.e4.ui.tests");
+		verify(logger).warn("Unable to create processor {0} from {1}",
+				new Object[] { "org.eclipse.e4.ui.tests.workbench.SimplePreProcessor_NotFound",
+						"org.eclipse.e4.ui.tests" });
 		assertEquals(0, application.getDescriptors().size());
 		verifyZeroInteractions(logger);
 	}
@@ -495,7 +514,8 @@
 	public void testProcessor_wrongAppId() throws Exception {
 		application.setElementId("newID");
 		testProcessor("org.eclipse.e4.ui.tests/data/ModelAssembler/processors_initial.xml", true, true);
-		verify(logger).warn("Could not find element with id 'org.eclipse.e4.ui.tests.modelassembler.app'");
+		verify(logger).warn("Could not find element with id '{0}'",
+				new Object[] { "org.eclipse.e4.ui.tests.modelassembler.app" });
 		verifyZeroInteractions(logger);
 		assertEquals(1, application.getDescriptors().size());
 		assertEquals("simpleprocessor.post", application.getDescriptors().get(0).getElementId());