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.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
25 import java.util.WeakHashMap;
27 import org.eclipse.core.commands.contexts.ContextManagerEvent;
28 import org.eclipse.core.commands.contexts.IContextManagerListener;
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.FileLocator;
31 import org.eclipse.core.runtime.IConfigurationElement;
32 import org.eclipse.core.runtime.IExtension;
33 import org.eclipse.core.runtime.IExtensionPoint;
34 import org.eclipse.core.runtime.IStatus;
35 import org.eclipse.core.runtime.MultiStatus;
36 import org.eclipse.core.runtime.Platform;
37 import org.eclipse.core.runtime.Status;
38 import org.eclipse.core.runtime.dynamichelpers.ExtensionTracker;
39 import org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler;
40 import org.eclipse.core.runtime.dynamichelpers.IExtensionTracker;
41 import org.eclipse.core.runtime.dynamichelpers.IFilter;
42 import org.eclipse.jface.resource.ImageDescriptor;
43 import org.eclipse.ui.IEditorDescriptor;
44 import org.eclipse.ui.IWorkbench;
45 import org.eclipse.ui.PlatformUI;
46 import org.eclipse.ui.contexts.IContextService;
47 import org.osgi.framework.Bundle;
48 import org.simantics.databoard.Bindings;
49 import org.simantics.db.ReadGraph;
50 import org.simantics.db.Resource;
51 import org.simantics.db.common.request.PossibleIndexRoot;
52 import org.simantics.db.common.utils.Logger;
53 import org.simantics.db.common.utils.NameUtils;
54 import org.simantics.db.exception.DatabaseException;
55 import org.simantics.db.layer0.adapter.Instances;
56 import org.simantics.modeling.ModelingResources;
57 import org.simantics.scl.reflection.OntologyVersions;
58 import org.simantics.ui.internal.Activator;
59 import org.simantics.ui.utils.ResourceAdaptionUtils;
60 import org.simantics.utils.datastructures.MapList;
61 import org.simantics.utils.ui.ExceptionUtils;
62 import org.simantics.utils.ui.action.IPriorityAction;
66 * @author Tuukka Lehtonen
68 public final class EditorRegistry implements IExtensionChangeHandler, IEditorRegistry {
71 * The maximum amount of entries to cache
72 * {@link #getAdaptersFor(ReadGraph, Resource)} results for. Context activation
73 * changes invalidate this cache.
75 private static final int MAX_CACHE_SIZE = 50;
78 private final static String NAMESPACE = Activator.PLUGIN_ID;
80 private final static String EP_NAME = "resourceEditorAdapter";
83 private final static String EL_NAME_ADAPTER = "adapter";
85 private final static String EL_NAME_ADAPTERCLASS = "adapterClass";
87 private final static String EL_NAME_GROUP = "group";
89 private final static String EL_NAME_IN_CONTEXT = "inContext";
92 private static final String ATTR_CLASS = "class";
94 private static final String ATTR_IMAGE = "image";
96 private static final String ATTR_LABEL = "label";
98 private static final String ATTR_TYPE_URIS = "type_uris";
100 private static final String ATTR_EDITOR_ID = "editorId";
102 private static final String ATTR_PRIORITY = "priority";
104 private static final String ATTR_GROUP_ID = "groupId";
106 private static final String ATTR_ID = "id";
109 private static class Group {
110 public final String id;
111 public final List<EditorAdapterDescriptor> adapters;
115 this.adapters = new ArrayList<EditorAdapterDescriptor>();
119 this.adapters = new ArrayList<EditorAdapterDescriptor>(g.adapters);
121 void add(EditorAdapterDescriptor desc) {
124 void remove(EditorAdapterDescriptor desc) {
125 adapters.remove(desc);
129 private static final EditorAdapter[] EMPTY_ADAPTER_ARRAY = new EditorAdapter[0];
131 private static EditorRegistry INSTANCE;
133 private ExtensionTracker tracker;
135 private EditorAdapterDescriptorImpl[] extensions = new EditorAdapterDescriptorImpl[0];
137 private Map<String, EditorAdapterDescriptorImpl> idToExtension = new HashMap<String, EditorAdapterDescriptorImpl>();
139 private Map<String, Group> groupMap = new HashMap<String, Group>();
141 private WeakHashMap<Object, EditorAdapter[]> adapterCache = new WeakHashMap<Object, EditorAdapter[]>(
144 private CircularBuffer<Object> cacheQueue = new CircularBuffer<Object>(
148 * A set of all the context id's that are currently referenced by all the
149 * loaded resourceEditorAdapter extensions.
151 private Set<String> referencedContextIds = new HashSet<String>();
154 * The current set of active contexts.
156 private Set<String> activeContextIds = new HashSet<String>();
159 * Used to store all input -> editor mappings. In the Eclipse IDE, this
160 * information is stored as persistent properties in each IFile represented
161 * by eclipse Resource's. This implementation stores all the mappings in
164 * Maybe in the future it would be possible to store these mapping in the
165 * graph in a way that allows us not to publish those changes to the outside
168 private final EditorMappingImpl editorMapping = new EditorMappingImpl();
170 public synchronized static IEditorRegistry getInstance() {
171 if (INSTANCE == null) {
172 INSTANCE = new EditorRegistry();
177 public static synchronized void dispose() {
178 if (INSTANCE != null) {
184 private EditorRegistry() {
185 tracker = new ExtensionTracker();
187 // Cache defined actions
188 IExtensionPoint expt = Platform.getExtensionRegistry().getExtensionPoint(NAMESPACE, EP_NAME);
189 loadExtensions(expt.getConfigurationElements());
191 // Start tracking for new and removed extensions
192 IFilter filter = ExtensionTracker.createExtensionPointFilter(expt);
193 tracker.registerHandler(this, filter);
198 private void close() {
204 editorMapping.clear();
207 idToExtension = null;
214 // * Must reset {@link #getAdaptersFor(ReadGraph, Resource)} query caches when
215 // * perspectives are changed because EditorAdapters may return
216 // * different results in different perspectives.
218 // private IPerspectiveListener perspectiveListener = new PerspectiveAdapter() {
219 // public void perspectiveActivated(IWorkbenchPage page, IPerspectiveDescriptor perspective) {
224 private final IContextManagerListener contextListener = new IContextManagerListener() {
226 public void contextManagerChanged(ContextManagerEvent event) {
227 if (event.isActiveContextsChanged()) {
228 //System.out.println("EVENT: " + event.isActiveContextsChanged() + ", " + event.isContextChanged() + ", " + event.isContextDefined() + ", " + Arrays.toString(event.getPreviouslyActiveContextIds().toArray()));
230 Collection<?> active = event.getContextManager().getActiveContextIds();
231 Collection<?> previouslyActive = event.getPreviouslyActiveContextIds();
233 Collection<String> added = new HashSet<String>(4);
234 Collection<String> removed = new HashSet<String>(4);
236 for (Object o : active) {
237 if (!previouslyActive.contains(o))
238 added.add((String) o);
240 for (Object o : previouslyActive) {
241 if (!active.contains(o))
242 removed.add((String) o);
245 //System.out.println("ADDED: " + Arrays.toString(added.toArray()) + ", REMOVED: " + Arrays.toString(removed.toArray()));
246 contextChange(added, removed);
251 private boolean containsAny(Collection<String> c, Collection<String> anyOf) {
254 for (String s : anyOf)
260 private void contextChange(Collection<String> added, Collection<String> removed) {
261 // Update active context id set
262 if (!added.isEmpty())
263 activeContextIds.addAll(added);
264 if (!removed.isEmpty())
265 activeContextIds.remove(removed);
267 // Clear caches if necessary
268 if (containsAny(referencedContextIds, added) || containsAny(referencedContextIds, removed)) {
273 @SuppressWarnings("unchecked")
274 private void hookListeners() {
275 IWorkbench wb = PlatformUI.getWorkbench();
276 IContextService contextService = (IContextService) wb.getService(IContextService.class);
277 contextService.addContextManagerListener(contextListener);
278 activeContextIds = new HashSet<String>(contextService.getActiveContextIds());
281 private void unhookListeners() {
282 IWorkbench wb = PlatformUI.getWorkbench();
283 IContextService contextService = (IContextService) wb.getService(IContextService.class);
284 if (contextService != null) {
285 contextService.removeContextManagerListener(contextListener);
289 private String[] parseInContexts(IConfigurationElement parent) {
290 List<String> contexts = null;
291 for (IConfigurationElement el : parent.getChildren(EL_NAME_IN_CONTEXT)) {
292 String id = el.getAttribute(ATTR_ID);
294 if (contexts == null)
295 contexts = new ArrayList<String>(4);
299 return contexts != null ? contexts.toArray(new String[contexts.size()]) : null;
302 private synchronized void loadExtensions(IConfigurationElement[] elements) {
303 org.eclipse.ui.IEditorRegistry editorRegistry = PlatformUI.getWorkbench().getEditorRegistry();
305 Set<EditorAdapterDescriptorImpl> newExtensions = new HashSet<EditorAdapterDescriptorImpl>(Arrays.asList(extensions));
306 Map<String, Group> newGroups = new HashMap<String, Group>();
307 Set<String> newReferencedContextIds = new HashSet<String>(referencedContextIds);
309 for (IConfigurationElement el : elements) {
310 String name = el.getName();
312 String id = el.getAttribute(ATTR_ID);
313 String groupId = el.getAttribute(ATTR_GROUP_ID);
314 EditorAdapter adapter = null;
316 String priority = el.getAttribute(ATTR_PRIORITY);
317 int pri = IPriorityAction.NORMAL;
319 if (priority != null && !priority.trim().isEmpty())
320 pri = Integer.parseInt(priority);
321 } catch (NumberFormatException e) {
322 ExceptionUtils.logError("Non-integer priority value '" + priority + "' for '" + name + "' extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", e);
325 String[] inContexts = null;
327 if (EL_NAME_GROUP.equals(name)) {
328 if (id == null || id.isEmpty()) {
329 ExceptionUtils.logWarning("A group extension contributed by " + el.getDeclaringExtension().getContributor().getName() + " does not define a required id.", null);
331 if (!newGroups.containsKey(id)) {
332 newGroups.put(id, new Group(id));
336 } else if (EL_NAME_ADAPTER.equals(name)) {
337 String editorId = el.getAttribute(ATTR_EDITOR_ID);
338 IEditorDescriptor editorDesc = editorRegistry.findEditor(editorId);
339 if (editorDesc == null) {
340 ExceptionUtils.logError("Non-existent editorId '" + editorId + "' in extension contributed by '" + el.getDeclaringExtension().getContributor().getName() + "'", null);
344 String type_uris = OntologyVersions.getInstance().currentVersion(el.getAttribute(ATTR_TYPE_URIS));
345 String[] typeUris = type_uris != null ? type_uris.split(",") : new String[0];
347 String label = el.getAttribute(ATTR_LABEL);
348 String image = el.getAttribute(ATTR_IMAGE);
349 ImageDescriptor imageDesc = null;
351 label = editorDesc.getLabel();
354 URL resolved = FileLocator.resolve(new URL(image));
355 imageDesc = ImageDescriptor.createFromURL(resolved);
356 } catch (IOException e) {
357 // Try fallback method
358 Bundle bundle = Platform.getBundle(el.getDeclaringExtension().getContributor().getName());
359 imageDesc = ImageDescriptor.createFromURL(bundle.getEntry(image));
362 imageDesc = editorDesc.getImageDescriptor();
365 SimpleEditorAdapter _adapter = new SimpleEditorAdapter(label, imageDesc, editorId, (String[]) null, typeUris);
366 _adapter.setPriority(pri);
370 inContexts = parseInContexts(el);
371 } else if (EL_NAME_ADAPTERCLASS.equals(name)) {
372 adapter = (EditorAdapter) el.createExecutableExtension(ATTR_CLASS);
373 if (adapter instanceof Prioritized) {
374 ((Prioritized) adapter).setPriority(pri);
376 inContexts = parseInContexts(el);
379 if (adapter != null) {
380 EditorAdapterDescriptorImpl ext = new EditorAdapterDescriptorImpl(id, groupId, adapter, inContexts);
381 //System.out.println("Adding editor adapter extension from " + el.getContributor().getName() + ": " + ext.getId() + ", " + ext.getAdapter());
383 // Start tracking the new extension object, its removal will be notified of
384 // with removeExtension(extension, Object[]).
385 tracker.registerObject(el.getDeclaringExtension(), ext, IExtensionTracker.REF_STRONG);
387 if (id != null && !id.isEmpty()) {
388 idToExtension.put(id, ext);
390 if (inContexts != null)
391 for (String ctx : inContexts)
392 newReferencedContextIds.add(ctx);
394 newExtensions.add(ext);
396 } catch (CoreException e) {
397 ExceptionUtils.logError("Failed to initialize resourceEditorAdapter extension \"" + name + "\": "
398 + e.getMessage(), e);
402 for (EditorAdapterDescriptorImpl desc : idToExtension.values()) {
403 if (desc.getGroupId() != null) {
404 Group g = newGroups.get(desc.getGroupId());
412 this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]);
413 this.groupMap = newGroups;
414 this.referencedContextIds = newReferencedContextIds;
419 public void addExtension(IExtensionTracker tracker, IExtension extension) {
420 loadExtensions(extension.getConfigurationElements());
424 public synchronized void removeExtension(IExtension extension, Object[] objects) {
425 Set<EditorAdapterDescriptorImpl> newExtensions = new HashSet<EditorAdapterDescriptorImpl>(Arrays.asList(extensions));
426 Map<String, EditorAdapterDescriptorImpl> idMap = new HashMap<String, EditorAdapterDescriptorImpl>(idToExtension);
427 Set<String> removedContextReferences = new HashSet<String>();
429 Map<String, Group> newGroups = new HashMap<String, Group>();
430 for (Group g : groupMap.values()) {
431 newGroups.put(g.id, new Group(g));
434 for (Object o : objects) {
435 EditorAdapterDescriptor ext = (EditorAdapterDescriptor) o;
437 tracker.unregisterObject(extension, o);
438 newExtensions.remove(ext);
439 idMap.remove(ext.getId());
441 if (ext.getGroupId() != null) {
442 Group g = newGroups.get(ext.getGroupId());
447 for (String ctx : ext.getInContexts())
448 removedContextReferences.add(ctx);
451 // Go through the remaining editor adapters and
452 // check whether they still reference any of the removed
453 // context ids. Ids that are still referenced will not be
454 // removed from <code>referencedContextIds</code>
455 for (EditorAdapterDescriptorImpl desc : newExtensions) {
456 for (String ctx : desc.getInContexts()) {
457 removedContextReferences.remove(ctx);
460 Set<String> newReferencedContextIds = new HashSet<String>(referencedContextIds);
461 newReferencedContextIds.removeAll(removedContextReferences);
464 this.extensions = newExtensions.toArray(new EditorAdapterDescriptorImpl[newExtensions.size()]);
465 this.idToExtension = idMap;
466 this.groupMap = newGroups;
467 this.referencedContextIds = newReferencedContextIds;
471 public EditorAdapterDescriptor getExtensionById(String id) {
472 return idToExtension.get(id);
476 public EditorAdapter getAdapterById(String id) {
477 EditorAdapterDescriptor ext = idToExtension.get(id);
478 return ext == null ? null : ext.getAdapter();
481 private void clearCache() {
482 synchronized (adapterCache) {
483 adapterCache.clear();
489 public EditorAdapterDescriptor[] getEditorAdapters() {
494 public EditorAdapter[] getAdaptersFor(ReadGraph g, final Object r) throws DatabaseException {
496 EditorAdapter[] result;
497 synchronized (adapterCache) {
498 result = adapterCache.get(r);
503 MultiStatus status = null;
505 final MapList<String, EditorAdapter> l = new MapList<String, EditorAdapter>();
506 for (EditorAdapterDescriptor a : extensions) {
508 // Filter out adapters that are not active in the current context configuration.
509 if (!a.isActive(activeContextIds))
511 // Filter out adapters that just can't handle the input.
512 if (!a.getAdapter().canHandle(g, r))
515 // NOTE: Group is null if there is no group.
516 l.add(a.getGroupId(), a.getAdapter());
517 } catch (RuntimeException e) {
519 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null);
520 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
525 Resource res = ResourceAdaptionUtils.toSingleResource(r);
527 ModelingResources MOD = ModelingResources.getInstance(g);
528 Resource indexRoot = g.syncRequest(new PossibleIndexRoot(res));
529 if (indexRoot != null) {
530 Instances query = g.adapt(MOD.EditorContribution, Instances.class);
531 for(Resource contribution : query.find(g, indexRoot)) {
535 String id = g.getRelatedValue(contribution, MOD.EditorContribution_editorId, Bindings.STRING);
536 String label = NameUtils.getSafeLabel(g, contribution);
538 Resource imageResource = g.getPossibleObject(contribution, MOD.EditorContribution_HasImage);
539 ImageDescriptor image = imageResource == null ? null : g.adapt(imageResource, ImageDescriptor.class);
541 Integer priority = g.getRelatedValue(contribution, MOD.EditorContribution_priority, Bindings.INTEGER);
542 EditorAdapterDescriptor a = new GraphEditorAdapterDescriptor(id, label, image, contribution, priority);
544 // Filter out adapters that are not active in the current context configuration.
545 if (!a.isActive(activeContextIds))
547 // Filter out adapters that just can't handle the input.
548 if (!a.getAdapter().canHandle(g, r))
551 l.add(a.getGroupId(), a.getAdapter());
553 } catch (DatabaseException e) {
554 Logger.defaultLogError(e);
560 result = gatherAdapterResult(l);
561 updateCache(r, result);
563 if (status != null && !status.isOK())
564 Activator.getDefault().getLog().log(status);
569 private EditorAdapter[] gatherAdapterResult(MapList<String, EditorAdapter> map) {
570 final List<EditorAdapter> result = new ArrayList<EditorAdapter>(8);
571 for (String group : map.getKeys()) {
572 List<EditorAdapter> grp = map.getValues(group);
577 EditorAdapter highestPriorityAdapter = null;
578 for (EditorAdapter adapter : grp) {
579 if (highestPriorityAdapter == null || adapter.getPriority() > highestPriorityAdapter.getPriority()) {
580 highestPriorityAdapter = adapter;
583 result.add(highestPriorityAdapter);
586 return result.toArray(EMPTY_ADAPTER_ARRAY);
590 public EditorMapping getMappings() {
591 return editorMapping;
594 private void updateCache(Object r, EditorAdapter[] result) {
595 synchronized (adapterCache) {
596 adapterCache.put(r, result);
597 Object removed = cacheQueue.write(r);
598 if (removed != null) {
599 adapterCache.remove(removed);
605 private static class CircularBuffer<T> {
606 private final WeakReference<?>[] buffer;
610 private boolean full;
611 private final int size;
613 CircularBuffer(int size) {
615 throw new IllegalArgumentException("zero size not allowed");
617 this.buffer = new WeakReference[size];
622 public void clear() {
623 this.head = this.tail = 0;
625 Arrays.fill(buffer, null);
629 * @param id an ID, other than 0L
630 * @return 0L if the buffer was not yet full, otherwise
631 * @throws IllegalArgumentException for 0L id
633 @SuppressWarnings("unchecked")
636 throw new IllegalArgumentException("null resource id");
639 WeakReference<?> prev = buffer[head];
640 buffer[head++] = new WeakReference<T>(id);
643 return (T) prev.get();
645 buffer[head++] = new WeakReference<T>(id);
651 // Nothing was yet overwritten
656 public String toString() {
657 return Arrays.toString(buffer);
663 public EditorAdapter[] getDefaultAdaptersFor(ReadGraph g, Object r) throws DatabaseException {
664 EditorAdapter[] results;
666 MultiStatus status = null;
668 final MapList<String, EditorAdapter> l = new MapList<String, EditorAdapter>();
669 for (EditorAdapterDescriptor a : extensions) {
672 // NOTE: Group is null if there is no group.
673 l.add(a.getGroupId(), a.getAdapter());
674 } catch (RuntimeException e) {
676 status = new MultiStatus(Activator.PLUGIN_ID, 0, "Unexpected errors occured in EditorAdapters:" , null);
677 status.add(new Status(IStatus.ERROR, Activator.PLUGIN_ID, e.getMessage(), e));
680 results = gatherAdapterResult(l);
682 if (status != null && !status.isOK())
683 Activator.getDefault().getLog().log(status);
685 // If no default editor is found, get all that can handle
686 if (results.length > 0)
689 return getAdaptersFor(g, r);