X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.ui%2Fsrc%2Forg%2Fsimantics%2Fui%2Fworkbench%2Feditor%2FEditorRegistry.java;fp=bundles%2Forg.simantics.ui%2Fsrc%2Forg%2Fsimantics%2Fui%2Fworkbench%2Feditor%2FEditorRegistry.java;h=b952966e493509ad5e11e6b8897b29273c52deaa;hb=0ae2b770234dfc3cbb18bd38f324125cf0faca07;hp=c4d0dd294efe056e9d31e96f20c3a94212f2ec24;hpb=24e2b34260f219f0d1644ca7a138894980e25b14;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.ui/src/org/simantics/ui/workbench/editor/EditorRegistry.java b/bundles/org.simantics.ui/src/org/simantics/ui/workbench/editor/EditorRegistry.java index c4d0dd294..b952966e4 100644 --- a/bundles/org.simantics.ui/src/org/simantics/ui/workbench/editor/EditorRegistry.java +++ b/bundles/org.simantics.ui/src/org/simantics/ui/workbench/editor/EditorRegistry.java @@ -1,692 +1,692 @@ -/******************************************************************************* - * Copyright (c) 2007, 2010 Association for Decentralized Information Management - * in Industry THTH ry. - * 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: - * VTT Technical Research Centre of Finland - initial API and implementation - *******************************************************************************/ -package org.simantics.ui.workbench.editor; - -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; - -import org.eclipse.core.commands.contexts.ContextManagerEvent; -import org.eclipse.core.commands.contexts.IContextManagerListener; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.IConfigurationElement; -import org.eclipse.core.runtime.IExtension; -import org.eclipse.core.runtime.IExtensionPoint; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.MultiStatus; -import org.eclipse.core.runtime.Platform; -import org.eclipse.core.runtime.Status; -import org.eclipse.core.runtime.dynamichelpers.ExtensionTracker; -import org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler; -import org.eclipse.core.runtime.dynamichelpers.IExtensionTracker; -import org.eclipse.core.runtime.dynamichelpers.IFilter; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.ui.IEditorDescriptor; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.contexts.IContextService; -import org.osgi.framework.Bundle; -import org.simantics.databoard.Bindings; -import org.simantics.db.ReadGraph; -import org.simantics.db.Resource; -import org.simantics.db.common.request.PossibleIndexRoot; -import org.simantics.db.common.utils.Logger; -import org.simantics.db.common.utils.NameUtils; -import org.simantics.db.exception.DatabaseException; -import org.simantics.db.layer0.adapter.Instances; -import org.simantics.modeling.ModelingResources; -import org.simantics.scl.reflection.OntologyVersions; -import org.simantics.ui.internal.Activator; -import org.simantics.ui.utils.ResourceAdaptionUtils; -import org.simantics.utils.datastructures.MapList; -import org.simantics.utils.ui.ExceptionUtils; -import org.simantics.utils.ui.action.IPriorityAction; - - -/** - * @author Tuukka Lehtonen - */ -public final class EditorRegistry implements IExtensionChangeHandler, IEditorRegistry { - - /** - * The maximum amount of entries to cache - * {@link #getAdaptersFor(ReadGraph, Resource)} results for. Context activation - * changes invalidate this cache. - */ - private static final int MAX_CACHE_SIZE = 50; - - - private final static String NAMESPACE = Activator.PLUGIN_ID; - - private final static String EP_NAME = "resourceEditorAdapter"; - - - private final static String EL_NAME_ADAPTER = "adapter"; - - private final static String EL_NAME_ADAPTERCLASS = "adapterClass"; - - private final static String EL_NAME_GROUP = "group"; - - private final static String EL_NAME_IN_CONTEXT = "inContext"; - - - private static final String ATTR_CLASS = "class"; - - private static final String ATTR_IMAGE = "image"; - - private static final String ATTR_LABEL = "label"; - - private static final String ATTR_TYPE_URIS = "type_uris"; - - private static final String ATTR_EDITOR_ID = "editorId"; - - private static final String ATTR_PRIORITY = "priority"; - - private static final String ATTR_GROUP_ID = "groupId"; - - private static final String ATTR_ID = "id"; - - - private static class Group { - public final String id; - public final List adapters; - - Group(String id) { - this.id = id; - this.adapters = new ArrayList(); - } - Group(Group g) { - this.id = g.id; - this.adapters = new ArrayList(g.adapters); - } - void add(EditorAdapterDescriptor desc) { - adapters.add(desc); - } - void remove(EditorAdapterDescriptor desc) { - adapters.remove(desc); - } - } - - private static final EditorAdapter[] EMPTY_ADAPTER_ARRAY = new EditorAdapter[0]; - - private static EditorRegistry INSTANCE; - - private ExtensionTracker tracker; - - private EditorAdapterDescriptorImpl[] extensions = new EditorAdapterDescriptorImpl[0]; - - private Map idToExtension = new HashMap(); - - private Map groupMap = new HashMap(); - - private WeakHashMap adapterCache = new WeakHashMap( - MAX_CACHE_SIZE); - - private CircularBuffer cacheQueue = new CircularBuffer( - MAX_CACHE_SIZE); - - /** - * A set of all the context id's that are currently referenced by all the - * loaded resourceEditorAdapter extensions. - */ - private Set referencedContextIds = new HashSet(); - - /** - * The current set of active contexts. - */ - private Set activeContextIds = new HashSet(); - - /** - * Used to store all input -> editor mappings. In the Eclipse IDE, this - * information is stored as persistent properties in each IFile represented - * by eclipse Resource's. This implementation stores all the mappings in - * this single map. - * - * Maybe in the future it would be possible to store these mapping in the - * graph in a way that allows us not to publish those changes to the outside - * world. - */ - private final EditorMappingImpl editorMapping = new EditorMappingImpl(); - - public synchronized static IEditorRegistry getInstance() { - if (INSTANCE == null) { - INSTANCE = new EditorRegistry(); - } - return INSTANCE; - } - - public static synchronized void dispose() { - if (INSTANCE != null) { - INSTANCE.close(); - INSTANCE = null; - } - } - - private EditorRegistry() { - tracker = new ExtensionTracker(); - - // Cache defined actions - IExtensionPoint expt = Platform.getExtensionRegistry().getExtensionPoint(NAMESPACE, EP_NAME); - loadExtensions(expt.getConfigurationElements()); - - // Start tracking for new and removed extensions - IFilter filter = ExtensionTracker.createExtensionPointFilter(expt); - tracker.registerHandler(this, filter); - - hookListeners(); - } - - private void close() { - unhookListeners(); - - tracker.close(); - tracker = null; - - editorMapping.clear(); - - extensions = null; - idToExtension = null; - groupMap = null; - adapterCache = null; - cacheQueue = null; - } - -// /** -// * Must reset {@link #getAdaptersFor(ReadGraph, Resource)} query caches when -// * perspectives are changed because EditorAdapters may return -// * different results in different perspectives. -// */ -// private IPerspectiveListener perspectiveListener = new PerspectiveAdapter() { -// public void perspectiveActivated(IWorkbenchPage page, IPerspectiveDescriptor perspective) { -// clearCache(); -// } -// }; - - private final IContextManagerListener contextListener = new IContextManagerListener() { - @Override - public void contextManagerChanged(ContextManagerEvent event) { - if (event.isActiveContextsChanged()) { - //System.out.println("EVENT: " + event.isActiveContextsChanged() + ", " + event.isContextChanged() + ", " + event.isContextDefined() + ", " + Arrays.toString(event.getPreviouslyActiveContextIds().toArray())); - - Collection active = event.getContextManager().getActiveContextIds(); - Collection previouslyActive = event.getPreviouslyActiveContextIds(); - - Collection added = new HashSet(4); - Collection removed = new HashSet(4); - - for (Object o : active) { - if (!previouslyActive.contains(o)) - added.add((String) o); - } - for (Object o : previouslyActive) { - if (!active.contains(o)) - removed.add((String) o); - } - - //System.out.println("ADDED: " + Arrays.toString(added.toArray()) + ", REMOVED: " + Arrays.toString(removed.toArray())); - contextChange(added, removed); - } - } - }; - - private boolean containsAny(Collection c, Collection anyOf) { - if (anyOf.isEmpty()) - return false; - for (String s : anyOf) - if (c.contains(s)) - return true; - return false; - } - - private void contextChange(Collection added, Collection removed) { - // Update active context id set - if (!added.isEmpty()) - activeContextIds.addAll(added); - if (!removed.isEmpty()) - activeContextIds.remove(removed); - - // Clear caches if necessary - if (containsAny(referencedContextIds, added) || containsAny(referencedContextIds, removed)) { - clearCache(); - } - } - - @SuppressWarnings("unchecked") - private void hookListeners() { - IWorkbench wb = PlatformUI.getWorkbench(); - IContextService contextService = (IContextService) wb.getService(IContextService.class); - contextService.addContextManagerListener(contextListener); - activeContextIds = new HashSet(contextService.getActiveContextIds()); - } - - private void unhookListeners() { - IWorkbench wb = PlatformUI.getWorkbench(); - IContextService contextService = (IContextService) wb.getService(IContextService.class); - if (contextService != null) { - contextService.removeContextManagerListener(contextListener); - } - } - - private String[] parseInContexts(IConfigurationElement parent) { - List contexts = null; - for (IConfigurationElement el : parent.getChildren(EL_NAME_IN_CONTEXT)) { - String id = el.getAttribute(ATTR_ID); - if (id != null) { - if (contexts == null) - contexts = new ArrayList(4); - contexts.add(id); - } - } - return contexts != null ? contexts.toArray(new String[contexts.size()]) : null; - } - - private synchronized void loadExtensions(IConfigurationElement[] elements) { - org.eclipse.ui.IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry(); - - Set newExtensions = new HashSet(Arrays.asList(extensions)); - Map newGroups = new HashMap(); - Set newReferencedContextIds = new HashSet(referencedContextIds); - - for (IConfigurationElement el : elements) { - String name = el.getName(); - try { - String id = el.getAttribute(ATTR_ID); - String groupId = el.getAttribute(ATTR_GROUP_ID); - EditorAdapter adapter = null; - - String priority = el.getAttribute(ATTR_PRIORITY); - int pri = IPriorityAction.NORMAL; - try { - if (priority != null && !priority.trim().isEmpty()) - pri = Integer.parseInt(priority); - } catch (NumberFormatException e) { - ExceptionUtils.logError("Non-integer priority value '" + priority + "' for '" + name + "' extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", e); - } - - String[] inContexts = null; - - if (EL_NAME_GROUP.equals(name)) { - if (id == null || id.isEmpty()) { - ExceptionUtils.logWarning("A group extension contributed by " + el.getDeclaringExtension().getContributor().getName() + " does not define a required id.", null); - } else { - if (!newGroups.containsKey(id)) { - newGroups.put(id, new Group(id)); - } - } - continue; - } else if (EL_NAME_ADAPTER.equals(name)) { - String editorId = el.getAttribute(ATTR_EDITOR_ID); - IEditorDescriptor editorDesc = editorRegistry.findEditor(editorId); - if (editorDesc == null) { - ExceptionUtils.logError("Non-existent editorId '" + editorId + "' in extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", null); - continue; - } - - String type_uris = OntologyVersions.getInstance().currentVersion(el.getAttribute(ATTR_TYPE_URIS)); - String[] typeUris = type_uris != null ? type_uris.split(",") : new String[0]; - - String label = el.getAttribute(ATTR_LABEL); - String image = el.getAttribute(ATTR_IMAGE); - ImageDescriptor imageDesc = null; - if (label == null) - label = editorDesc.getLabel(); - if (image != null) { - try { - URL resolved = FileLocator.resolve(new URL(image)); - imageDesc = ImageDescriptor.createFromURL(resolved); - } catch (IOException e) { - // Try fallback method - Bundle bundle = Platform.getBundle(el.getDeclaringExtension().getContributor().getName()); - imageDesc = ImageDescriptor.createFromURL(bundle.getEntry(image)); - } - } else { - imageDesc = editorDesc.getImageDescriptor(); - } - - SimpleEditorAdapter _adapter = new SimpleEditorAdapter(label, imageDesc, editorId, (String[]) null, typeUris); - _adapter.setPriority(pri); - - adapter = _adapter; - - inContexts = parseInContexts(el); - } else if (EL_NAME_ADAPTERCLASS.equals(name)) { - adapter = (EditorAdapter) el.createExecutableExtension(ATTR_CLASS); - if (adapter instanceof Prioritized) { - ((Prioritized) adapter).setPriority(pri); - } - inContexts = parseInContexts(el); - } - - if (adapter != null) { - EditorAdapterDescriptorImpl ext = new EditorAdapterDescriptorImpl(id, groupId, adapter, inContexts); - //System.out.println("Adding editor adapter extension from " + el.getContributor().getName() + ": " + ext.getId() + ", " + ext.getAdapter()); - - // Start tracking the new extension object, its removal will be notified of - // with removeExtension(extension, Object[]). - tracker.registerObject(el.getDeclaringExtension(), ext, IExtensionTracker.REF_STRONG); - - if (id != null && !id.isEmpty()) { - idToExtension.put(id, ext); - } - if (inContexts != null) - for (String ctx : inContexts) - newReferencedContextIds.add(ctx); - - newExtensions.add(ext); - } - } catch (CoreException e) { - ExceptionUtils.logError("Failed to initialize resourceEditorAdapter extension \"" + name + "\": " - + e.getMessage(), e); - } - } - - for (EditorAdapterDescriptorImpl desc : idToExtension.values()) { - if (desc.getGroupId() != null) { - Group g = newGroups.get(desc.getGroupId()); - if (g != null) { - g.add(desc); - } - } - } - - clearCache(); - this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]); - this.groupMap = newGroups; - this.referencedContextIds = newReferencedContextIds; - } - - - @Override - public void addExtension(IExtensionTracker tracker, IExtension extension) { - loadExtensions(extension.getConfigurationElements()); - } - - @Override - public synchronized void removeExtension(IExtension extension, Object[] objects) { - Set newExtensions = new HashSet(Arrays.asList(extensions)); - Map idMap = new HashMap(idToExtension); - Set removedContextReferences = new HashSet(); - - Map newGroups = new HashMap(); - for (Group g : groupMap.values()) { - newGroups.put(g.id, new Group(g)); - } - - for (Object o : objects) { - EditorAdapterDescriptor ext = (EditorAdapterDescriptor) o; - - tracker.unregisterObject(extension, o); - newExtensions.remove(ext); - idMap.remove(ext.getId()); - - if (ext.getGroupId() != null) { - Group g = newGroups.get(ext.getGroupId()); - if (g != null) { - g.remove(ext); - } - } - for (String ctx : ext.getInContexts()) - removedContextReferences.add(ctx); - } - - // Go through the remaining editor adapters and - // check whether they still reference any of the removed - // context ids. Ids that are still referenced will not be - // removed from referencedContextIds - for (EditorAdapterDescriptorImpl desc : newExtensions) { - for (String ctx : desc.getInContexts()) { - removedContextReferences.remove(ctx); - } - } - Set newReferencedContextIds = new HashSet(referencedContextIds); - newReferencedContextIds.removeAll(removedContextReferences); - - // Atomic assignment - this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]); - this.idToExtension = idMap; - this.groupMap = newGroups; - this.referencedContextIds = newReferencedContextIds; - } - - @Override - public EditorAdapterDescriptor getExtensionById(String id) { - return idToExtension.get(id); - } - - @Override - public EditorAdapter getAdapterById(String id) { - EditorAdapterDescriptor ext = idToExtension.get(id); - return ext == null ? null : ext.getAdapter(); - } - - private void clearCache() { - synchronized (adapterCache) { - adapterCache.clear(); - cacheQueue.clear(); - } - } - - @Override - public EditorAdapterDescriptor[] getEditorAdapters() { - return extensions; - } - - @Override - public EditorAdapter[] getAdaptersFor(ReadGraph g, final Object r) throws DatabaseException { - - EditorAdapter[] result; - synchronized (adapterCache) { - result = adapterCache.get(r); - if (result != null) - return result; - } - - MultiStatus status = null; - - final MapList l = new MapList(); - for (EditorAdapterDescriptor a : extensions) { - try { - // Filter out adapters that are not active in the current context configuration. - if (!a.isActive(activeContextIds)) - continue; - // Filter out adapters that just can't handle the input. - if (!a.getAdapter().canHandle(g, r)) - continue; - - // NOTE: Group is null if there is no group. - l.add(a.getGroupId(), a.getAdapter()); - } catch (RuntimeException e) { - if (status == null) - status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null); - status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); - } - } - - - Resource res = ResourceAdaptionUtils.toSingleResource(r); - if (res != null) { - ModelingResources MOD = ModelingResources.getInstance(g); - Resource indexRoot = g.syncRequest(new PossibleIndexRoot(res)); - if (indexRoot != null) { - Instances query = g.adapt(MOD.EditorContribution, Instances.class); - for(Resource contribution : query.find(g, indexRoot)) { - - try { - - String id = g.getRelatedValue(contribution, MOD.EditorContribution_editorId, Bindings.STRING); - String label = NameUtils.getSafeLabel(g, contribution); - - Resource imageResource = g.getPossibleObject(contribution, MOD.EditorContribution_HasImage); - ImageDescriptor image = imageResource == null ? null : g.adapt(imageResource, ImageDescriptor.class); - - Integer priority = g.getRelatedValue(contribution, MOD.EditorContribution_priority, Bindings.INTEGER); - EditorAdapterDescriptor a = new GraphEditorAdapterDescriptor(id, label, image, contribution, priority); - - // Filter out adapters that are not active in the current context configuration. - if (!a.isActive(activeContextIds)) - continue; - // Filter out adapters that just can't handle the input. - if (!a.getAdapter().canHandle(g, r)) - continue; - - l.add(a.getGroupId(), a.getAdapter()); - - } catch (DatabaseException e) { - Logger.defaultLogError(e); - } - } - } - } - - result = gatherAdapterResult(l); - updateCache(r, result); - - if (status != null && !status.isOK()) - Activator.getDefault().getLog().log(status); - - return result; - } - - private EditorAdapter[] gatherAdapterResult(MapList map) { - final List result = new ArrayList(8); - for (String group : map.getKeys()) { - List grp = map.getValues(group); - if (group == null) { - if (grp != null) - result.addAll(grp); - } else { - EditorAdapter highestPriorityAdapter = null; - for (EditorAdapter adapter : grp) { - if (highestPriorityAdapter == null || adapter.getPriority() > highestPriorityAdapter.getPriority()) { - highestPriorityAdapter = adapter; - } - } - result.add(highestPriorityAdapter); - } - } - return result.toArray(EMPTY_ADAPTER_ARRAY); - } - - @Override - public EditorMapping getMappings() { - return editorMapping; - } - - private void updateCache(Object r, EditorAdapter[] result) { - synchronized (adapterCache) { - adapterCache.put(r, result); - Object removed = cacheQueue.write(r); - if (removed != null) { - adapterCache.remove(removed); - } - } - } - - - private static class CircularBuffer { - private final WeakReference[] buffer; - - private int head; - private int tail; - private boolean full; - private final int size; - - CircularBuffer(int size) { - if (size == 0) - throw new IllegalArgumentException("zero size not allowed"); - - this.buffer = new WeakReference[size]; - this.size = size; - clear(); - } - - public void clear() { - this.head = this.tail = 0; - this.full = false; - Arrays.fill(buffer, null); - } - - /** - * @param id an ID, other than 0L - * @return 0L if the buffer was not yet full, otherwise - * @throws IllegalArgumentException for 0L id - */ - @SuppressWarnings("unchecked") - T write(T id) { - if (id == null) - throw new IllegalArgumentException("null resource id"); - - if (full) { - WeakReference prev = buffer[head]; - buffer[head++] = new WeakReference(id); - head %= size; - tail = head; - return (T) prev.get(); - } else { - buffer[head++] = new WeakReference(id); - head %= size; - if (head == tail) { - full = true; - } - } - // Nothing was yet overwritten - return null; - } - - @Override - public String toString() { - return Arrays.toString(buffer); - } - } - - - @Override - public EditorAdapter[] getDefaultAdaptersFor(ReadGraph g, Object r) throws DatabaseException { - EditorAdapter[] results; - - MultiStatus status = null; - - final MapList l = new MapList(); - for (EditorAdapterDescriptor a : extensions) { - try { - - // NOTE: Group is null if there is no group. - l.add(a.getGroupId(), a.getAdapter()); - } catch (RuntimeException e) { - if (status == null) - status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null); - status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); - } - } - results = gatherAdapterResult(l); - - if (status != null && !status.isOK()) - Activator.getDefault().getLog().log(status); - - // If no default editor is found, get all that can handle - if (results.length > 0) - return results; - else - return getAdaptersFor(g, r); - } - -} +/******************************************************************************* + * Copyright (c) 2007, 2010 Association for Decentralized Information Management + * in Industry THTH ry. + * 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: + * VTT Technical Research Centre of Finland - initial API and implementation + *******************************************************************************/ +package org.simantics.ui.workbench.editor; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.eclipse.core.commands.contexts.ContextManagerEvent; +import org.eclipse.core.commands.contexts.IContextManagerListener; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtension; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.dynamichelpers.ExtensionTracker; +import org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler; +import org.eclipse.core.runtime.dynamichelpers.IExtensionTracker; +import org.eclipse.core.runtime.dynamichelpers.IFilter; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.contexts.IContextService; +import org.osgi.framework.Bundle; +import org.simantics.databoard.Bindings; +import org.simantics.db.ReadGraph; +import org.simantics.db.Resource; +import org.simantics.db.common.request.PossibleIndexRoot; +import org.simantics.db.common.utils.Logger; +import org.simantics.db.common.utils.NameUtils; +import org.simantics.db.exception.DatabaseException; +import org.simantics.db.layer0.adapter.Instances; +import org.simantics.modeling.ModelingResources; +import org.simantics.scl.reflection.OntologyVersions; +import org.simantics.ui.internal.Activator; +import org.simantics.ui.utils.ResourceAdaptionUtils; +import org.simantics.utils.datastructures.MapList; +import org.simantics.utils.ui.ExceptionUtils; +import org.simantics.utils.ui.action.IPriorityAction; + + +/** + * @author Tuukka Lehtonen + */ +public final class EditorRegistry implements IExtensionChangeHandler, IEditorRegistry { + + /** + * The maximum amount of entries to cache + * {@link #getAdaptersFor(ReadGraph, Resource)} results for. Context activation + * changes invalidate this cache. + */ + private static final int MAX_CACHE_SIZE = 50; + + + private final static String NAMESPACE = Activator.PLUGIN_ID; + + private final static String EP_NAME = "resourceEditorAdapter"; + + + private final static String EL_NAME_ADAPTER = "adapter"; + + private final static String EL_NAME_ADAPTERCLASS = "adapterClass"; + + private final static String EL_NAME_GROUP = "group"; + + private final static String EL_NAME_IN_CONTEXT = "inContext"; + + + private static final String ATTR_CLASS = "class"; + + private static final String ATTR_IMAGE = "image"; + + private static final String ATTR_LABEL = "label"; + + private static final String ATTR_TYPE_URIS = "type_uris"; + + private static final String ATTR_EDITOR_ID = "editorId"; + + private static final String ATTR_PRIORITY = "priority"; + + private static final String ATTR_GROUP_ID = "groupId"; + + private static final String ATTR_ID = "id"; + + + private static class Group { + public final String id; + public final List adapters; + + Group(String id) { + this.id = id; + this.adapters = new ArrayList(); + } + Group(Group g) { + this.id = g.id; + this.adapters = new ArrayList(g.adapters); + } + void add(EditorAdapterDescriptor desc) { + adapters.add(desc); + } + void remove(EditorAdapterDescriptor desc) { + adapters.remove(desc); + } + } + + private static final EditorAdapter[] EMPTY_ADAPTER_ARRAY = new EditorAdapter[0]; + + private static EditorRegistry INSTANCE; + + private ExtensionTracker tracker; + + private EditorAdapterDescriptorImpl[] extensions = new EditorAdapterDescriptorImpl[0]; + + private Map idToExtension = new HashMap(); + + private Map groupMap = new HashMap(); + + private WeakHashMap adapterCache = new WeakHashMap( + MAX_CACHE_SIZE); + + private CircularBuffer cacheQueue = new CircularBuffer( + MAX_CACHE_SIZE); + + /** + * A set of all the context id's that are currently referenced by all the + * loaded resourceEditorAdapter extensions. + */ + private Set referencedContextIds = new HashSet(); + + /** + * The current set of active contexts. + */ + private Set activeContextIds = new HashSet(); + + /** + * Used to store all input -> editor mappings. In the Eclipse IDE, this + * information is stored as persistent properties in each IFile represented + * by eclipse Resource's. This implementation stores all the mappings in + * this single map. + * + * Maybe in the future it would be possible to store these mapping in the + * graph in a way that allows us not to publish those changes to the outside + * world. + */ + private final EditorMappingImpl editorMapping = new EditorMappingImpl(); + + public synchronized static IEditorRegistry getInstance() { + if (INSTANCE == null) { + INSTANCE = new EditorRegistry(); + } + return INSTANCE; + } + + public static synchronized void dispose() { + if (INSTANCE != null) { + INSTANCE.close(); + INSTANCE = null; + } + } + + private EditorRegistry() { + tracker = new ExtensionTracker(); + + // Cache defined actions + IExtensionPoint expt = Platform.getExtensionRegistry().getExtensionPoint(NAMESPACE, EP_NAME); + loadExtensions(expt.getConfigurationElements()); + + // Start tracking for new and removed extensions + IFilter filter = ExtensionTracker.createExtensionPointFilter(expt); + tracker.registerHandler(this, filter); + + hookListeners(); + } + + private void close() { + unhookListeners(); + + tracker.close(); + tracker = null; + + editorMapping.clear(); + + extensions = null; + idToExtension = null; + groupMap = null; + adapterCache = null; + cacheQueue = null; + } + +// /** +// * Must reset {@link #getAdaptersFor(ReadGraph, Resource)} query caches when +// * perspectives are changed because EditorAdapters may return +// * different results in different perspectives. +// */ +// private IPerspectiveListener perspectiveListener = new PerspectiveAdapter() { +// public void perspectiveActivated(IWorkbenchPage page, IPerspectiveDescriptor perspective) { +// clearCache(); +// } +// }; + + private final IContextManagerListener contextListener = new IContextManagerListener() { + @Override + public void contextManagerChanged(ContextManagerEvent event) { + if (event.isActiveContextsChanged()) { + //System.out.println("EVENT: " + event.isActiveContextsChanged() + ", " + event.isContextChanged() + ", " + event.isContextDefined() + ", " + Arrays.toString(event.getPreviouslyActiveContextIds().toArray())); + + Collection active = event.getContextManager().getActiveContextIds(); + Collection previouslyActive = event.getPreviouslyActiveContextIds(); + + Collection added = new HashSet(4); + Collection removed = new HashSet(4); + + for (Object o : active) { + if (!previouslyActive.contains(o)) + added.add((String) o); + } + for (Object o : previouslyActive) { + if (!active.contains(o)) + removed.add((String) o); + } + + //System.out.println("ADDED: " + Arrays.toString(added.toArray()) + ", REMOVED: " + Arrays.toString(removed.toArray())); + contextChange(added, removed); + } + } + }; + + private boolean containsAny(Collection c, Collection anyOf) { + if (anyOf.isEmpty()) + return false; + for (String s : anyOf) + if (c.contains(s)) + return true; + return false; + } + + private void contextChange(Collection added, Collection removed) { + // Update active context id set + if (!added.isEmpty()) + activeContextIds.addAll(added); + if (!removed.isEmpty()) + activeContextIds.remove(removed); + + // Clear caches if necessary + if (containsAny(referencedContextIds, added) || containsAny(referencedContextIds, removed)) { + clearCache(); + } + } + + @SuppressWarnings("unchecked") + private void hookListeners() { + IWorkbench wb = PlatformUI.getWorkbench(); + IContextService contextService = (IContextService) wb.getService(IContextService.class); + contextService.addContextManagerListener(contextListener); + activeContextIds = new HashSet(contextService.getActiveContextIds()); + } + + private void unhookListeners() { + IWorkbench wb = PlatformUI.getWorkbench(); + IContextService contextService = (IContextService) wb.getService(IContextService.class); + if (contextService != null) { + contextService.removeContextManagerListener(contextListener); + } + } + + private String[] parseInContexts(IConfigurationElement parent) { + List contexts = null; + for (IConfigurationElement el : parent.getChildren(EL_NAME_IN_CONTEXT)) { + String id = el.getAttribute(ATTR_ID); + if (id != null) { + if (contexts == null) + contexts = new ArrayList(4); + contexts.add(id); + } + } + return contexts != null ? contexts.toArray(new String[contexts.size()]) : null; + } + + private synchronized void loadExtensions(IConfigurationElement[] elements) { + org.eclipse.ui.IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry(); + + Set newExtensions = new HashSet(Arrays.asList(extensions)); + Map newGroups = new HashMap(); + Set newReferencedContextIds = new HashSet(referencedContextIds); + + for (IConfigurationElement el : elements) { + String name = el.getName(); + try { + String id = el.getAttribute(ATTR_ID); + String groupId = el.getAttribute(ATTR_GROUP_ID); + EditorAdapter adapter = null; + + String priority = el.getAttribute(ATTR_PRIORITY); + int pri = IPriorityAction.NORMAL; + try { + if (priority != null && !priority.trim().isEmpty()) + pri = Integer.parseInt(priority); + } catch (NumberFormatException e) { + ExceptionUtils.logError("Non-integer priority value '" + priority + "' for '" + name + "' extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", e); + } + + String[] inContexts = null; + + if (EL_NAME_GROUP.equals(name)) { + if (id == null || id.isEmpty()) { + ExceptionUtils.logWarning("A group extension contributed by " + el.getDeclaringExtension().getContributor().getName() + " does not define a required id.", null); + } else { + if (!newGroups.containsKey(id)) { + newGroups.put(id, new Group(id)); + } + } + continue; + } else if (EL_NAME_ADAPTER.equals(name)) { + String editorId = el.getAttribute(ATTR_EDITOR_ID); + IEditorDescriptor editorDesc = editorRegistry.findEditor(editorId); + if (editorDesc == null) { + ExceptionUtils.logError("Non-existent editorId '" + editorId + "' in extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", null); + continue; + } + + String type_uris = OntologyVersions.getInstance().currentVersion(el.getAttribute(ATTR_TYPE_URIS)); + String[] typeUris = type_uris != null ? type_uris.split(",") : new String[0]; + + String label = el.getAttribute(ATTR_LABEL); + String image = el.getAttribute(ATTR_IMAGE); + ImageDescriptor imageDesc = null; + if (label == null) + label = editorDesc.getLabel(); + if (image != null) { + try { + URL resolved = FileLocator.resolve(new URL(image)); + imageDesc = ImageDescriptor.createFromURL(resolved); + } catch (IOException e) { + // Try fallback method + Bundle bundle = Platform.getBundle(el.getDeclaringExtension().getContributor().getName()); + imageDesc = ImageDescriptor.createFromURL(bundle.getEntry(image)); + } + } else { + imageDesc = editorDesc.getImageDescriptor(); + } + + SimpleEditorAdapter _adapter = new SimpleEditorAdapter(label, imageDesc, editorId, (String[]) null, typeUris); + _adapter.setPriority(pri); + + adapter = _adapter; + + inContexts = parseInContexts(el); + } else if (EL_NAME_ADAPTERCLASS.equals(name)) { + adapter = (EditorAdapter) el.createExecutableExtension(ATTR_CLASS); + if (adapter instanceof Prioritized) { + ((Prioritized) adapter).setPriority(pri); + } + inContexts = parseInContexts(el); + } + + if (adapter != null) { + EditorAdapterDescriptorImpl ext = new EditorAdapterDescriptorImpl(id, groupId, adapter, inContexts); + //System.out.println("Adding editor adapter extension from " + el.getContributor().getName() + ": " + ext.getId() + ", " + ext.getAdapter()); + + // Start tracking the new extension object, its removal will be notified of + // with removeExtension(extension, Object[]). + tracker.registerObject(el.getDeclaringExtension(), ext, IExtensionTracker.REF_STRONG); + + if (id != null && !id.isEmpty()) { + idToExtension.put(id, ext); + } + if (inContexts != null) + for (String ctx : inContexts) + newReferencedContextIds.add(ctx); + + newExtensions.add(ext); + } + } catch (CoreException e) { + ExceptionUtils.logError("Failed to initialize resourceEditorAdapter extension \"" + name + "\": " + + e.getMessage(), e); + } + } + + for (EditorAdapterDescriptorImpl desc : idToExtension.values()) { + if (desc.getGroupId() != null) { + Group g = newGroups.get(desc.getGroupId()); + if (g != null) { + g.add(desc); + } + } + } + + clearCache(); + this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]); + this.groupMap = newGroups; + this.referencedContextIds = newReferencedContextIds; + } + + + @Override + public void addExtension(IExtensionTracker tracker, IExtension extension) { + loadExtensions(extension.getConfigurationElements()); + } + + @Override + public synchronized void removeExtension(IExtension extension, Object[] objects) { + Set newExtensions = new HashSet(Arrays.asList(extensions)); + Map idMap = new HashMap(idToExtension); + Set removedContextReferences = new HashSet(); + + Map newGroups = new HashMap(); + for (Group g : groupMap.values()) { + newGroups.put(g.id, new Group(g)); + } + + for (Object o : objects) { + EditorAdapterDescriptor ext = (EditorAdapterDescriptor) o; + + tracker.unregisterObject(extension, o); + newExtensions.remove(ext); + idMap.remove(ext.getId()); + + if (ext.getGroupId() != null) { + Group g = newGroups.get(ext.getGroupId()); + if (g != null) { + g.remove(ext); + } + } + for (String ctx : ext.getInContexts()) + removedContextReferences.add(ctx); + } + + // Go through the remaining editor adapters and + // check whether they still reference any of the removed + // context ids. Ids that are still referenced will not be + // removed from referencedContextIds + for (EditorAdapterDescriptorImpl desc : newExtensions) { + for (String ctx : desc.getInContexts()) { + removedContextReferences.remove(ctx); + } + } + Set newReferencedContextIds = new HashSet(referencedContextIds); + newReferencedContextIds.removeAll(removedContextReferences); + + // Atomic assignment + this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]); + this.idToExtension = idMap; + this.groupMap = newGroups; + this.referencedContextIds = newReferencedContextIds; + } + + @Override + public EditorAdapterDescriptor getExtensionById(String id) { + return idToExtension.get(id); + } + + @Override + public EditorAdapter getAdapterById(String id) { + EditorAdapterDescriptor ext = idToExtension.get(id); + return ext == null ? null : ext.getAdapter(); + } + + private void clearCache() { + synchronized (adapterCache) { + adapterCache.clear(); + cacheQueue.clear(); + } + } + + @Override + public EditorAdapterDescriptor[] getEditorAdapters() { + return extensions; + } + + @Override + public EditorAdapter[] getAdaptersFor(ReadGraph g, final Object r) throws DatabaseException { + + EditorAdapter[] result; + synchronized (adapterCache) { + result = adapterCache.get(r); + if (result != null) + return result; + } + + MultiStatus status = null; + + final MapList l = new MapList(); + for (EditorAdapterDescriptor a : extensions) { + try { + // Filter out adapters that are not active in the current context configuration. + if (!a.isActive(activeContextIds)) + continue; + // Filter out adapters that just can't handle the input. + if (!a.getAdapter().canHandle(g, r)) + continue; + + // NOTE: Group is null if there is no group. + l.add(a.getGroupId(), a.getAdapter()); + } catch (RuntimeException e) { + if (status == null) + status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null); + status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); + } + } + + + Resource res = ResourceAdaptionUtils.toSingleResource(r); + if (res != null) { + ModelingResources MOD = ModelingResources.getInstance(g); + Resource indexRoot = g.syncRequest(new PossibleIndexRoot(res)); + if (indexRoot != null) { + Instances query = g.adapt(MOD.EditorContribution, Instances.class); + for(Resource contribution : query.find(g, indexRoot)) { + + try { + + String id = g.getRelatedValue(contribution, MOD.EditorContribution_editorId, Bindings.STRING); + String label = NameUtils.getSafeLabel(g, contribution); + + Resource imageResource = g.getPossibleObject(contribution, MOD.EditorContribution_HasImage); + ImageDescriptor image = imageResource == null ? null : g.adapt(imageResource, ImageDescriptor.class); + + Integer priority = g.getRelatedValue(contribution, MOD.EditorContribution_priority, Bindings.INTEGER); + EditorAdapterDescriptor a = new GraphEditorAdapterDescriptor(id, label, image, contribution, priority); + + // Filter out adapters that are not active in the current context configuration. + if (!a.isActive(activeContextIds)) + continue; + // Filter out adapters that just can't handle the input. + if (!a.getAdapter().canHandle(g, r)) + continue; + + l.add(a.getGroupId(), a.getAdapter()); + + } catch (DatabaseException e) { + Logger.defaultLogError(e); + } + } + } + } + + result = gatherAdapterResult(l); + updateCache(r, result); + + if (status != null && !status.isOK()) + Activator.getDefault().getLog().log(status); + + return result; + } + + private EditorAdapter[] gatherAdapterResult(MapList map) { + final List result = new ArrayList(8); + for (String group : map.getKeys()) { + List grp = map.getValues(group); + if (group == null) { + if (grp != null) + result.addAll(grp); + } else { + EditorAdapter highestPriorityAdapter = null; + for (EditorAdapter adapter : grp) { + if (highestPriorityAdapter == null || adapter.getPriority() > highestPriorityAdapter.getPriority()) { + highestPriorityAdapter = adapter; + } + } + result.add(highestPriorityAdapter); + } + } + return result.toArray(EMPTY_ADAPTER_ARRAY); + } + + @Override + public EditorMapping getMappings() { + return editorMapping; + } + + private void updateCache(Object r, EditorAdapter[] result) { + synchronized (adapterCache) { + adapterCache.put(r, result); + Object removed = cacheQueue.write(r); + if (removed != null) { + adapterCache.remove(removed); + } + } + } + + + private static class CircularBuffer { + private final WeakReference[] buffer; + + private int head; + private int tail; + private boolean full; + private final int size; + + CircularBuffer(int size) { + if (size == 0) + throw new IllegalArgumentException("zero size not allowed"); + + this.buffer = new WeakReference[size]; + this.size = size; + clear(); + } + + public void clear() { + this.head = this.tail = 0; + this.full = false; + Arrays.fill(buffer, null); + } + + /** + * @param id an ID, other than 0L + * @return 0L if the buffer was not yet full, otherwise + * @throws IllegalArgumentException for 0L id + */ + @SuppressWarnings("unchecked") + T write(T id) { + if (id == null) + throw new IllegalArgumentException("null resource id"); + + if (full) { + WeakReference prev = buffer[head]; + buffer[head++] = new WeakReference(id); + head %= size; + tail = head; + return (T) prev.get(); + } else { + buffer[head++] = new WeakReference(id); + head %= size; + if (head == tail) { + full = true; + } + } + // Nothing was yet overwritten + return null; + } + + @Override + public String toString() { + return Arrays.toString(buffer); + } + } + + + @Override + public EditorAdapter[] getDefaultAdaptersFor(ReadGraph g, Object r) throws DatabaseException { + EditorAdapter[] results; + + MultiStatus status = null; + + final MapList l = new MapList(); + for (EditorAdapterDescriptor a : extensions) { + try { + + // NOTE: Group is null if there is no group. + l.add(a.getGroupId(), a.getAdapter()); + } catch (RuntimeException e) { + if (status == null) + status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null); + status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e)); + } + } + results = gatherAdapterResult(l); + + if (status != null && !status.isOK()) + Activator.getDefault().getLog().log(status); + + // If no default editor is found, get all that can handle + if (results.length > 0) + return results; + else + return getAdaptersFor(g, r); + } + +}