1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.ui.workbench.editor;
14 import java.io.IOException;
15 import java.lang.ref.WeakReference;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Comparator;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
26 import java.util.WeakHashMap;
28 import org.eclipse.core.commands.contexts.ContextManagerEvent;
29 import org.eclipse.core.commands.contexts.IContextManagerListener;
30 import org.eclipse.core.runtime.CoreException;
31 import org.eclipse.core.runtime.FileLocator;
32 import org.eclipse.core.runtime.IConfigurationElement;
33 import org.eclipse.core.runtime.IExtension;
34 import org.eclipse.core.runtime.IExtensionPoint;
35 import org.eclipse.core.runtime.IStatus;
36 import org.eclipse.core.runtime.MultiStatus;
37 import org.eclipse.core.runtime.Platform;
38 import org.eclipse.core.runtime.Status;
39 import org.eclipse.core.runtime.dynamichelpers.ExtensionTracker;
40 import org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler;
41 import org.eclipse.core.runtime.dynamichelpers.IExtensionTracker;
42 import org.eclipse.core.runtime.dynamichelpers.IFilter;
43 import org.eclipse.jface.resource.ImageDescriptor;
44 import org.eclipse.ui.IEditorDescriptor;
45 import org.eclipse.ui.IWorkbench;
46 import org.eclipse.ui.PlatformUI;
47 import org.eclipse.ui.contexts.IContextService;
48 import org.osgi.framework.Bundle;
49 import org.simantics.databoard.Bindings;
50 import org.simantics.db.ReadGraph;
51 import org.simantics.db.Resource;
52 import org.simantics.db.common.request.PossibleIndexRoot;
53 import org.simantics.db.common.utils.Logger;
54 import org.simantics.db.common.utils.NameUtils;
55 import org.simantics.db.exception.DatabaseException;
56 import org.simantics.db.layer0.adapter.Instances;
57 import org.simantics.modeling.ModelingResources;
58 import org.simantics.scl.reflection.OntologyVersions;
59 import org.simantics.ui.internal.Activator;
60 import org.simantics.ui.utils.ResourceAdaptionUtils;
61 import org.simantics.utils.datastructures.MapList;
62 import org.simantics.utils.ui.ExceptionUtils;
63 import org.simantics.utils.ui.action.IPriorityAction;
67 * @author Tuukka Lehtonen
69 public final class EditorRegistry implements IExtensionChangeHandler, IEditorRegistry {
72 * The maximum amount of entries to cache
73 * {@link #getAdaptersFor(ReadGraph, Resource)} results for. Context activation
74 * changes invalidate this cache.
76 private static final int MAX_CACHE_SIZE = 50;
79 private final static String NAMESPACE = Activator.PLUGIN_ID;
81 private final static String EP_NAME = "resourceEditorAdapter";
84 private final static String EL_NAME_ADAPTER = "adapter";
86 private final static String EL_NAME_ADAPTERCLASS = "adapterClass";
88 private final static String EL_NAME_GROUP = "group";
90 private final static String EL_NAME_IN_CONTEXT = "inContext";
93 private static final String ATTR_CLASS = "class";
95 private static final String ATTR_IMAGE = "image";
97 private static final String ATTR_LABEL = "label";
99 private static final String ATTR_TYPE_URIS = "type_uris";
101 private static final String ATTR_EDITOR_ID = "editorId";
103 private static final String ATTR_PRIORITY = "priority";
105 private static final String ATTR_GROUP_ID = "groupId";
107 private static final String ATTR_ID = "id";
109 private static final Comparator<EditorAdapter> ADAPTER_COMPARATOR = (o1, o2) -> -(o1.getPriority() - o2.getPriority());
111 private static class Group {
112 public final String id;
113 public final List<EditorAdapterDescriptor> adapters;
117 this.adapters = new ArrayList<EditorAdapterDescriptor>();
121 this.adapters = new ArrayList<EditorAdapterDescriptor>(g.adapters);
123 void add(EditorAdapterDescriptor desc) {
126 void remove(EditorAdapterDescriptor desc) {
127 adapters.remove(desc);
131 private static final EditorAdapter[] EMPTY_ADAPTER_ARRAY = new EditorAdapter[0];
133 private static EditorRegistry INSTANCE;
135 private ExtensionTracker tracker;
137 private EditorAdapterDescriptorImpl[] extensions = new EditorAdapterDescriptorImpl[0];
139 private Map<String, EditorAdapterDescriptorImpl> idToExtension = new HashMap<String, EditorAdapterDescriptorImpl>();
141 private Map<String, Group> groupMap = new HashMap<String, Group>();
143 private WeakHashMap<Object, EditorAdapter[]> adapterCache = new WeakHashMap<Object, EditorAdapter[]>(
146 private CircularBuffer<Object> cacheQueue = new CircularBuffer<Object>(
150 * A set of all the context id's that are currently referenced by all the
151 * loaded resourceEditorAdapter extensions.
153 private Set<String> referencedContextIds = new HashSet<String>();
156 * The current set of active contexts.
158 private Set<String> activeContextIds = new HashSet<String>();
161 * Used to store all input -> editor mappings. In the Eclipse IDE, this
162 * information is stored as persistent properties in each IFile represented
163 * by eclipse Resource's. This implementation stores all the mappings in
166 * Maybe in the future it would be possible to store these mapping in the
167 * graph in a way that allows us not to publish those changes to the outside
170 private final EditorMappingImpl editorMapping = new EditorMappingImpl();
172 public synchronized static IEditorRegistry getInstance() {
173 if (INSTANCE == null) {
174 INSTANCE = new EditorRegistry();
179 public static synchronized void dispose() {
180 if (INSTANCE != null) {
186 private EditorRegistry() {
187 tracker = new ExtensionTracker();
189 // Cache defined actions
190 IExtensionPoint expt = Platform.getExtensionRegistry().getExtensionPoint(NAMESPACE, EP_NAME);
191 loadExtensions(expt.getConfigurationElements());
193 // Start tracking for new and removed extensions
194 IFilter filter = ExtensionTracker.createExtensionPointFilter(expt);
195 tracker.registerHandler(this, filter);
200 private void close() {
206 editorMapping.clear();
209 idToExtension = null;
216 // * Must reset {@link #getAdaptersFor(ReadGraph, Resource)} query caches when
217 // * perspectives are changed because EditorAdapters may return
218 // * different results in different perspectives.
220 // private IPerspectiveListener perspectiveListener = new PerspectiveAdapter() {
221 // public void perspectiveActivated(IWorkbenchPage page, IPerspectiveDescriptor perspective) {
226 private final IContextManagerListener contextListener = new IContextManagerListener() {
228 public void contextManagerChanged(ContextManagerEvent event) {
229 if (event.isActiveContextsChanged()) {
230 //System.out.println("EVENT: " + event.isActiveContextsChanged() + ", " + event.isContextChanged() + ", " + event.isContextDefined() + ", " + Arrays.toString(event.getPreviouslyActiveContextIds().toArray()));
232 Collection<?> active = event.getContextManager().getActiveContextIds();
233 Collection<?> previouslyActive = event.getPreviouslyActiveContextIds();
235 Collection<String> added = new HashSet<String>(4);
236 Collection<String> removed = new HashSet<String>(4);
238 for (Object o : active) {
239 if (!previouslyActive.contains(o))
240 added.add((String) o);
242 for (Object o : previouslyActive) {
243 if (!active.contains(o))
244 removed.add((String) o);
247 //System.out.println("ADDED: " + Arrays.toString(added.toArray()) + ", REMOVED: " + Arrays.toString(removed.toArray()));
248 contextChange(added, removed);
253 private boolean containsAny(Collection<String> c, Collection<String> anyOf) {
256 for (String s : anyOf)
262 private void contextChange(Collection<String> added, Collection<String> removed) {
263 // Update active context id set
264 if (!added.isEmpty())
265 activeContextIds.addAll(added);
266 if (!removed.isEmpty())
267 activeContextIds.remove(removed);
269 // Clear caches if necessary
270 if (containsAny(referencedContextIds, added) || containsAny(referencedContextIds, removed)) {
275 @SuppressWarnings("unchecked")
276 private void hookListeners() {
277 IWorkbench wb = PlatformUI.getWorkbench();
278 IContextService contextService = (IContextService) wb.getService(IContextService.class);
279 contextService.addContextManagerListener(contextListener);
280 activeContextIds = new HashSet<String>(contextService.getActiveContextIds());
283 private void unhookListeners() {
284 IWorkbench wb = PlatformUI.getWorkbench();
285 IContextService contextService = (IContextService) wb.getService(IContextService.class);
286 if (contextService != null) {
287 contextService.removeContextManagerListener(contextListener);
291 private String[] parseInContexts(IConfigurationElement parent) {
292 List<String> contexts = null;
293 for (IConfigurationElement el : parent.getChildren(EL_NAME_IN_CONTEXT)) {
294 String id = el.getAttribute(ATTR_ID);
296 if (contexts == null)
297 contexts = new ArrayList<String>(4);
301 return contexts != null ? contexts.toArray(new String[contexts.size()]) : null;
304 private synchronized void loadExtensions(IConfigurationElement[] elements) {
305 org.eclipse.ui.IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();
307 Set<EditorAdapterDescriptorImpl> newExtensions = new HashSet<EditorAdapterDescriptorImpl>(Arrays.asList(extensions));
308 Map<String, Group> newGroups = new HashMap<String, Group>();
309 Set<String> newReferencedContextIds = new HashSet<String>(referencedContextIds);
311 for (IConfigurationElement el : elements) {
312 String name = el.getName();
314 String id = el.getAttribute(ATTR_ID);
315 String groupId = el.getAttribute(ATTR_GROUP_ID);
316 EditorAdapter adapter = null;
318 String priority = el.getAttribute(ATTR_PRIORITY);
319 int pri = IPriorityAction.NORMAL;
321 if (priority != null && !priority.trim().isEmpty())
322 pri = Integer.parseInt(priority);
323 } catch (NumberFormatException e) {
324 ExceptionUtils.logError("Non-integer priority value '" + priority + "' for '" + name + "' extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", e);
327 String[] inContexts = null;
329 if (EL_NAME_GROUP.equals(name)) {
330 if (id == null || id.isEmpty()) {
331 ExceptionUtils.logWarning("A group extension contributed by " + el.getDeclaringExtension().getContributor().getName() + " does not define a required id.", null);
333 if (!newGroups.containsKey(id)) {
334 newGroups.put(id, new Group(id));
338 } else if (EL_NAME_ADAPTER.equals(name)) {
339 String editorId = el.getAttribute(ATTR_EDITOR_ID);
340 IEditorDescriptor editorDesc = editorRegistry.findEditor(editorId);
341 if (editorDesc == null) {
342 ExceptionUtils.logError("Non-existent editorId '" + editorId + "' in extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", null);
346 String type_uris = OntologyVersions.getInstance().currentVersion(el.getAttribute(ATTR_TYPE_URIS));
347 String[] typeUris = type_uris != null ? type_uris.split(",") : new String[0];
349 String label = el.getAttribute(ATTR_LABEL);
350 String image = el.getAttribute(ATTR_IMAGE);
351 ImageDescriptor imageDesc = null;
353 label = editorDesc.getLabel();
356 URL resolved = FileLocator.resolve(new URL(image));
357 imageDesc = ImageDescriptor.createFromURL(resolved);
358 } catch (IOException e) {
359 // Try fallback method
360 Bundle bundle = Platform.getBundle(el.getDeclaringExtension().getContributor().getName());
361 imageDesc = ImageDescriptor.createFromURL(bundle.getEntry(image));
364 imageDesc = editorDesc.getImageDescriptor();
367 SimpleEditorAdapter _adapter = new SimpleEditorAdapter(label, imageDesc, editorId, (String[]) null, typeUris);
368 _adapter.setPriority(pri);
372 inContexts = parseInContexts(el);
373 } else if (EL_NAME_ADAPTERCLASS.equals(name)) {
374 adapter = (EditorAdapter) el.createExecutableExtension(ATTR_CLASS);
375 if (adapter instanceof Prioritized) {
376 ((Prioritized) adapter).setPriority(pri);
378 inContexts = parseInContexts(el);
381 if (adapter != null) {
382 EditorAdapterDescriptorImpl ext = new EditorAdapterDescriptorImpl(id, groupId, adapter, inContexts);
383 //System.out.println("Adding editor adapter extension from " + el.getContributor().getName() + ": " + ext.getId() + ", " + ext.getAdapter());
385 // Start tracking the new extension object, its removal will be notified of
386 // with removeExtension(extension, Object[]).
387 tracker.registerObject(el.getDeclaringExtension(), ext, IExtensionTracker.REF_STRONG);
389 if (id != null && !id.isEmpty()) {
390 idToExtension.put(id, ext);
392 if (inContexts != null)
393 for (String ctx : inContexts)
394 newReferencedContextIds.add(ctx);
396 newExtensions.add(ext);
398 } catch (CoreException e) {
399 ExceptionUtils.logError("Failed to initialize resourceEditorAdapter extension \"" + name + "\": "
400 + e.getMessage(), e);
404 for (EditorAdapterDescriptorImpl desc : idToExtension.values()) {
405 if (desc.getGroupId() != null) {
406 Group g = newGroups.get(desc.getGroupId());
414 this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]);
415 this.groupMap = newGroups;
416 this.referencedContextIds = newReferencedContextIds;
421 public void addExtension(IExtensionTracker tracker, IExtension extension) {
422 loadExtensions(extension.getConfigurationElements());
426 public synchronized void removeExtension(IExtension extension, Object[] objects) {
427 Set<EditorAdapterDescriptorImpl> newExtensions = new HashSet<EditorAdapterDescriptorImpl>(Arrays.asList(extensions));
428 Map<String, EditorAdapterDescriptorImpl> idMap = new HashMap<String, EditorAdapterDescriptorImpl>(idToExtension);
429 Set<String> removedContextReferences = new HashSet<String>();
431 Map<String, Group> newGroups = new HashMap<String, Group>();
432 for (Group g : groupMap.values()) {
433 newGroups.put(g.id, new Group(g));
436 for (Object o : objects) {
437 EditorAdapterDescriptor ext = (EditorAdapterDescriptor) o;
439 tracker.unregisterObject(extension, o);
440 newExtensions.remove(ext);
441 idMap.remove(ext.getId());
443 if (ext.getGroupId() != null) {
444 Group g = newGroups.get(ext.getGroupId());
449 for (String ctx : ext.getInContexts())
450 removedContextReferences.add(ctx);
453 // Go through the remaining editor adapters and
454 // check whether they still reference any of the removed
455 // context ids. Ids that are still referenced will not be
456 // removed from <code>referencedContextIds</code>
457 for (EditorAdapterDescriptorImpl desc : newExtensions) {
458 for (String ctx : desc.getInContexts()) {
459 removedContextReferences.remove(ctx);
462 Set<String> newReferencedContextIds = new HashSet<String>(referencedContextIds);
463 newReferencedContextIds.removeAll(removedContextReferences);
466 this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]);
467 this.idToExtension = idMap;
468 this.groupMap = newGroups;
469 this.referencedContextIds = newReferencedContextIds;
473 public EditorAdapterDescriptor getExtensionById(String id) {
474 return idToExtension.get(id);
478 public EditorAdapter getAdapterById(String id) {
479 EditorAdapterDescriptor ext = idToExtension.get(id);
480 return ext == null ? null : ext.getAdapter();
483 private void clearCache() {
484 synchronized (adapterCache) {
485 adapterCache.clear();
491 public EditorAdapterDescriptor[] getEditorAdapters() {
496 public EditorAdapter[] getAdaptersFor(ReadGraph g, final Object r) throws DatabaseException {
498 EditorAdapter[] result;
499 synchronized (adapterCache) {
500 result = adapterCache.get(r);
505 MultiStatus status = null;
507 final MapList<String, EditorAdapter> l = new MapList<String, EditorAdapter>();
508 for (EditorAdapterDescriptor a : extensions) {
510 // Filter out adapters that are not active in the current context configuration.
511 if (!a.isActive(activeContextIds))
513 // Filter out adapters that just can't handle the input.
514 if (!a.getAdapter().canHandle(g, r))
517 // NOTE: Group is null if there is no group.
518 l.add(a.getGroupId(), a.getAdapter());
519 } catch (RuntimeException e) {
521 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null);
522 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
527 Resource res = ResourceAdaptionUtils.toSingleResource(r);
529 ModelingResources MOD = ModelingResources.getInstance(g);
530 Resource indexRoot = g.syncRequest(new PossibleIndexRoot(res));
531 if (indexRoot != null) {
532 Instances query = g.adapt(MOD.EditorContribution, Instances.class);
533 for(Resource contribution : query.find(g, indexRoot)) {
537 String id = g.getRelatedValue(contribution, MOD.EditorContribution_editorId, Bindings.STRING);
538 String label = NameUtils.getSafeLabel(g, contribution);
540 Resource imageResource = g.getPossibleObject(contribution, MOD.EditorContribution_HasImage);
541 ImageDescriptor image = imageResource == null ? null : g.adapt(imageResource, ImageDescriptor.class);
543 Integer priority = g.getRelatedValue(contribution, MOD.EditorContribution_priority, Bindings.INTEGER);
544 EditorAdapterDescriptor a = new GraphEditorAdapterDescriptor(id, label, image, contribution, priority);
546 // Filter out adapters that are not active in the current context configuration.
547 if (!a.isActive(activeContextIds))
549 // Filter out adapters that just can't handle the input.
550 if (!a.getAdapter().canHandle(g, r))
553 l.add(a.getGroupId(), a.getAdapter());
555 } catch (DatabaseException e) {
556 Logger.defaultLogError(e);
562 result = gatherAdapterResult(l);
564 Arrays.sort(result, ADAPTER_COMPARATOR);
566 updateCache(r, result);
568 if (status != null && !status.isOK())
569 Activator.getDefault().getLog().log(status);
574 private EditorAdapter[] gatherAdapterResult(MapList<String, EditorAdapter> map) {
575 final List<EditorAdapter> result = new ArrayList<EditorAdapter>(8);
576 for (String group : map.getKeys()) {
577 List<EditorAdapter> grp = map.getValues(group);
582 EditorAdapter highestPriorityAdapter = null;
583 for (EditorAdapter adapter : grp) {
584 if (highestPriorityAdapter == null || adapter.getPriority() > highestPriorityAdapter.getPriority()) {
585 highestPriorityAdapter = adapter;
588 result.add(highestPriorityAdapter);
591 return result.toArray(EMPTY_ADAPTER_ARRAY);
595 public EditorMapping getMappings() {
596 return editorMapping;
599 private void updateCache(Object r, EditorAdapter[] result) {
600 synchronized (adapterCache) {
601 adapterCache.put(r, result);
602 Object removed = cacheQueue.write(r);
603 if (removed != null) {
604 adapterCache.remove(removed);
610 private static class CircularBuffer<T> {
611 private final WeakReference<?>[] buffer;
615 private boolean full;
616 private final int size;
618 CircularBuffer(int size) {
620 throw new IllegalArgumentException("zero size not allowed");
622 this.buffer = new WeakReference[size];
627 public void clear() {
628 this.head = this.tail = 0;
630 Arrays.fill(buffer, null);
634 * @param id an ID, other than 0L
635 * @return 0L if the buffer was not yet full, otherwise
636 * @throws IllegalArgumentException for 0L id
638 @SuppressWarnings("unchecked")
641 throw new IllegalArgumentException("null resource id");
644 WeakReference<?> prev = buffer[head];
645 buffer[head++] = new WeakReference<T>(id);
648 return (T) prev.get();
650 buffer[head++] = new WeakReference<T>(id);
656 // Nothing was yet overwritten
661 public String toString() {
662 return Arrays.toString(buffer);
668 public EditorAdapter[] getDefaultAdaptersFor(ReadGraph g, Object r) throws DatabaseException {
669 EditorAdapter[] results;
671 MultiStatus status = null;
673 final MapList<String, EditorAdapter> l = new MapList<String, EditorAdapter>();
674 for (EditorAdapterDescriptor a : extensions) {
677 // NOTE: Group is null if there is no group.
678 l.add(a.getGroupId(), a.getAdapter());
679 } catch (RuntimeException e) {
681 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null);
682 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
685 results = gatherAdapterResult(l);
687 if (status != null && !status.isOK())
688 Activator.getDefault().getLog().log(status);
690 // If no default editor is found, get all that can handle
691 if (results.length > 0)
694 return getAdaptersFor(g, r);