/*****************************************************************************
* Copyright (c) 2010 CEA LIST.
*
* 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:
* Camille Letavernier (CEA LIST) camille.letavernier@cea.fr - Initial API and implementation
*****************************************************************************/
package org.eclipse.papyrus.views.properties.runtime;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.runtime.IPath;
import org.eclipse.emf.common.util.EList;
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.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.papyrus.infra.emf.utils.EMFHelper;
import org.eclipse.papyrus.views.properties.Activator;
import org.eclipse.papyrus.views.properties.contexts.Context;
import org.eclipse.papyrus.views.properties.contexts.DataContextElement;
import org.eclipse.papyrus.views.properties.contexts.Property;
import org.eclipse.papyrus.views.properties.contexts.Section;
import org.eclipse.papyrus.views.properties.contexts.Tab;
import org.eclipse.papyrus.views.properties.environment.CompositeWidgetType;
import org.eclipse.papyrus.views.properties.environment.Environment;
import org.eclipse.papyrus.views.properties.environment.EnvironmentPackage;
import org.eclipse.papyrus.views.properties.environment.LayoutType;
import org.eclipse.papyrus.views.properties.environment.Namespace;
import org.eclipse.papyrus.views.properties.environment.PropertyEditorType;
import org.eclipse.papyrus.views.properties.environment.StandardWidgetType;
import org.eclipse.papyrus.views.properties.environment.Type;
import org.eclipse.papyrus.views.properties.environment.WidgetType;
import org.eclipse.papyrus.views.properties.extensions.ContextExtensionPoint;
import org.eclipse.papyrus.views.properties.extensions.EnvironmentExtensionPoint;
import org.eclipse.papyrus.views.properties.root.PropertiesRoot;
import org.eclipse.papyrus.views.properties.root.RootFactory;
import org.eclipse.papyrus.views.properties.runtime.preferences.ContextDescriptor;
import org.eclipse.papyrus.views.properties.runtime.preferences.Preferences;
import org.eclipse.papyrus.views.properties.runtime.preferences.PreferencesFactory;
import org.eclipse.papyrus.views.properties.util.PropertiesUtil;
/**
* Central class of the Property View framework. It lists the available environments and contexts,
* and is responsible for Enabling or Disabling contexts programmatically.
*
* All {@link Context}s should have unique names.
*
* @see ContextExtensionPoint
* @see EnvironmentExtensionPoint
* @see Preferences
* @see ConfigurationManager#instance
*
* @author Camille Letavernier
*/
public class ConfigurationManager {
private final Preferences preferences;
private final PropertiesRoot root;
private final ResourceSet resourceSet = new ResourceSetImpl();
private boolean started = false;
/**
* All contexts (Whether they are applied or not)
*/
private final Map<URI, Context> contexts;
private final Set<Context> enabledContexts;
private final Map<Context, Boolean> customizableContexts;
/**
* The global constraint engine
*/
public ViewConstraintEngine constraintEngine;
/**
* The singleton instance
*/
public final static ConfigurationManager instance = new ConfigurationManager();
private ConfigurationManager() {
constraintEngine = new ViewConstraintEngineImpl();
enabledContexts = new LinkedHashSet<Context>();
customizableContexts = new HashMap<Context, Boolean>();
contexts = new LinkedHashMap<URI, Context>();
root = RootFactory.eINSTANCE.createPropertiesRoot();
preferences = loadPreferences();
}
private void start() {
if(started) {
return;
}
started = true;
new ContextExtensionPoint();
new EnvironmentExtensionPoint();
loadCustomContexts();
}
private EObject loadEMFModel(URI sourceURI) throws IOException {
return EMFHelper.loadEMFModel(resourceSet, sourceURI);
}
private Preferences loadPreferences() {
IPath path = Activator.getDefault().getPreferencesPath();
String preferencesPath = path.toString() + "/preferences.xmi"; //$NON-NLS-1$
URI preferencesURI = URI.createFileURI(preferencesPath);
try {
EObject model = loadEMFModel(preferencesURI);
if(model != null && model instanceof Preferences) {
return (Preferences)model;
}
} catch (Exception ex) {
//File not found : we ignore the exception //TODO : improve the exceptions (FileNotFound is not the only one that can occur)
}
//If we're here, then the preferences.xmi doesn't exist or isn't valid : we create it
return createPreferences(preferencesURI);
}
private Preferences createPreferences(URI preferencesURI) {
Preferences preferencesStore = PreferencesFactory.eINSTANCE.createPreferences();
Resource resource = resourceSet.createResource(preferencesURI);
resource.getContents().add(preferencesStore);
saveModel(preferencesStore);
return preferencesStore;
}
private void loadCustomContexts() {
IPath path = Activator.getDefault().getPreferencesPath();
File preferencesDirectory = path.toFile();
for(File contextDirectory : preferencesDirectory.listFiles()) {
try {
if(contextDirectory.isDirectory()) {
loadCustomContext(contextDirectory);
}
} catch (Exception ex) {
Activator.log.error(ex);
}
}
}
/**
* Refresh the Context represented by the given File. The File should be a
* valid Context model. This method should be called when a model is edited
* at runtime.
*
* @param contextFile
* A File containing a valid Context model
*/
public void refresh(File contextFile) {
URI contextURI = URI.createFileURI(contextFile.getAbsolutePath());
ResourceSet tmpResourceSet = new ResourceSetImpl();
EObject root;
try {
root = EMFHelper.loadEMFModel(tmpResourceSet, contextURI);
if(root != null) {
for(Object rootObject : root.eResource().getContents()) {
if(rootObject instanceof Context) {
refresh((Context)rootObject);
}
}
}
} catch (IOException ex) {
Activator.log.error(ex);
}
}
private void refresh(Context context) {
// TODO : get the right URI from the context file :
// ppe:/context/<plugin>/<path> if it is in the workspace,
// ppe:/context/<preferences>/<path> if it is registered through
// preferences
URI contextURI = EcoreUtil.getURI(context);
if(contexts.containsKey(contextURI)) {
// Unloads the previous objects corresponding to this context
Context previousContext = contexts.get(contextURI);
enabledContexts.remove(previousContext);
previousContext.eResource().unload();
// Adds the new object corresponding to this context
try {
addContext(contextURI);
constraintEngine.refresh();
} catch (IOException ex) {
Activator.log.error(ex);
}
}
}
private void loadCustomContext(File contextDirectory) throws IOException {
String contextPath = contextDirectory.getPath() + "/" + contextDirectory.getName() + ".ctx"; //$NON-NLS-1$ //$NON-NLS-2$
URI contextURI = URI.createFileURI(contextPath);
try {
EObject model = loadEMFModel(contextURI);
if(model instanceof Context) {
Context context = (Context)model;
addContext(context, findDescriptor(context).isApplied(), true);
}
} catch (IOException ex) {
//Silent : The file has been removed from the preferences, but the folder still exists
}
}
/**
* Tests if a Context is enabled.
*
* @param context
* @return
* true if the given context is enabled.
*
* @see Preferences
*/
public boolean isApplied(Context context) {
return !isCustomizable(context) || findDescriptor(context).isApplied();
}
/**
* Retrieves the ContextDescriptor associated to the specified context.
* If a matching descriptor cannot be found, a new Descriptor is created
* in the preferences.
*
* @param context
* @return
*/
private ContextDescriptor findDescriptor(Context context) {
if(context.getName() == null || context.getName().equals("")) { //$NON-NLS-1$
return null;
}
for(ContextDescriptor descriptor : preferences.getContexts()) {
if(descriptor.getName().equals(context.getName())) {
return descriptor;
}
}
//The descriptor hasn't been found : We create it
ContextDescriptor descriptor = PreferencesFactory.eINSTANCE.createContextDescriptor();
descriptor.setName(context.getName());
preferences.getContexts().add(descriptor);
savePreferences();
return descriptor;
}
/**
* Adds a context via its URI. The URI should represent a valid Context model.
* The model is loaded in the ConfigurationManager's resourceSet.
*
* @param uri
* The context's URI
* @throws IOException
* If the model behind this URI is not a valid Context
*/
public void addContext(URI uri, boolean customizable) throws IOException {
EObject firstRootObject = loadEMFModel(uri);
if(firstRootObject != null) {
for(EObject rootObject : firstRootObject.eResource().getContents()) {
if(rootObject instanceof Context) {
Context context = (Context)rootObject;
addContext(context, findDescriptor(context).isApplied(), customizable);
}
}
}
}
/**
* Adds a context via its URI. The URI should represent a valid Context model.
* The model is loaded in the ConfigurationManager's resourceSet.
*
* @param uri
* The context's URI
* @throws IOException
* If the model behind this URI is not a valid Context
*/
public void addContext(URI uri) throws IOException {
addContext(uri, true);
}
/**
* Programmatically register a new context to this ConfigurationManager.
* Most of the time, new contexts should be registered through {@link ContextExtensionPoint}.
* However, you can still call this method when creating a Context at runtime, programmatically
* (Wizards, ...)
* All {@link Context} should have unique names
*
* @param context
* The new context to register
* @param apply
* Whether the context should be enabled or not
*
* @see ConfigurationManager#addContext(URI)
*/
public void addContext(Context context, boolean apply) {
addContext(context, apply, true);
}
/**
* Programmatically register a new context to this ConfigurationManager.
* Most of the time, new contexts should be registered through {@link ContextExtensionPoint}.
* However, you can still call this method when creating a Context at runtime, programmatically
* (Wizards, ...)
* All {@link Context} should have unique names
*
* @param context
* The new context to register
* @param apply
* Whether the context should be enabled or not
*
* @see ConfigurationManager#addContext(URI)
*/
public void addContext(Context context, boolean apply, boolean isCustomizable) {
customizableContexts.put(context, isCustomizable);
contexts.put(EcoreUtil.getURI(context), context);
//If the context is not customizable, then it must always be applied
if(apply || !isCustomizable) {
enableContext(context, true);
} else {
disableContext(context, true);
}
}
/**
* @return the list of <strong>enabled</strong> contexts
*/
public Collection<Context> getEnabledContexts() {
return enabledContexts;
}
/**
* Disable a Context.
*
* @param context
* The Context to disable
* @param update
* If true, the constraint engine will be updated to handle the
* modification
* If false, you should call manually {@link #update()} to refresh
* the constraint engine
* @see Preferences
* @see #enableContext(Context, boolean)
*/
public void disableContext(Context context, boolean update) {
if(!isCustomizable(context)) {
throw new IllegalStateException("Non-customizable contexts cannot be disabled. Trying to disable " + context.getName());
}
update = enabledContexts.remove(context) && update;
//Update the preferences
ContextDescriptor descriptor = findDescriptor(context);
if(descriptor.isApplied()) {
descriptor.setApplied(false);
savePreferences();
}
if(update) {
//Update the Engine
update();
}
}
/**
* Enables a Context
*
* @param context
* The Context to enable
* @param update
* If true, the constraint engine will be updated to handle the
* modification
* If false, you should call manually {@link #update()} to refresh
* the constraint engine
*
* @see #disableContext(Context, boolean)
*/
public void enableContext(Context context, boolean update) {
enabledContexts.add(context);
//root.getContexts().add(context);
//Update the preferences
ContextDescriptor descriptor = findDescriptor(context);
if(!descriptor.isApplied()) {
descriptor.setApplied(true);
savePreferences();
}
if(update) {
//Update the Engine
constraintEngine.addContext(context);
}
}
/**
* Tests if a Context is a plugin context. plugin contexts
* are registered through {@link ContextExtensionPoint} and are
* read-only.
*
* @param context
* @return
* True if the context comes from a plugin, and is thus read-only
*/
public boolean isPlugin(Context context) {
URI uri = EcoreUtil.getURI(context);
boolean result = !(uri.isFile() || uri.isPlatformResource());
return result;
}
/**
* Loads a Context from the given URI. The model is loaded in the {@link ConfigurationManager}'s resourceSet
*
* @param uri
* The URI from which the Context is loaded
* @return
* The loaded context
* @throws IOException
* If the URI doesn't represent a valid Context model
*/
public Context getContext(URI uri) throws IOException {
return (Context)loadEMFModel(uri);
}
private void addEnvironment(Environment environment) {
root.getEnvironments().add(environment);
}
/**
* Adds a new Environment from the given URI.
*
* @param uri
* The URI from which the Environment is retrieved.
* @throws IOException
* if the URI doesn't represent a valid Environment model
*/
public void addEnvironment(URI uri) throws IOException {
Environment environment = (Environment)loadEMFModel(uri);
addEnvironment(environment);
}
/**
* @return
* The PropertiesRoot for the Property view framework. The PropertiesRoot contains
* all registered Environments and Contexts (Whether they are enabled or disabled)
*/
public PropertiesRoot getPropertiesRoot() {
return root;
}
/**
* Returns the context from the given context name
*
* @param contextName
* The name of the context to retrieve
* @return
* The context corresponding to the given name
*/
public Context getContext(String contextName) {
for(Context context : getContexts()) {
if(context.getName().equals(contextName)) {
return context;
}
}
return null;
}
private void savePreferences() {
saveModel(preferences);
}
private void saveModel(EObject eObject) {
try {
eObject.eResource().save(Collections.EMPTY_MAP);
} catch (IOException ex) {
Activator.log.error(ex);
}
}
/**
* Returns all the known contexts, even if they are not applied
* To get only applied contexts, see {@link #getEnabledContexts()}
*
* @return All known contexts
*
* @see PropertiesRoot#getContexts()
*/
public Collection<Context> getContexts() {
return contexts.values();
}
/**
* Returns all the known customizable contexts.
*
* @return All known contexts
*
* @see PropertiesRoot#getContexts()
* @see {@link #getEnabledContexts()}
*/
public Collection<Context> getCustomizableContexts() {
List<Context> result = new LinkedList<Context>();
for(Context context : contexts.values()) {
if(isCustomizable(context)) {
result.add(context);
}
}
return result;
}
private <T extends WidgetType> T getDefaultWidget(int featureID, Class<T> theClass, String widgetName, String namespacePrefix) {
EStructuralFeature feature = EnvironmentPackage.Literals.ENVIRONMENT.getEStructuralFeature(featureID);
for(Environment environment : root.getEnvironments()) {
T widget = findWidgetTypeByClassName((EList<T>)environment.eGet(feature), widgetName, namespacePrefix);
if(widget != null) {
return widget;
}
}
return null;
}
private <T extends WidgetType> T findWidgetTypeByClassName(Collection<T> types, String className, String namespacePrefix) {
for(T widgetType : types) {
if(widgetType.getWidgetClass().equals(className) && PropertiesUtil.namespaceEqualsByName(widgetType.getNamespace(), namespacePrefix)) {
return widgetType;
}
}
return null;
}
/**
* @return the default implementation of CompositeWidgetType
*/
public CompositeWidgetType getDefaultCompositeType() {
return getDefaultWidget(EnvironmentPackage.ENVIRONMENT__COMPOSITE_WIDGET_TYPES, CompositeWidgetType.class, "Composite", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* @return the default implementation of LayoutType
*/
public LayoutType getDefaultLayoutType() {
return getDefaultWidget(EnvironmentPackage.ENVIRONMENT__LAYOUT_TYPES, LayoutType.class, "PropertiesLayout", "ppel"); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* @return the default implementation of StandardWidgetType
*/
public StandardWidgetType getDefaultWidgetType() {
return getDefaultWidget(EnvironmentPackage.ENVIRONMENT__WIDGET_TYPES, StandardWidgetType.class, "Label", ""); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* @param propertyType
* @param multiple
* @return the default implementation of PropertyEditorType for the given property Type
* and multiplicity
*/
public PropertyEditorType getDefaultEditorType(Type propertyType, boolean multiple) {
String propertyEditorName = null;
switch(propertyType) {
case BOOLEAN:
propertyEditorName = multiple ? "MultiBoolean" : "BooleanRadio"; //$NON-NLS-1$ //$NON-NLS-2$
break;
case ENUMERATION:
propertyEditorName = multiple ? "MultiEnum" : "EnumCombo"; //$NON-NLS-1$ //$NON-NLS-2$
break;
case INTEGER:
propertyEditorName = multiple ? "MultiInteger" : "IntegerEditor"; //$NON-NLS-1$ //$NON-NLS-2$
break;
case REFERENCE:
propertyEditorName = multiple ? "MultiReference" : "ReferenceDialog"; //$NON-NLS-1$ //$NON-NLS-2$
break;
case STRING:
propertyEditorName = multiple ? "MultiString" : "StringEditor"; //$NON-NLS-1$ //$NON-NLS-2$
break;
}
if(propertyEditorName == null) {
return null;
}
return getDefaultWidget(EnvironmentPackage.ENVIRONMENT__PROPERTY_EDITOR_TYPES, PropertyEditorType.class, propertyEditorName, "ppe"); //$NON-NLS-1$
}
/**
* Returns the default XWT namespaces
*
* @return the default XWT namespaces
*/
public Set<Namespace> getBaseNamespaces() {
Set<Namespace> result = new HashSet<Namespace>();
result.add(getNamespaceByName("")); //$NON-NLS-1$
result.add(getNamespaceByName("x")); //$NON-NLS-1$
result.add(getNamespaceByName("j")); //$NON-NLS-1$
return result;
}
/**
* @param name
* @return
* The namespace corresponding to the given name
*/
public Namespace getNamespaceByName(String name) {
for(Environment environment : root.getEnvironments()) {
for(Namespace namespace : environment.getNamespaces()) {
if(PropertiesUtil.namespaceEqualsByName(namespace, name)) {
return namespace;
}
}
}
Activator.log.warn("Cannot find a registered namespace for '" + name + "'"); //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
/**
* @param property
* @return
* the default PropertyEditorType for the given Property
*/
public PropertyEditorType getDefaultEditorType(Property property) {
return getDefaultEditorType(property.getType(), property.getMultiplicity() != 1);
}
/**
* Disable, then unregisters a Context. The Context won't be available anymore in the framework
* (not even in the Preferences page). This method <strong>won't</strong> delete the context's files
* on the file system.
*
* @param context
* The context to delete
*/
public void deleteContext(Context context) {
if(!isCustomizable(context)) {
throw new IllegalStateException("Non-customizable contexts cannot be deleted. Trying to delete " + context.getName());
}
Resource resource = context.eResource();
contexts.remove(EcoreUtil.getURI(context));
disableContext(context, true);
root.getContexts().remove(context);
resource.unload();
resourceSet.getResources().remove(resource);
}
/**
* Initializes the ConfigurationManager instance. This method should be called only once
*/
public static void init() {
instance.start();
}
/**
* Retrieves the Property object associated to the propertyPath in the given context
*
* @param propertyPath
* @param context
* @return
* The property associated to the given propertyPath
*/
public Property getProperty(String propertyPath, Context context) {
String elementName = propertyPath.substring(0, propertyPath.lastIndexOf(":")); //$NON-NLS-1$
String propertyName = propertyPath.substring(propertyPath.lastIndexOf(":") + 1, propertyPath.length()); //$NON-NLS-1$
Set<DataContextElement> elements = new HashSet<DataContextElement>();
Collection<Context> allContexts;
if(context == null) {
allContexts = getContexts();
} else {
allContexts = PropertiesUtil.getDependencies(context);
}
for(Context ctx : allContexts) {
elements.addAll(ctx.getDataContexts());
}
DataContextElement element = PropertiesUtil.getContextElementByQualifiedName(elementName, elements);
if(element != null) {
for(Property property : element.getProperties()) {
if(property.getName().equals(propertyName)) {
return property;
}
}
}
return null;
}
/**
* Updates the constraint engine to handle changes in the contexts
* activation
*/
public void update() {
constraintEngine.refresh();
}
/**
* Checks the conflicts between all applied configurations
* A Conflict may occur when two sections have the same ID : they can't
* be displayed at the same time
*
* @return
* The list of conflicts
*/
public Collection<ConfigurationConflict> checkConflicts() {
Map<String, List<Context>> sections = new HashMap<String, List<Context>>();
Map<String, ConfigurationConflict> conflicts = new HashMap<String, ConfigurationConflict>();
for(Context context : getEnabledContexts()) {
for(Tab tab : context.getTabs()) {
for(Section section : tab.getSections()) {
String sectionID = section.getName();
List<Context> contexts = sections.get(sectionID);
if(contexts == null) {
contexts = new LinkedList<Context>();
sections.put(sectionID, contexts);
} else {
ConfigurationConflict conflict = conflicts.get(sectionID);
if(conflict == null) {
conflict = new ConfigurationConflict(sectionID);
conflicts.put(sectionID, conflict);
conflict.addContext(contexts.get(0));
}
conflict.addContext(context);
}
contexts.add(context);
}
}
}
return conflicts.values();
}
/**
* Returns the ResourceSet associated to the ConfigurationManager,
* ie. the ResourceSet containing all Environments and Contexts
*
* @return
*/
public ResourceSet getResourceSet() {
return resourceSet;
}
public boolean isCustomizable(Context propertyViewConfiguration) {
if(customizableContexts.containsKey(propertyViewConfiguration)) {
return customizableContexts.get(propertyViewConfiguration);
}
//Default value for isCustomizable is true. However, if the context is
//not stored in customizableContexts, then it's an error. We should
//disable customization tools for this one...
return false;
}
}