Thursday, March 11, 2010
Eclipse: Using extension-point to avoid cyclic dependency between 2 plugins without refactoring code
I have encountered a cyclic dependency issue while developing application using eclipse based plug-in development environment. Here I am going to explain the issue, which you might be also encountered during the plug-in development and which would have lead you to re-factorize your code to solve this issue. But here I am going to use extension-point using which I can invoke execute code from another plug-in and get results without adding interdependency between them.
Example:
I have eclipse plug-ins PluginA and PluginB, here PluginB is already dependent on PluginA. Now I have some class method in PluginB, and I would like to execute that method from one of the class in PluginA .
- If we add the dependency of PluginA on PluginB then it would result in cyclic dependency.
- Alternatively, what we can do is to re-factor the code in such a way that this method and relevant calsses moved to PluginA so that it would be available in both PluginA and PluginB, but this solution will be expensive in the well deployed system since it involves lot of testing efforts and it may break, if we missed out something during re-factoring, And there may more such logic should come out and again we need to re-factorize with same process.
- Here, I am going to discuss alternate and permanent solution which can be used for calling method of the class which belong to PluginB form some class of PluginA without adding dependency and avoiding the re-factorization of the code.
Solution
Step 1: Create extension point.
- Create new common plug-in let's say PluginC.
- In this common plug-in create on interface as follows:
package test; import java.util.Map; /** * @author Tejash Shah * */ public interface IActionHandler { /** * This method is used by ActionManager class to set the id when contribution is added in plugin.xml * * @param id */ public void setId(String id); /** * @return the unique id of the Action Handler */ public String getId(); /** * @param variables * Here, you can pass the set of Objects which is required by the Action Handler to run the action. Here * key will be name of variable and value can be any Object containing value * @return the result of the execution, which can be any kind of Object */ public Object run(Map
variables); } - Now open plugin.xml for PluginC using Plug-In Editor of eclipse, switch to "Extension Points" tab and click on "Add" button to add the extension point
- Provide the "Extension Point ID", "Extension Point Name" and "Extension Point Schema" as per your requirements.
- On "Finish" it will open editor for defining "Extension Point" schema as follows, switch to "Definition" tab in it
- Click on "New Element" to add additional element bellow, and provide its Name as "ActionHandler"
- Then select "ActionHandler" element and add 2 "New Attribute" named "id" and "handlerClass" for the same and set attribute details as follows:
- For "id" element select value of "Use" as "requied" and rest of them keep it as it is
- For "handlerClass" select the value of "Use" as "requied", then set "Type" to "java" and for value of "Implements" click on "Browse..." button and select the interface IActionHandler which we defined previously.
- After this select "extension" element in the tree and add "New Sequence" and select check box for "Unbounded" for the newly added sequence.
- Then right click on the "Sequence (1-*)" and form context menu select "New" -> "ActionHandler" and for "ActionHAndler" also select check box for the "Unbounded".
- Now, "Save All" the edited files (plugin.xml and schema file).
- If you check the source of the schema (.exsd) file for this extension-point, it would be as follows:
<?xml version='1.0' encoding='UTF-8'?> <!-- Schema file written by PDE --> <schema targetNamespace="Test" xmlns="http://www.w3.org/2001/XMLSchema"> <annotation> <appinfo> <meta.schema plugin="Test" id="test.actionHandler" name="SampleActionHandler"/> </appinfo> <documentation> [Enter description of this extension point.] </documentation> </annotation> <element name="extension"> <annotation> <appinfo> <meta.element /> </appinfo> </annotation> <complexType> <sequence minOccurs="1" maxOccurs="unbounded"> <element ref="ActionHandler" minOccurs="1" maxOccurs="unbounded"/> </sequence> <attribute name="point" type="string" use="required"> <annotation> <documentation> </documentation> </annotation> </attribute> <attribute name="id" type="string"> <annotation> <documentation> </documentation> </annotation> </attribute> <attribute name="name" type="string"> <annotation> <documentation> </documentation> <appinfo> <meta.attribute translatable="true"/> </appinfo> </annotation> </attribute> </complexType> </element> <element name="ActionHandler"> <complexType> <attribute name="id" type="string" use="required"> <annotation> <documentation> </documentation> </annotation> </attribute> <attribute name="handlerClass" type="string" use="required"> <annotation> <documentation> The class which will provide implementation of IActionHandler and where you can write the logic to handle the action. You can it's instance by providing id of ActionHandler and you can call run() method to excute the action </documentation> <appinfo> <meta.attribute kind="java" basedOn=":test.IActionHandler"/> </appinfo> </annotation> </attribute> </complexType> </element> <annotation> <appinfo> <meta.section type="since"/> </appinfo> <documentation> [Enter the first release in which this extension point appears.] </documentation> </annotation> <annotation> <appinfo> <meta.section type="examples"/> </appinfo> <documentation> [Enter extension point usage example here.] </documentation> </annotation> <annotation> <appinfo> <meta.section type="apiinfo"/> </appinfo> <documentation> [Enter API information here.] </documentation> </annotation> <annotation> <appinfo> <meta.section type="implementation"/> </appinfo> <documentation> [Enter information about supplied implementation of this extension point.] </documentation> </annotation> </schema>
- Create new class "SampleActionManager" withing PluginC as follows to load the all contributed actionHandlers.
/** * */ package test; import java.util.HashMap; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.Platform; /** * @author Tejash Shah * */ public class SampleActionManager { private static SampleActionManager handler = new SampleActionManager(); private static final String ATTRIB_ID = "id"; private static final String ATTRIB_CLASS = "handlerClass"; private static final String SAMPEL_ACTION_HANDLER = "test.actionHandler"; // ID given in Plugin.xml while adding extension point private Map
actionHandlers; private SampleActionManager() { loadSonicActionHandler(); } private void loadSonicActionHandler() { actionHandlers = new HashMap (); IConfigurationElement[] configurationElements = getConfigurationElements(SAMPEL_ACTION_HANDLER); for (int i = 0; i < configurationElements.length; i++) { try { Object extensionObject = configurationElements[i].createExecutableExtension(ATTRIB_CLASS); IActionHandler contributor = (IActionHandler)extensionObject; String id = configurationElements[i].getAttribute(ATTRIB_ID); contributor.setId(id); actionHandlers.put(id, contributor); } catch (CoreException e) { e.printStackTrace(); } } } public static SampleActionManager getInstance() { return handler; } public IActionHandler getActionHandlerbyId(String id) { return actionHandlers.get(id); } public static IConfigurationElement[] getConfigurationElements(String extensionPointID) { IExtensionRegistry registry = Platform.getExtensionRegistry(); IExtensionPoint contentTypesXP = registry.getExtensionPoint(extensionPointID); if (contentTypesXP == null) return new IConfigurationElement[0]; IConfigurationElement[] allContentTypeCEs = contentTypesXP.getConfigurationElements(); return allContentTypeCEs; } }
- You can also optionally create an abstract class "AbstractActionHandler" as follows:
/** * */ package test; /** * @author Tejash * */ public abstract class AbstractActionHandler implements IActionHandler { private String id = ""; /* (non-Javadoc) * @see test.IActionHandler#getId() */ public String getId() { return this.id; } /* (non-Javadoc) * @see test.IActionHandler#setId(java.lang.String) */ public void setId(String id) { this.id = id; } }
- Open plugin.xml for the PluginA and PluginB in "Plug-In Editor" switch to "Dependencies" tab and add dependency to PlugInC in both the plug-in and save both plugin.xml
- Open plugin.xml for the PluginB in "Plug-in Editor" and go to "Extensions" tab, click on "Add" button and add the extension for "test.actionHandler" which we have defined in PluginC.
- You can provide "ID" and "Name" for this extension as shown in the bellow image.
- Then select the child node of Extension to provide extension related details as follows:
- e.g. I have given "id" as "MyActionHandler", then click on "handlerClass" link which will launch "New Java Class Wizard". Here you can provide all required details as shown in the above image. And optionally select Sueprclass value as the "AbstractActionHandler"Class if you have created it previously.
- On click on "Finish" it will create default implementation of "MyActionHandler" class, you change run method to invoke any method of class which belong to PluginB. view the sample code as follows:
/** * */ package test; import java.util.Map; /** * @author tejash * */ public class MyActionHandler extends AbstractActionHandler implements IActionHandler { /** * */ public MyActionHandler() { // TODO Auto-generated constructor stub } /* (non-Javadoc) * @see test.IActionHandler#run(java.util.Map) */ public Object run(Map
variables) { // You logic to invoke method particular class goes here //e.g. // return Myclass.myMethod(); // If you have some arguments then you can receive as part of variables object return null; } }
- Now in the PluginA where we wanted to make call to some method of PlugInB, we can get the ActionHandler object for the same and then pass appropriate variable and invoke run method to get the result object as follows:
IActionHandler actionHandler = SampleActionManager.getInstance().getActionHandlerbyId("MyActionHandler"); Map
variables = new HashMap (); //Here you can add as many objects to variables //then make call to run method to execute method in another plug-in and get results as follows: Object result = actionHandler.run(variables); //use the result here for further processing logic - Once you get the result Object, you can write further processing logic on it
You are now able to invoke a class method of PluginB from PluginA without adding dependency or code re-factoring.
Note: Next time if you code across similar issue for other class methods even if they belong to some other plugin say PluginX then also you can use the same "Extension" by just adding dependent on PluginC. Then create ActionHandler using Extension.