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