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=c4d0dd294efe056e9d31e96f20c3a94212f2ec24;hb=969bd23cab98a79ca9101af33334000879fb60c5;hp=0000000000000000000000000000000000000000;hpb=866dba5cd5a3929bbeae85991796acb212338a08;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 new file mode 100644 index 000000000..c4d0dd294 --- /dev/null +++ b/bundles/org.simantics.ui/src/org/simantics/ui/workbench/editor/EditorRegistry.java @@ -0,0 +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); + } + +}