/******************************************************************************* * 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.Comparator; 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 final Comparator ADAPTER_COMPARATOR = (o1, o2) -> -(o1.getPriority() - o2.getPriority()); 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); Arrays.sort(result, ADAPTER_COMPARATOR); 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); } }