/*******************************************************************************
* Copyright (c) 2013, 2015IBM 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
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654
******************************************************************************/
package org.eclipse.e4.ui.internal.workbench;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IRegistryEventListener;
import org.eclipse.e4.ui.model.application.MApplicationElement;
import org.eclipse.e4.ui.model.application.impl.ApplicationPackageImpl;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.osgi.service.log.LogService;
/**
* A factory which is able to build the EMF based EObjects for the given {@link MApplicationElement}
* class.
*
* <p>
* This factory checks the Eclipse ExtensionRegistry for all registered EMF-packages, via the
* {@code "org.eclipse.emf.ecore.generated_package"} ExtensionPoint generated by EMF. It uses the
* EPackage Namespace URI mentioned in this ExtensionPoint to build a mapping between normal java
* class and the corresponding {@link EClass}.
* </p>
*
* <p>
* <b>Important:</b> The mapping will only contain {@link EClass}es which extend the
* {@link MApplicationElement} and are neither abstract nor an interface.
* </p>
*/
final class GenericMApplicationElementFactoryImpl {
/**
* An ExtensionRegistryListener which will build the required {@link Class} to {@link EClass}
* mapping.
*/
private final MApplicationElementClassToEClass emfGeneratedPackages;
/**
* Sole constructor.
*
* @param extensionRegistry
* the Eclipse ExtensionRegistry
* @throws NullPointerException
* if the given Eclipse ExtensionRegistry is {@code null}
*/
GenericMApplicationElementFactoryImpl(IExtensionRegistry extensionRegistry) {
if (extensionRegistry == null)
throw new NullPointerException("No ExtensionRegistry given!"); //$NON-NLS-1$
emfGeneratedPackages = new MApplicationElementClassToEClass();
// A clean-up would be nice but the only using service is realized as a singleton-service
// which is used throughout the running application and so this instance will also life as
// long as the application is running.
extensionRegistry.addListener(emfGeneratedPackages,
MApplicationElementClassToEClass.EP_MODEL_DEFINITION_ENRICHMENT);
emfGeneratedPackages.initialize(extensionRegistry);
}
/**
* Takes the given class and creates the corresponding {@link EObject} implementation for it.
*
* @param clazz
* the class for which the corresponding {@link EObject} should be created
* @return the corresponding {@link EObject} or {@code null} if it wasn't able to create one
* (e.g.: no {@link EClass} maps to the given {@link Class})
*/
public EObject createEObject(Class<? extends MApplicationElement> clazz) {
EClass eClass = emfGeneratedPackages.getEClass(clazz);
if (eClass != null) {
return EcoreUtil.create(eClass);
}
return null;
}
/**
* An Eclipse ExtensionRegistry-Listener which will build the required map to find the
* {@link EClass} for the given {@link Class}.
*
* <p>
* This Listener must be registered on EMF's {@value #EP_MODEL_DEFINITION_ENRICHMENT} extension
* point to build the appropriate mapping between {@link Class} and {@link EClass}.
* </p>
*
* <p>
* <b>Info:</b> This map will only contain concrete {@link EClass} objects which extend the
* {@link MApplicationElement}.
* </p>
*/
private static final class MApplicationElementClassToEClass implements IRegistryEventListener {
/** The extension point name which holds the required information. */
public static final String EP_MODEL_DEFINITION_ENRICHMENT = "org.eclipse.e4.workbench.model.definition.enrichment"; //$NON-NLS-1$
/**
* The configuration element inside the extension point which holds the required
* information.
*/
private static final String CONFIG_ELEMENT_NAME = "definitionEnrichment"; //$NON-NLS-1$
/** Attribute name which holds the EMF EPackage Namespace URI. */
private static final String CONFIG_ATTR_EPACKAGE_URI = "ePackageNS"; //$NON-NLS-1$
/** Holds the mapping between {@link Class} and {@link EClass}. */
private final ConcurrentMap<Class<? extends MApplicationElement>, EClass> classToEClass = new ConcurrentHashMap<>();
/**
* Holds the required information per extension point which needs to be clean-up in the
* {@link #removed(IExtension[])} method.
*/
private final ConcurrentMap<IExtension, List<Class<? extends MApplicationElement>>> registeredClasses = new ConcurrentHashMap<>();
/** A reference to the {@link MApplicationElement}-EClass. */
private final EClass mApplicationElementEClass = ApplicationPackageImpl.eINSTANCE
.getApplicationElement();
/**
* Method which will initialize the mapping with the information from the given Eclipse
* ExtensionRegistry.
*
* <p>
* The method will retrieve all {@link #EP_MODEL_DEFINITION_ENRICHMENT} extensions form the
* given Eclipse ExtensionRegistry and initializes the basic mapping.
* </p>
*
* @param extensionRegistry
* the Eclipse ExtensionRegistry on which the listener is already registered
*/
void initialize(IExtensionRegistry extensionRegistry) {
if (extensionRegistry == null) { // just for safety's sake
throw new IllegalArgumentException("No ExtensionRegistry given!"); //$NON-NLS-1$
}
IExtensionPoint epGeneratedPackage = extensionRegistry
.getExtensionPoint(EP_MODEL_DEFINITION_ENRICHMENT);
if (epGeneratedPackage != null) {
added(epGeneratedPackage.getExtensions());
}
}
/**
* Lookup the {@link EClass} for the given {@link Class}.
*
* @param elementType
* the {@link Class} to which the {@link EClass} should be found
* @return the corresponding {@link EClass} or {@code null} if none was found
*/
public EClass getEClass(Class<? extends MApplicationElement> elementType) {
return classToEClass.get(elementType);
}
@Override
public void added(IExtension[] extensions) {
for (IExtension extension : extensions) {
List<Class<? extends MApplicationElement>> elementsToCleanup = addToMapping(extension
.getConfigurationElements());
if (elementsToCleanup != null) {
// keep the list of registered class per extension to remove them in the
// #remove(IExtension[]) method
registeredClasses.put(extension, elementsToCleanup);
}
}
}
@Override
public void removed(IExtension[] extensions) {
for (IExtension extension : extensions) {
List<Class<? extends MApplicationElement>> modelClassesToRemove = registeredClasses
.remove(extension);
if (modelClassesToRemove != null) {
// clean-up
for (Class<? extends MApplicationElement> modelClass : modelClassesToRemove) {
classToEClass.remove(modelClass);
}
}
}
}
@Override
public void added(IExtensionPoint[] extensionPoints) {
// not of interest
}
@Override
public void removed(IExtensionPoint[] extensionPoints) {
// not of interest
}
/**
* Reads the information from the given {@link IConfigurationElement}s and updates the
* mapping.
*
* @param configurationElements
* the elements to read the information from
* @return the list of {@link Class}es which were put to the {@link #classToEClass} mapping
* or <code>null</code> if none were put into that list
*/
private List<Class<? extends MApplicationElement>> addToMapping(
IConfigurationElement[] configurationElements) {
if (configurationElements == null) {
return null;
}
List<Class<? extends MApplicationElement>> allMappedEntried = new ArrayList<>();
for (IConfigurationElement configElement : configurationElements) {
if (configElement.getName().equals(CONFIG_ELEMENT_NAME)) {
String emfNsURI = configElement.getAttribute(CONFIG_ATTR_EPACKAGE_URI);
// find EPackage
EPackage ePackage = EPackage.Registry.INSTANCE.getEPackage(emfNsURI);
// build Class to EClass mapping from the classes in the EPackage
Map<Class<? extends MApplicationElement>, EClass> mapping = buildMapping(ePackage);
if (mapping != null) {
for (Map.Entry<Class<? extends MApplicationElement>, EClass> entry : mapping
.entrySet()) {
// if the current thread added the mapping we keep the key so it can be
// removed afterwards in the #remove(IExtension[]) method
if (classToEClass.putIfAbsent(entry.getKey(), entry.getValue()) == null) {
allMappedEntried.add(entry.getKey());
}
}
}
}
}
// null means nothing from the given configurationElementes was added to the Class to
// EClass map
return allMappedEntried.isEmpty() ? null : allMappedEntried;
}
/**
* Utility method which walks through all {@link EClass} of the given {@link EPackage} to
* build a Class-To-EClass map.
* <p>
* This method will only take {@link EClass}es into account which extend the
* {@link MApplicationElement} and are neither a abstract class nor an interface. Which
* means the mapping will only contain {@link EModelService#createModelElement(Class)}
* relevant classes.
* </p>
*
* @param ePackage
* the EPackage to scan
* @return a map containing all {@link Class}es and their corresponding {@link EClass} which
* are provided by the given {@link EPackage} and extend the
* {@link MApplicationElement}; {@code null} otherwise
*/
private final Map<Class<? extends MApplicationElement>, EClass> buildMapping(
EPackage ePackage) {
if (ePackage == null)
return null;
List<EClassifier> eClassifiers = ePackage.getEClassifiers();
Map<Class<? extends MApplicationElement>, EClass> mapping = new HashMap<>();
for (EClassifier eClassifier : eClassifiers) {
if (eClassifier instanceof EClass) {
EClass eClass = (EClass) eClassifier;
if (mApplicationElementEClass.isSuperTypeOf(eClass) && !eClass.isAbstract()
&& !eClass.isInterface()) {
@SuppressWarnings("unchecked")
Class<? extends MApplicationElement> instanceClass = (Class<? extends MApplicationElement>) eClass
.getInstanceClass();
// the Map.Entry check is just for safety, because of the EMF special for
// Key/Value pairs in HashMaps
// (see: UIElements.ecore/application/StringToStringMap)
if (!instanceClass.equals(Map.Entry.class)) {
// add the entry, but if there was already a mapping we should log it
EClass previousEntry = mapping.put(instanceClass, eClass);
if (previousEntry != null) {
Activator
.log(LogService.LOG_WARNING,
instanceClass
+ " is mapped to multiple EClasses (" + eClass.getName() + ", " + previousEntry.getName() + ")!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
}
}
}
return mapping.isEmpty() ? null : mapping;
}
}
}