Adding Tools to the Explore Palette

Tools in the explore palette work by performing a query in the context of a diagram. In order to add new tools one must understand how queries are assembled and how they are executed. In order to create new query types and/or contribute queries to the explore palette eclipse plugins must be created and deployed in the runtime.

Query Assembly and Execution

Query, overlay and presentation context can be used to create commands to be executed.

The combination of a query object, a query overlay object and a query presentation context object can be used to retrieve a command that can execute the query and present results. The query object points to a particular query type, which will govern how it executes, what parameters are available (or required) and how the results will be presented in different contexts. The query object can also be used to store various default values to certain parameters. Query overlay objects point to exactly one query object and provide values for parameters that were not set in the query object. An overlay may override values defined in the query.

The query presentation context is what provides the details about where the query results should be presented. It is assumed that the query will be executed in the same way regardless of the context but it is the context that will provide the type of representation that should be used for the results as well as the location for the results to be displayed. When using tools in the explore palette this context will always be a diagram context with the location being the current editor at the position where the user clicked.

In some cases the combination of the query object and the query overlay object provided is not enough to satisfy all of the parameters that are needed for that query type. This is quite common for tools in the explore palette because it is the user that decides the query context (the object on which to perform the query). Query types that are used in the explore palette often attempt to use the edit part that the user clicks on as the query context. Otherwise, the query type could either choose to disable execution of the query if the user does not aim the tool at a valid edit part or choose to prompt the user if they click on an invalid edit part.

Create a New Query Type

Proceed to the "Add a Query to the Explore Palette" section if a suitable query type is already available.

Queries and overlays are represented using TopicQuery objects. TopicQuery objects are EMF objects (EObjects) that can be serialized in XML form. When storing query objects they are most often serialized in ".eqx" files.

TopicQueries have a number of fields that can be used to store and retrieve serializable data. For example, there is a "context" field that is accessible through the getContext() method. This field allows one to store zero or more references to other EObjects. There are annotation objects that can be stored in the TopicQuery with a map. The annotation map has a source (or some unique ID) as a key. Each annotation may store information in the form of strings or in the form of references to other EObjects.

Creating a Query Executor

The purpose of a query executor is to produce a set of results given a query overlay object and its query object. The results are assumed to be provided in a form that would allow them to be presented in a variety of forms and not just on a diagram.

The following plugin extension snippet declares an example of a query executor with a unique type:

plugin.xml:
   <extension
         point="com.ibm.xtools.emf.query.core.queries"> 
      <type
            ID="uml.inheritance.query"
            executor="uml.inheritance.query.UMLInheritanceExecutor"/>
    </extension>

The executor must extract the necessary information from the overlay and query objects. If the information is not available either there must be some configuration command that will configure the objects. Otherwise, the executor has no choice but to declare that it is not executable by returning an unexecutable command. In the following case, the executor simply requires the context object in the form of a UML Classifier.

The executor is free to perform any execution logic that is needed to discover the results and provide those results in virtually any form as long as the results can be represented using a Java object. The executor below uses the provided UML Classifier to query the EMF index to map out the inheritance hierarchy. It returns the result in the form of a special InheritanceResult object that it has defined. Presenters for this particular executor will be written to understand the InheritanceResult objects and present the results to the user. More on presenters later in this guide.

UMLInheritanceExecutor.java:
public class UMLInheritanceExecutor implements IQueryExecutor {

...
    public ICommand getExecuteCommand(final TopicQuery overlay, final ICommand configureCommand, TransactionalEditingDomain editingDomain, final Object shell) {
        TopicQueryOperations.isOverlay(overlay);
        
        // Either the query or the overlay must have exactly one context or there must be a
        //  configure command that will provide this context in order for this to be
        //  executable.
        if (TopicQueryOperations.getContext(overlay).size() != 1 && configureCommand == null) {
            return UnexecutableCommand.INSTANCE;
        }
        
        final ICommand retVal = new AbstractTransactionalCommand(editingDomain, "Find Inheritance Hierarchy", Collections.EMPTY_LIST) {
            public boolean canExecute() {
                // If there is a command that will configure the query then we will trust that
                //  this command will set the context before we execute.
                if (configureCommand != null) {
                    return true;
                }

                // Otherwise, make sure that there is exactly one context and it is a UML classifier
                Classifier classifier = getClassifier(overlay);
                return classifier != null;
            }
            
            protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info)
                throws ExecutionException {
                
                Classifier classifier = getClassifier(overlay);

                if (classifier == null) {
                     return CommandResult.newErrorCommandResult("Empty context, the query cannot be executed.");
                }
                
                // Construct a result object that will provide the presenter with enough
                //  information to present the results.
                InheritanceResult result = new InheritanceResult();
                
                IndexContext indexContext = IndexContext.createPlatformContext(classifier.eResource().getResourceSet());
                indexContext.getOptions().put(IndexContext.RESOLVE_PROXIES, Boolean.TRUE);
                
                Collection classifiers = Collections.singletonList(classifier);
                result.addClassifier(classifier);
                
                Map generalizationToSourceClassifier = new HashMap();
                
                try {
                    // Search until there are no more undiscovered ancestors of this classifier
                    while (classifiers.size() > 0) {

                        Collection generalizations = new ArrayList();
                        // Query the EMF index for generalizations of the current classifiers
                        Map results = IIndexSearchManager.INSTANCE.findReferencingEObjects(indexContext, classifiers, UMLPackage.eINSTANCE.getGeneralization_General(), null, monitor);
                        
                        for (Iterator i = results.entrySet().iterator(); i.hasNext();) {
                            Map.Entry entry = (Map.Entry)i.next();
                            
                            Classifier c = (Classifier)entry.getKey();
                            Collection g = (Collection)entry.getValue();
                            
                            for (Iterator j = g.iterator(); j.hasNext();) {
                                Generalization generalization = (Generalization)j.next();
                                
                                generalizationToSourceClassifier.put(generalization, c);
                                
                                generalizations.add(generalization);
                            }
                        }
                        
                        classifiers = new ArrayList();
                        
                        results = IIndexSearchManager.INSTANCE.findContainer(indexContext, generalizations, monitor);
                        
                        for (Iterator i = results.entrySet().iterator(); i.hasNext();) {
                            Map.Entry entry = (Map.Entry)i.next();
                            
                            Generalization g = (Generalization)entry.getKey();
                            Classifier c = (Classifier)entry.getValue();
                            
                            result.addClassifier(c);
                            
                            classifiers.add(c);
                            
                            result.addGeneralization(c, g, (Classifier)generalizationToSourceClassifier.get(g));
                        }
                    } 
         
                    Collection generalizations = IIndexSearchManager.INSTANCE.findContainedEObjects(indexContext, classifier, UMLPackage.eINSTANCE.getClassifier_Generalization(), null, monitor);
                    for (Iterator i = generalizations.iterator(); i.hasNext();) {
                        Generalization g = (Generalization)i.next();
                        
                        generalizationToSourceClassifier.put(g, classifier);
                    }
                    
                    // Now, find all descendants of this classifier
                    while (generalizations.size() > 0) {
                        classifiers = new ArrayList();
                        
                        Map results = IIndexSearchManager.INSTANCE.findReferencedEObjects(indexContext, generalizations, UMLPackage.eINSTANCE.getGeneralization_General(), null, monitor);
                        
                        for (Iterator i = results.entrySet().iterator(); i.hasNext();) {
                            Map.Entry entry = (Map.Entry)i.next();
                            
                            Generalization g = (Generalization)entry.getKey();
                            Collection c = (Collection)entry.getValue();
                            
                            for (Iterator j = c.iterator(); j.hasNext();) {
                                Classifier cls = (Classifier)j.next();
                                
                                result.addClassifier(cls);
                                
                                result.addGeneralization((Classifier)generalizationToSourceClassifier.get(g), g, cls);
                                
                                classifiers.add(cls);
                            }
                        }
                        
                        generalizations = new ArrayList();
                        
                        for (Iterator i = classifiers.iterator(); i.hasNext();) {
                            Classifier cls = (Classifier)i.next();
                            
                            Collection foundGeneralizations = IIndexSearchManager.INSTANCE.findContainedEObjects(indexContext, cls, UMLPackage.eINSTANCE.getClassifier_Generalization(), null, monitor);
                            
                            for (Iterator j = foundGeneralizations.iterator(); j.hasNext();) {
                                Generalization g = (Generalization)j.next();
                                
                                generalizations.add(g);
                                
                                generalizationToSourceClassifier.put(g, cls);
                            }
                        }
                    }
                } catch (IndexException e) {
                    return CommandResult.newErrorCommandResult(e);
                }
                
                // Pass this result object for the presenter
                return CommandResult.newOKCommandResult(result);
            }
        };
        
        return retVal;
    }
...
}

The executor searched for inheritance relationships from the context object using the EMF Index. For more information about the EMF index API see the Indexing and Searching Models programmer's guide.

Creating a Diagram Query Presenter

The presenter is the entity that takes results produced by the executor and renders them in the given query presentation context. For a given query type there can be many different presenters for different presentation contexts. However, there can only be one presenter for one query type for one presentation context.

plugin.xml:
   <extension
         point="com.ibm.xtools.emf.query.core.queries"> 
      <presenter
            class="uml.inheritance.query.UMLInheritancePresenter"
            presentationContext="com.ibm.xtools.emf.query.ui.diagram.DiagramPresentationContext"
            type="uml.inheritance.query"/>
    </extension>

The following query presenter is used to render the results from the above executor on a diagram. The AbstractDiagramQueryPresenter provides a special helper class called QueryDiagramRenderer to make it easier to add and arrange shapes on the diagram. The presenter uses the renderer by constructing an instance of the renderer and making callbacks to the renderer object from the present command. The present command must be placed into a composite command along with the renderer's command. The idea is that the present command will make callbacks to the renderer that will in turn populate its command with all of the necessary code. After the present command is finished, the renderer's command will peform the necessary operations.

UMLInheritancePresenter.java:
public class UMLInheritancePresenter extends AbstractDiagramQueryPresenter {
...
    public ICommand getPresentCommand(final TopicQuery overlay, final ICommand executeCommand, IQueryExecutor executor, IPresentationContext ctx, TransactionalEditingDomain editingDomain) {

        // We cannot present in anything other than a diagram context
        if (!(ctx instanceof DiagramPresentationContext)) {
            return UnexecutableCommand.INSTANCE;
        }

        CompositeCommand cmd = new CompositeCommand("Show Hierarchy on Diagram");
        
        final DiagramPresentationContext diagramContext = (DiagramPresentationContext)ctx;
        
        // Create a renderer with the policy of no view reuse. This means that all new views
        //  will be created and arranged on the diagram instead of reusing any existing views.
        final QueryDiagramRenderer renderer = new QueryDiagramRenderer(this, diagramContext, QueryDiagramRenderer.NO_REUSE);
        
        cmd.add(new AbstractTransactionalCommand(editingDomain, "", Collections.EMPTY_LIST) {
            protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info)
                throws ExecutionException {
                
                // Get the result that the executor gave us.
                InheritanceResult iResult = (InheritanceResult)executeCommand.getCommandResult().getReturnValue();
                
                // If there was no inheritance result then there is nothing to present.
                if (iResult == null) {
                    return CommandResult.newErrorCommandResult("No result to present!");
                }
                
                // Create views for all of the classifiers
                for (Iterator i = iResult.getClassifiers().iterator(); i.hasNext();) {
                    Classifier classifier = (Classifier)i.next();

                    renderer.createView(classifier, diagramContext.getGestureLocation(), overlay, diagramContext.getPreferencesHint());
                }
                
                // Create connection views for all of the generalizations
                for (Iterator i = iResult.getGeneralizations().iterator(); i.hasNext();) {
                    List generalizationData = (List)i.next();
                    Classifier c1 = (Classifier)generalizationData.get(0);
                    Generalization g = (Generalization)generalizationData.get(1);
                    Classifier c2 = (Classifier)generalizationData.get(2);
                 
                    renderer.createConnector(c1, g, c2, overlay, diagramContext.getPreferencesHint());
                }
                
                // Remove any stale information from the overlay
                renderer.purgeStaleResults(overlay);
                
                // Arrange all views created in this session with the default layout.
                // Also, arrange the context shape because otherwise it could be obscured by the new views.
                renderer.arrange(LayoutType.DEFAULT, false, overlay);
                
                // Perform any necessary operations to complete all of the above rendering.
                renderer.complete(overlay);
                
                return CommandResult.newOKCommandResult();
            }
        });
        
        // Add the renderer's command after our own to perform the requested view creation, arrange, etc.
        cmd.add(renderer.getCommand());
        
        return cmd;
    }
}

This presenter will take the inheritance result provided by the executor and renders all of the UML Class and Generalization shapes on the diagram.

Creating a Diagram Overlay Configurator

In order to handle cases where the query and overlay are not fully configured an overlay configurator is needed. Overlay configurators are optional as long as every query/overlay pair are always properly configured when executed. There are a number of ways that configurators can find the data necessary to complete the overlay. First, the configurator is declared against a specific presentation context so it can use information from that presentation context. Alternatively, the configurator can be provided with a Shell that will allow it to prompt the user for the necessary information.

plugin.xml:
   <extension
         point="com.ibm.xtools.emf.query.core.queries"> 
      <presenter
            class="uml.inheritance.query.UMLInheritanceQueryProvider"
            presentationContext="com.ibm.xtools.emf.query.ui.diagram.DiagramPresentationContext"
            type="uml.inheritance.query"
            overlayConfigurator="uml.inheritance.query.UMLInheritanceConfigurator"/>
    </extension>

The following overlay configurator uses the shape (edit part) that the user has clicked as the context of the overlay. This is necessary because queries may not have a context to find an inheritance hierarchy. It is possible that the user does not click on a classifier. In that case the configurator has no choice but to bring up a dialog to prompt the user.

public class UMLInheritanceConfigurator implements IOverlayConfigurator {
    public ICommand getOverlayConfigurationCommand(final TopicQuery overlay, final IPresentationContext ctx, TransactionalEditingDomain editingDomain, final Object shell) {
        assert TopicQueryOperations.isOverlay(overlay);
        
        // We can only configure an overlay in a diagram context
        if (!(ctx instanceof DiagramPresentationContext)) {
            return UnexecutableCommand.INSTANCE;
        }
        
        CompositeCommand retVal = new CompositeCommand("Configure overlay");
        
        final List newContext = new ArrayList();
        
        // Find out what the new context should be (if anything)
        retVal.add(new AbstractCommand("") {
        	protected CommandResult doExecuteWithResult(IProgressMonitor progressMonitor, IAdaptable info)
        		throws ExecutionException {
        		
                GraphicalEditPart classifierTarget = null;
                
                // Check if the user has clicked on a classifier edit part
                DiagramPresentationContext dgmCtx = (DiagramPresentationContext)ctx;
                if (!dgmCtx.getChosenEditParts().isEmpty()) {
                    classifierTarget = findClassifierTarget((EditPart)dgmCtx.getChosenEditParts().iterator().next());
                }
                
                // If there is in fact no context on either the query or the overlay
                if (overlay.getContext().isEmpty() && TopicQueryOperations.getQuery(overlay).getContext().isEmpty()) {
                    // The user clicked on a classifier edit part. Set the
                	//  context to be the semantic element behind this edit part
                	if (classifierTarget != null) {
                    	newContext.add(((View)classifierTarget.getModel()).getElement());
                    // Otherwise, we will need to prompt the user for the context.
                    } else {
                        IWizard wizard = new Wizard() {
                            private ContextSelectionPage page;
                            public void addPages() {
                                page = new ContextSelectionPage(true);
                                addPage(page);
                            }
                            
                            public boolean performFinish() {
                            	newContext.add(page.selectedClassifier);
                                return true;
                            }
                        };

                        final WizardDialog dialog = new WizardDialog((Shell)shell, wizard);
                        
                        // Wait for the user to pick the new context
                        ((Shell)shell).getDisplay().syncExec(new Runnable() {
                        	public void run() {
                        		dialog.open();
                        	}
                        });
 
                        // If the user canceled then we will need to abort
                        if (dialog.getReturnCode() != WizardDialog.OK) {
                            throw new OperationCanceledException();
                        }
                    }
                }
                
                return CommandResult.newOKCommandResult();
            }

            // There is no need to undo/redo the user's context selection
            protected CommandResult doRedoWithResult(IProgressMonitor progressMonitor, IAdaptable info)
                     throws ExecutionException {

                return CommandResult.newOKCommandResult();
            }

            protected CommandResult doUndoWithResult(IProgressMonitor progressMonitor, IAdaptable info)
                     throws ExecutionException {
				
                return CommandResult.newOKCommandResult();
            }
        });
        
        // This command will change the context of the overlay object. This
        //  must be done inside a transaction.
        retVal.add(new AbstractTransactionalCommand(editingDomain, "", Collections.EMPTY_LIST) {
            protected CommandResult doExecuteWithResult(IProgressMonitor monitor, IAdaptable info)
                throws ExecutionException {
                
            	if (newContext.size() > 0) {
            		overlay.getContext().add(newContext.get(0));
            	}
                
                return CommandResult.newOKCommandResult();
            }
        });
        
        return retVal;
    }
}

Add a Query to the Explore Palette

Once the appropriate pieces are written for the query type it is a straightforward task to add a tool to the explore palette. Each tool on the explore palette is really just a query object with a label and icon. When the user uses that tool on the diagram, the tool creates a command from the query and an overlay object with the context. The query and overlay objects are combined with an instance of a diagram presentation context to create a command. If the command is executable, the tool is enabled. Otherwise, the tool is disabled with the "disabled" cursor (circular icon with the diagonal slash). When the user clicks on the diagram using the tool the command is executed.

A query file (.eqx) is used to store all of the attributes of the query object. An example of a query file is provided below.

<?xml version="1.0" encoding="UTF-8"?>
<!--xtools2_universal_type_manager-->
<topic:TopicQuery xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:topic="http://www.ibm.com/xtools/6.0.0/Topic" xmi:id="_-365ELrYEduVSa8gVddLzA" topicId="a.declared.query.type">
    <details xmi:id="_iWxdgkXlEdyLfNBtEtYY5w" key="param1" value="true"/>
    <details xmi:id="_iWxdg0XlEdyLfNBtEtYY5w" key="param2" value="1"/>
    <details xmi:id="_iWxdhEXlEdyLfNBtEtYY5w" key="param3" value="foo"/>
    <details xmi:id="_iWxdhUXlEdyLfNBtEtYY5w" key="param4" value="xyz"/>
    <details xmi:id="_iWxdhkXlEdyLfNBtEtYY5w" key="param5" value="abc"/>
    <details xmi:id="_iWxdh0XlEdyLfNBtEtYY5w" key="param6" value="123"/>
</topic:TopicQuery>

This query file is placed into a plugin and is registered against the explore palette extension point. Along with the file the registration includes a name and an icon to display the query in the palette.

plugin.xml:
...
    <extension
          point="com.ibm.xtools.emf.query.ui.explorePalette">
       <paletteProvider>
         <enablement>
            <with
                  variable="diagramType">
               <or>
                  <equals
                        value="Freeform">
                  </equals>
                  <equals
                        value="Class">
                  </equals>
               </or>
            </with>
          </enablement>
          <contribution>
             <entry
                   description="Some description"
                   id="xyz.drawer"
                   kind="drawer"
                   label="A Drawer">
             </entry>
             <entry
                   description="Perform some exploration function"
                   id="xyz.tool"
                   kind="tool"
                   label="Explore Foo"
                   large_icon="icons/foo.gif
                   path="/xyz.drawer"
                   small_icon="icons/foo_small.gif"
                   system_query_file="foo.eqx">
             </entry>
          </contribution>
       </paletteProvider>
    </extension>
...

The above extension includes an enablement expression which will limit the visibility of the tool and the drawer to only the UML diagram types "Freeform" or "Class." This is an important way to limit the exposure of the tool so that it does not appear in every diagram editor that has an explore palette. Explore palette contributions can also be limited through the use of capabilities.

    <extension
         point="org.eclipse.ui.activities">
      <activity
         id="foo.bar.query.activity"
         description=""
         name="Explore Foo Activity">
      </activity>

      <activityPatternBinding
         pattern="foo\.bar\.query/.*"
         activityId="foo.bar.query.activity">
      </activityPatternBinding>

      <categoryActivityBinding
         categoryId="com.ibm.xtools.modeling"
         activityId="foo.bar.query.activity">
      </categoryActivityBinding>
    </extension>

This extension first defines an activity (aka capability) with the ID "foo.bar.query.activity." The pattern binding will ensure that only plugins with ID's that match the provided pattern will belong to the activity. A category activity binding will categorize the activity underneath an existing activity. In this case the activity is bound to the "Modeling" activity category.


Legal notices