Extensions and extension points
The first thing to understand in the registry is the terminology. An extension is a contributed functionality that is often found in the plugin.xml
file as an <extension>
element. The extension itself provides some configuration or customization that can be processed appropriately. An extension is like a USB device, such as a mouse or keyboard. For example, the new feed wizard was added as an extension in the previous chapter:
<extension point="org.eclipse.ui.newWizards"> <category name="Feeds" id="com.packtpub.e4.advanced.feeds.ui.category"/> </extension>
An extension point defines the contract of an extension, along with any required arguments or attributes that an extension must provide. An extension point is like a USB hub that allows extensions (USB devices) to be plugged in. For example, the newWizards
extension point is defined in the plugin.xml
file of the org.eclipse.ui
plug-in as follows:
<extension-point id="newWizards" name="%ExtPoint.newWizards" schema="schema/newWizards.exsd"/>
This refers to an XML schema document that defines the extension content, as shown in the following snippet:
<?xml version='1.0' encoding='UTF-8'?> <!-- Schema file written by PDE --> <schema targetNamespace="org.eclipse.ui" xmlns="http://www.w3.org/2001/XMLSchema"> <annotation> <appInfo> <meta.schema plugin="org.eclipse.ui" id="newWizards" name="Creation Wizards"/> </appInfo> </annotation> <element name="extension"> <complexType> <choice minOccurs="0" maxOccurs="unbounded"> <element ref="category"/> ... </choice> </complexType> </element> <element name="category"> <complexType> <attribute name="id" type="string" use="required"/> <attribute name="name" type="string" use="required"/> </complexType> </element> ... </schema>
The schema defines the point for the org.eclipse.ui.newWizards
extension (the ID is the concatenation of the values of meta.schema plugin
and id
). It declares that the extension has a category, which has required id
and name
attributes.
The schema also allows the PDE to verify whether elements are missing when editing a plugin.xml
file, or provide code completion to insert required or optional elements.
Fortunately, PDE comes with good support to build this schema via a graphical user interface, so the XML can remain hidden.
Creating an extension point
To demonstrate the process of creating a new extension point in Eclipse, a feed parser will be created. This takes a Feed
instance (which contains a URL) and returns an array of FeedItem
instances. Extensions can be contributed to provide different feed parsers; this allows a MockFeedParser
instance to be initially created that can then be substituted for other implementations in future.
Executable extension points tend to have a class
attribute, whose class typically implements a particular interface. An IFeedParser
interface will be created to represent the abstract API of all feed parsers; extensions that provide a feed parser will be expected to implement this interface.
Creating an IFeedParser interface
Since the feed parser could be used outside of a UI, it makes sense to create a new plug-in project called com.packtpub.e4.advanced.feeds
and to refactor the Feed
instance from the UI package (created in Chapter 1, Plugging in to JFace and the Common Navigator Framework) into this package as well.
Tip
When you refactor the Feed
class, ensure that Update fully qualified names in non-Java text files option is selected, or remember to refactor the name in the fully qualified names in the plugin.xml
file, since the class name is used in several places in enablement tests.
Note that you will also need to ensure that the com.packtpub.e4.advanced.feeds
package is exported from the plug-in (from the Runtime tab of the manifest editor) and the package is imported by the com.packtpub.e4.advanced.feeds.ui
plug-in (from the Dependencies tab of the manifest editor).
One this is done, an interface IFeedParser
will be created to parse a feed, as shown in the following code snippet:
import java.util.List; public interface IFeedParser { public List<FeedItem> parseFeed(Feed feed); }
The intent is that this will return a list of items parsed from the feed. To do this, a FeedItem
class will be needed as well. Each FeedItem
instance will have an associated parent Feed
, along with some other metadata.
Note
It would be possible to create a mutable FeedItem
instance with getter/setter pairs for each attribute. However, this leads to the possibility that a feed might be inadvertently mutated after it has been constructed.
A second approach is to use the constructor to add all arguments. Unfortunately, this prevents evolution of the class; as new parameters are added, more constructors need to be created with the values in place.
A better solution is to use the builder pattern, which allows a separate object to assemble the instance. This way, the object can be created but not mutated after it is returned. Visit http://en.wikipedia.org/wiki/Builder_pattern for more information.
To instantiate a FeedItem
class, an inner Builder
class will be used. This has access to the private fields of the FeedItem
class, but permits the object to be returned without a means of mutating it afterwards:
package com.packtpub.e4.advanced.feeds; import java.util.Date; public class FeedItem { // FeedItem fields private Date date; private Feed feed; private FeedItem(Feed feed) { this.feed = feed; } public Date getDate() { return date; } public Feed getFeed() { return feed; } // FeedItem.Builder class public static class Builder { private FeedItem item; public Builder(Feed feed) { item = new FeedItem(feed); } public FeedItem build() { if(item.date == null) { item.date = new Date(); } return item; } public Builder setDate(Date date) { item.date = date; return this; } } }
The preceding example shows how the builder pattern is used, in this case, for two fields: a parent feed
and date
. To extend the FeedItem
class, add accessors in the builder to set other elements such as the following:
- Title
- URL
- HTML
A FeedItem
class can now be instantiated using the following code:
new FeedItem.Builder(feed).setDate(new Date()).build();
Note
Note that the builder pattern is fairly common in Java, as is the literate programming style used; by returning instances of Builder
at the end of each setter method, this allows chaining of method calls into a single expression. The build
method can also perform any necessary validation to verify that all mandatory fields have been assigned and any optional fields are assigned default values if necessary.
Creating a MockFeedParser class
To provide some feed data that can be used by a parser without having to make a network connection, a MockFeedParser
class can be created. This will take a Feed
instance and return a set of hardcoded FeedItems
, allowing further testing to be done.
Because this class isn't intended to be directly visible to downstream users, put the class in a different package, com.packtpub.e4.advanced.feeds.internal
. This way, the package will be hidden by the OSGi runtime and so dependent classes won't be able to see or instantiate it. The following code illustrates the creation of the MockFeedParser
class:
Tip
By default, PDE and the Maven maven-bundle-plugin
hide packages with internal
in their name. This allows the public API to be separated from the internal implementation details to downstream clients.
public class MockFeedParser implements IFeedParser { public List<FeedItem> parseFeed(Feed feed) { List<FeedItem> items = new ArrayList<FeedItem>(3); items.add(new FeedItem.Builder(feed).setTitle("1st").build()); items.add(new FeedItem.Builder(feed).setTitle("2nd").build()); items.add(new FeedItem.Builder(feed).setTitle("3rd").build()); return items; } }
The mock can be populated with more data, such as an HTML body or different dates, if these are desired.
Creating the extension point schema
The extension point for the feed will be called feedParser
, and it will use the IFeedParser
interface.
To create an extension point, open up the plug-in's manifest by double-clicking on the plugin.xml
or MANIFEST.MF
files, or by navigating to Plug-in tools | Open Manifest from the project. Switch to the Extension Points tab, click on Add, and enter feedParser
for both the ID and the name in the dialog that shows up. This is shown in the following screenshot:
After clicking on Finish, the schema editor will be shown:
The Description, Since, Examples, API Information, Supplied Implementation, and Copyright are all text-based fields that are used to generate the documentation and can be left blank. However, this documentation will be shown to users in the future and is used to generate the information as seen in the Eclipse help center and at http://help.eclipse.org.
Switching to the Definition tab allows the contents of the extension point to be modified. Select the extension element, click on the New Element button to its right, and give it the name feedParser
. This will be the name of the XML element that is expected by clients. To give it an attribute value, ensure feedParser is selected, click on New Attribute, and give it the name class
. Its type will be java and it will be required; use the Browse... button next to the Implements textbox to select the IFeedParser
created earlier.
The resulting schema definition will now look something like what is shown in the following screenshot:
Under the covers, the extension is represented in two different files. The first is the plugin.xml
file, which includes the following:
<?xml version="1.0" encoding="UTF-8"?> <?eclipse version="3.4"?> <plugin> <extension-point id="feedParser" name="feedParser" schema="schema/feedParser.exsd"/> </plugin>
The schema reference points to the schema definition, which was created by the UI previously.
Tip
Note that the build.properties
file should be updated to include the schema
directory in the binary output; otherwise, implementors of the plug-in won't be able to verify whether the feedParser
element is correctly provided or not.
The schema is not necessary for the extension mechanism to work; it is mainly used by PDE when allowing the element to be created in the plugin.xml
file. However, it has value in communicating documentation to other users of the extension point in both its intent and its requirements, and so providing the schema is considered best practice.
There are other values that can be defined on the extension point. For example, each element has one of the following values:
- Name: This is the name that will be used for the element or attribute. This must be a valid XML name for elements and attributes.
- Deprecated: This is
false
by default, but can be changed totrue
. This is used to indicate to clients that an extension point should no longer be used; it is often combined with the description to suggest an alternative or replacement function. - Translatable: If an attribute has a value that can be translated (such as a label or another human-readable string), then this value should be
true
. If so, the value in theplugin.xml
may be a percent string such as%description
, and the value will be pulled out automatically from the localizedplugin.properties
file by Eclipse when the extension point is loaded. - Description: This is a human-readable description that can be shown by PDE or converted into a help document that indicates how the point should be used.
- Use: The value for this can be
optional
,required
, ordefault
. If an attribute is marked asoptional
, then it does not need to be present. If an attribute is marked asrequired
, then it must be present. If it is marked asdefault
, then a default value box is shown that allows the default value to be defined, which is used when the attribute is missing. - Type: This is the attribute type. The attribute value can be of one of the following types:
- Boolean: This specifies that the attribute can have the value
true
orfalse
. - String: This specifies that the attribute value can be a string. Strings may be translatable, and they can have restrictions that are preset values that the string can take (such as
North
,South
,East
, andWest
orUP
andDOWN
). - Java: This specifies that the attribute must be a type that either extends the specified class or implements the specified interface.
- Resource: This specifies that the attribute can have a resource type.
- Identifier: This specifies that the attribute might reference another ID in another schema document using an XPath-like expression of the form
org.eclipse.jdt.ui.javaDocWizard/@point
, whereorg.eclipse.jdt.ui
is the plug-in namespace,javaDocWizard
is an extension, and@point
is the attributepoint
within that element.
Note
There is a DTD approximation that is used to show an approximate Document Type Definition of the children. If an element has no children, then it will show
EMPTY
; for a text element, it will show(#PCDATA)
.PCDATA, which stands for Parsed Character Data, is used in HTML and originally came from SGML.
- Boolean: This specifies that the attribute can have the value
In addition, elements can be repeated. The schema editor permits a sequence of elements (in other words, a list) or a choice of items (one of a set). These are known as compositors and can be switched between using the Type drop-down. Compositors have a minimum value and a maximum value; if the minimum is zero, then it is effectively optional. The maximum value, if specified, allows a fixed number to be specified (for example months=12
); but if the unbounded option is checked, then the compositor can have any number of child elements.
Typically, an extension point will permit more than one element to be added. To enable this, it is necessary to add a Sequence
element underneath the extension
element. The sequence is necessary to permit more than one element to be provided.
In the PDE schema editor, click on the extension element and choose New Sequence. The minimum value should be 1
and the sequence should be unbounded; these are the typical defaults, as shown in the following screenshot:
To add the feedParser
point to the extension, drag-and-drop the feedParser element underneath the Sequence element, as shown in the following screenshot:
The feedParser.esxd
schema should be similar to the following:
<?xml version='1.0' encoding='UTF-8'?> <!-- Schema file written by PDE --> <schema targetNamespace="com.packtpub.e4.advanced.feeds" xmlns="http://www.w3.org/2001/XMLSchema"> <annotation> <appinfo> <meta.schema plugin="com.packtpub.e4.advanced.feeds" id="feedParser" name="feedParser"/> </appinfo> <documentation>…</documentation> </annotation> <element name="extension"> <annotation> <appinfo> <meta.element /> </appinfo> </annotation> <complexType> <sequence minOccurs="1" maxOccurs="unbounded"> <element ref="feedParser"/> </sequence> <attribute name="point" type="string" use="required"/> <attribute name="id" type="string" use="required"/> <attribute name="name" type="string"/> </complexType> </element> <element name="feedParser"> <complexType> <attribute name="class" type="string" use="required"> <annotation> <documentation>…</documentation> <appinfo> <meta.attribute kind="java" basedOn=":com.packtpub.e4.advanced.feeds.IFeedParser"/> </appinfo> </annotation> </attribute> </complexType> </element> </schema>
Now the schema definition is complete.
Using the extension point
As with other extensions, they are added to a plug-in's plugin.xml
file. It's not uncommon for a plug-in to define both the extension point and an extension in the same file.
Tip
Note that defining an extension point in the same file as an extension means that there is no way of removing that extension from the platform if that extension point is used elsewhere. Providing a plug-in that defines the extension point and then separate plug-ins for the extensions allows the extensions to be individually removed from the platform.
To add MockFeedParser
to the plugin.xml
file, add the following:
<extension point="com.packtpub.e4.advanced.feeds.feedParser"> <feedParser class= "com.packtpub.e4.advanced.feeds.internal.MockFeedParser"/> </extension>
In order to provide an easy way for clients to obtain a list of feed parsers, a class FeedParserFactory
will be created in the feeds
plug-in. This class will be used to provide a list of IFeedParser
instances without having a specific API dependency on the extension registry itself. The code is as follows:
package com.packtpub.e4.advanced.feeds; public class FeedParserFactory { private static FeedParserFactory DEFAULT; public static FeedParserFactory getDefault() { if (DEFAULT == null) { DEFAULT = new FeedParserFactory(); } return DEFAULT; } }
The registry is managed with an IExtensionRegistry
interface, which can be accessed via the org.eclipse.equinox.registry
bundle. It is possible to dynamically register extension elements, but the most common practice is to read the extensions that exist in the runtime. To do this, add the org.eclipse.equinox.registry
bundle to the list of imported bundles in the manifest as follows:
Require-Bundle: org.eclipse.equinox.registry
The extension registry manages a set of extension points, which are identified with an ID—typically consisting of the contributing bundle ID and the specific ID of the extension point. From the preceding definition, the values will be com.packtpub.e4.advanced.feeds
and feedParser
, respectively. Add the following to the FeedParserFactory
class:
public List<IFeedParser> getFeedParsers() { List<IFeedParser> parsers = new ArrayList<IFeedParser>(); IExtensionRegistry registry = RegistryFactory.getRegistry(); IExtensionPoint extensionPoint = registry.getExtensionPoint( "com.packtpub.e4.advanced.feeds", "feedParser"); … // continued below return parsers; }
Note
IExtensionPoint
represents the extension point definition itself. This might return null
if there is no such extension point, so it should be checked before use. In previous versions of Eclipse, the identifiers used to be stored as a single string, such as com.packtpub.e4.advanced.feeds.feedParser
. However, this resulted in many thousands of strings that took up a lot of space in the PermGen area of the JVM. By splitting them into two separate strings, many extensions in the same plug-in share the same namespace, which results in just a single entry in the PermGen area. Note that PermGen has been removed in the latest JVM versions.
If the preceding code returns a non-null value, it can be interrogated further. The most common call is getConfigurationElements
, which allows the extensions to be parsed. This gives a tree-like view of the content of the extension, mapping closely to the structure of the entries in the plugin.xml
file:
if (extensionPoint != null) { IConfigurationElement[] elements = extensionPoint.getConfigurationElements(); for (int i = 0; i < elements.length; i++) { IConfigurationElement element = elements[i]; … // continued below } }
If the extension point contained only textual information (for example, Mylyn's use of the registry to store URLs such as http://bugs.eclipse.org to report Eclipse bugs), then the element could be interrogated to return the actual textual value. In the feedParser
example, the attribute containing the class name is the one of interest.
In this case, the extension point defines a class to be instantiated. To do this, there is a method called createExecutableExtension
that takes an attribute name and then instantiates a class using that name from the appropriate bundle. In effect, this is similar to class.forName(extension.getAttribute("class"))
, but uses the correct ClassLoader
.
Note
Although it might be tempting to think that using Class.forName()
would work on the returned class name, this doesn't work in the case where the class comes from outside the current plug-in. Since each bundle has its own ClassLoader
and the plug-in that uses the extension is almost always not the bundle that provides the extension, it would not work in most cases.
Since it's possible that the extension has a semantic error, or that the plug-in might not be loaded successfully, the instantiation of the class should try
and catch CoreException
. If an error occurs, then the extension won't be useful; the runtime might choose to log the error (for further diagnostics) if appropriate. Don't forget to check whether the returned instance is of the correct type using instanceof
; this will also look for null
.
The object is instantiated with a zero-argument constructor and returned to the caller, shown as follows:
try { Object parser = element.createExecutableExtension("class"); if (parser instanceof IFeedParser) { parsers.add((IFeedParser) parser); } } catch (CoreException e) { // ignore or log as appropriate }
Caching extension points
Should the return values of the extension be cached? It depends on what the use case is likely to be. If they are only going to be used transiently, then there might be no point in creating them each time. On the other hand, if they are cached, then additional code will need to be created to register listeners to note when the plug-ins are uninstalled.
The extension registry does a reasonable job of caching and returning values from the calls, so these can be assumed to be fast. However, for the instantiated objects, if identity is important, then it might be necessary to arrange some kind of cache of the executable extensions.
If the return result is cached, then any new plug-ins that are subsequently installed might not be seen.
The extension registry also has listener support; calling addListener
on the registry will provide a way of picking up changes to a specific extension point. This can be used to update any caches when changes occur.
Integrating the extension with the content and label providers
Having defined an extension point with a FeedItem
provider, the next step is to integrate it with the FeedLabelProvider
and the FeedContentProvider
classes created in the previous chapter. Integrating it into the FeedLabelProvider
class is really simple because this is just an addition of a couple of lines:
public String getText(Object element) { if (element instanceof Feed) { return ((Feed) element).getName(); } else if (element instanceof FeedItem) { return ((FeedItem)element).getTitle(); } else { return null; } }
When a FeedItem
element is seen in the tree, its title will be used as the label.
Integrating with the FeedContentProvider
class is the next step. To start off, declare that all Feed
elements have children and the parent of the FeedItem
element is Feed
itself:
public Object getParent(Object element) { if(element instanceof FeedItem) { return ((FeedItem) element).getFeed(); } return null; } public boolean hasChildren(Object element) { if(element instanceof Feed) { return true; } return false; }
To parse the feed URL, perform the following steps:
- Acquire the
IFeedParser
list (which comes from the extension registry). - Iterate through the list and attempt to acquire the
FeedItem
list. - If the value is non-null, return.
The code will be similar to the following:
public Object[] getChildren(Object parentElement) { Object[] result = NO_CHILDREN; if (parentElement instanceof IResource) { … } else if (parentElement instanceof Feed) { Feed feed = (Feed)parentElement; FeedParserFactory factory = FeedParserFactory.getDefault(); List<IFeedParser> parsers = factory.getFeedParsers(); for (IFeedParser parser : parsers) { List<FeedItem> items = parser.parseFeed(feed); if(items != null && !items.isEmpty()) { return items.toArray(); } } } }
This pattern allows the FeedParserFactory
class to return items in the order of preference such that the first parser that handles the feed can return a value.
Run the Eclipse application, create a feed (the URL and title won't matter at this point), and then drill down into the feeds and then into a single feed. The test data that was used in the MockFeedParser
class should be shown in the list, as shown in the following screenshot:
Showing a feed in the browser
Now that feeds are showing as individual elements in the content provider, the ShowFeedInBrowserHandler
class can be copied and modified to allow individual FeedItem
entries to be added. Refer to the Adding commands to the common navigator section in Chapter 1, Plugging in to JFace and the Common Navigator Framework.
Copy the ShowFeedInBrowserHandler
class to ShowFeedItemInBrowserHandler
. The only change that's needed is the change from Feed
to FeedItem
in the instanceof
test and subsequent cast:
public class ShowFeedItemInBrowserHandler extends AbstractHandler{ public Object execute(ExecutionEvent event) throws ExecutionException { ISelection selection = HandlerUtil.getCurrentSelection(event); if (selection instanceof IStructuredSelection) { Iterator<?> it = ((IStructuredSelection) selection) .iterator(); while (it.hasNext()) { Object object = (Object) it.next(); // if (object instanceof Feed) { // String url = ((Feed) object).getUrl(); if (object instanceof FeedItem) { String url = ((FeedItem) object).getUrl(); ...
It will also be necessary to duplicate both the handler and command entries in the plugin.xml
file, along with the change to the feed item. This is shown in the following:
<extension point="org.eclipse.ui.commands"> <command description="Shows the selected feed item in browser" defaultHandler= "com.packtpub.e4.advanced.feeds.ui.ShowFeedItemInBrowserHandler" id= "com.packtpub.e4.advanced.feeds.ui.ShowFeedItemInBrowserCommand" name="Show Feed Item in Browser"/> </extension>
Enabling the menu item is necessary to show the command associated with any selected FeedItem
instances as follows:
<extension point="org.eclipse.ui.menus"> <menuContribution allPopups="false" locationURI= "popup:org.eclipse.ui.navigator.ProjectExplorer#PopupMenu"> <command style="push" commandId= "com.packtpub.e4.advanced.feeds.ui.ShowFeedItemInBrowserCommand"> <visibleWhen checkEnabled="false"> <with variable="selection"> <iterate ifEmpty="false" operator="or"> <adapt type= "com.packtpub.e4.advanced.feeds.FeedItem"/> </iterate> </with> </visibleWhen> </command> </menuContribution> </extension>
If the application is run, then a MalformedURLException
will be generated, since the MockFeedParser
doesn't have any URLs set on it. Modify it as follows:
items.add(new FeedItem.Builder(feed).setTitle("AlBlue's Blog"). setUrl("http://alblue.bandlem.com").build()); items.add(new FeedItem.Builder(feed).setTitle("Packt Publishing"). setUrl("http://www.packtpub.com").build()); items.add(new FeedItem.Builder(feed).setTitle("Source Code"). setUrl("https://github.com/alblue/com.packtpub.e4.advanced"). build());
Now running the application will show the URLs when selected, as shown in the following screenshot:
Implementing a real feed parser
The MockFeedParser
can be replaced with an implementation to parse RSS feeds. This requires parsing some simple XML to understand the feed reference.
An RSS feed looks like:
<rss version="2.0"> <channel> <title>Eclipse Example Feed</title> <description>Descriptive feed information</description> <link>http://eclipse.org/</link> <item> <title>Luna released</title> <description>Eclipse Luna has been released</description> <link>http://eclipse.org/luna/</link> <pubDate>Wed, 25 June 2014 09:00:00 -0500</pubDate> </item> </channel> </rss>
Unfortunately, since this is XML, it will require parsing. There are several ways of doing this, but using a DocumentBuilder
instance will allow the elements to be iterated through and pull out the title
and link
elements, along with pubDate
.
Create an RSSFeedParser
class (with a couple of helper methods) to parse a date from the RFC822 format and a mechanism to parse a text value from an Element
, as shown in the following code snippet:
package com.packtpub.e4.advanced.feeds.internal; public class RSSFeedParser implements IFeedParser { public List<FeedItem> parseFeed(Feed feed) { … } private Date parseDate(String date) { try { return new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz") .parse(date); } catch (Exception e) { return null; } } private String getTextValueOf(Node item, String element) { try { return ((Element) item).getElementsByTagName(element). item(0).getTextContent(); } catch (Exception e) { return null; } } }
These helper methods can be used to parse the elements out of the feed by parsing the URL of the Feed
with a DocumentBuilder
instance and then using getElementsByName
to find the item
elements. The parseFeed
method will be similar to the following code:
public List<FeedItem> parseFeed(Feed feed) { try { List<FeedItem> feedItems = new ArrayList<FeedItem>(); DocumentBuilder builder = DocumentBuilderFactory.newInstance() .newDocumentBuilder(); Document doc = builder.parse( new URL(feed.getUrl()).openStream()); NodeList items = doc.getElementsByTagName("item"); for (int i = 0; i < items.getLength(); i++) { Node item = items.item(i); Builder feedItem = new FeedItem.Builder(feed); feedItem.setTitle(getTextValueOf(item,"title")); feedItem.setUrl(getTextValueOf(item,"link")); feedItem.setDate(parseDate(getTextValueOf(item,"pubDate"))); feedItems.add(feedItem.build()); } return feedItems; } catch (Exception e) { return null; } }
This looks for elements called item
, finds child text elements with title
and link
, and sets them into the feed.
To add this to the Eclipse instance, add RSSFeedParser
to the plugin.xml
filein the feedParser
extension point:
<extension point="com.packtpub.e4.advanced.feeds.feedParser"> <feedParser class= "com.packtpub.e4.advanced.feeds.internal.RSSFeedParser"/> </extension>
If the MockFeedParser
instance is still present, the ordering in the plugin.xml
file will be important. By default, the order returned in the array is the same as they are in the file. FeedParserFactory
returns the first parser that successfully parses the feed; therefore, if the MockFeedParser
is first, then the real content of the feed will not be returned.
Tip
The feed parser is non-optimal; the feed will be parsed and potentially reacquired multiple times. It would be desirable to have the contents of the source document cached, but this is an optimization left for you.
Run the Eclipse application and use the add feed wizard to add a feed for the Packt Publishing RSS feed, http://www.packtpub.com/rss.xml.
Adding support for Atom
Not all feeds use RSS as a feed type, partly because RSS was incompletely specified and had several slightly different incompatible feed formats. Atom was designed to resolve the issues with RSS but, in practice, there are approximately an equal number of feeds specified in RSS and Atom.
An Atom feed looks like:
<feed xmlns="http://www.w3.org/2005/Atom"> <title>AlBlue's Blog</title> <entry> <title>Eclipse 4 Book Published</title> <updated>2013-07-01T12:00:00+01:00</updated> <link href=" http://alblue.bandlem.com/2013/07/eclipse-book-published.html"/> </entry> </feed>
Parsing it will not be significantly different from before; the structure can be used to pull out the entry
and the contained title
, link
, and updated
references. However, there are a couple of points that are worth noting in this example:
- The Java Date APIs do not understand colons in time zones, so
+01:00
must be converted to+0100
to be parsed - The reference for the link is stored inside an
href
attribute instead of as a text node
An AtomFeedParser
class can be created as follows:
public class AtomFeedParser implements IFeedParser { public List<FeedItem> parseFeed(Feed feed) { try { List<FeedItem> feedItems = new ArrayList<FeedItem>(); DocumentBuilder builder = DocumentBuilderFactory .newInstance().newDocumentBuilder(); Document doc = builder.parse( new URL(feed.getUrl()).openStream()); NodeList items = doc.getElementsByTagName("entry"); for (int i = 0; i < items.getLength(); i++) { Node item = items.item(i); Builder feedItem = new FeedItem.Builder(feed); feedItem.setTitle(getTextValueOf(item, "title")); feedItem.setUrl(getTextValueOfAttribute( item, "link", "href")); feedItem.setDate(parseDate(getTextValueOf( item,"updated"))); feedItems.add(feedItem.build()); } return feedItems; } catch (Exception e) { return null; } } … }
The parseDate
method is similar to the following code:
private Date parseDate(String date) { try { if (date.length() > 22 && date.charAt(22) == ':') { date = date.substring(0, 22) + date.substring(23); } return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") .parse(date); } catch (Exception e) { return null; } }
To parse an attribute value out from XML, an additional helper method can be created as follows:
private String getTextValueOfAttribute(Node item, String element, String attribute) { try { return ((Element) item).getElementsByTagName(element).item(0) .getAttributes().getNamedItem(attribute).getNodeValue(); } catch (Exception e) { return null; } }
Adding the class into the extension points will allow the element to be parsed if the feed is not an RSS feed as follows:
<extension point="com.packtpub.e4.advanced.feeds.feedParser"> <feedParser class= "com.packtpub.e4.advanced.feeds.internal.RSSFeedParser"/> <feedParser class= "com.packtpub.e4.advanced.feeds.internal.AtomFeedParser"/> </extension>
Now when the feed is downloaded, it will attempt to parse it as RSS first and then fall back to Atom afterwards.
Making the parser namespace aware
The Atom specification uses XML namespaces. To parse the feed properly, the document builder must be specified as namespace aware and the elements lookup needs to use the equivalent getElementsByTagNameNS
.
In the AtomFeedParser
class, define a static
constant to hold the Atom namespace:
private static final String ATOM = "http://www.w3.org/2005/Atom";
Then, replace the calls to getElementsByTagName
with the namespace aware equivalent getElementsByTagNameNS
, as shown in the following code:
// NodeList items = doc.getElementsByTagName("entry"); NodeList items = doc.getElementsByTagNameNS(ATOM, "entry"); … // return ((Element) item) // .getElementsByTagName(element).item(0) return ((Element) item) .getElementsByTagNameNS(ATOM,element).item(0)
Finally, to ensure that the document builder is using namespace aware parsing, the DocumentBuilderFactory
instance needs to be appropriately configured before DocumentBuilder
is instantiated. The code is as follows:
// DocumentBuilder builder = DocumentBuilderFactory.newInstance() // .newDocumentBuilder(); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); DocumentBuilder builder = factory.newDocumentBuilder();
When the Atom feed is parsed, it will be correctly represented if there are multiple namespaces or a default namespace is not specified.
Priority and ordering
The order of the IConfigurationElement
instances returned by the registry should not be relied upon for consistency. Relying on a specific order will prevent others from easily being able to contribute additional implementations from other plug-ins. If an ordering is desired, additional metadata should be added to the extension to allow the ordering to be calculated after retrieval.
In this case, MockFeedParser
should have the lowest priority and RSSFeedParser
should have a higher priority than AtomFeedParser
. To implement this, an extra attribute priority
will be added to the extension point so that the results can be processed after loading.
Although any ordering can be used (for example, high
/medium
/low
, or top
/bottom
), it is easier to deal with numerical priorities and perform an integer sort. Using both positive and negative numbers allows a full range of priorities and also allows some extensions to register themselves as less desirable than the default, which can remain at zero.
Modify the extension point schema feedParser.esxd
and add a new attribute under the feedParser element called priority
. Since the XML schema for extension points does not permit numeric values, use string as the type and the value can be parsed afterwards. This is shown in the following screenshot:
Now, in the plugin.xml
file where the feeds are defined, add a priority of -1
to MockFeedParser
and a priority of 1
to RSSFeedParser
:
<extension point="com.packtpub.e4.advanced.feeds.feedParser"> <feedParser priority="1" class="com.packtpub.e4.advanced.feeds.internal.RSSFeedParser"/> <feedParser class="com.packtpub.e4.advanced.feeds.internal.AtomFeedParser"/> <feedParser priority="-1" class="com.packtpub.e4.advanced.feeds.internal.MockFeedParser"/> </extension>
Since the IFeedParser
interface doesn't have a priority attribute that can be set, the IConfigurationElement
instances must be sorted after retrieval, but before iteration. To do this, the Arrays
class can be used by calling sort
with an appropriate Comparator
.
Create a class called FeedParserConfigurationComparator
in the internal
package, and then make it implement Comparator
with a target type of IConfigurationElement
. Write a helper method to parse an integer from a string, treating a missing value (null
) or integers that cannot be parsed as a zero value.
The class will be similar to the following code:
public class FeedParserConfigurationComparator implements Comparator<IConfigurationElement> { private static final String PRIORITY = "priority"; public int compare(IConfigurationElement o1, IConfigurationElement o2) { String a1 = o1.getAttribute(PRIORITY); String a2 = o2.getAttribute(PRIORITY); return parseInt(a2) – parseInt(a1); } private int parseInt(String string) { try { return Integer.parseInt(string); } catch (Exception e) { return 0; } } }
This will sort the extensions based on the numerical value of the priority
attribute. To invoke it, call a sort
method after the configuration elements are accessed in FeedParserFactory
:
IConfigurationElement[] elements = extensionPoint.getConfigurationElements(); Arrays.sort(elements, new FeedParserConfigurationComparator());
Now when the extensions are processed, they will be done in priority order.
Note
Using a singleton for comparators?
It's possible to use the singleton pattern for the comparator, since it uses no instance variables. By making the constructor private and instantiating a public static final
constant, it can be referred to with FeedParserConfigurationComparator.INSTANCE
. This is useful if the comparator is being used in a lot of places, since the same instance will be reused. However, if it is used infrequently, creating and disposing the comparator will be fairly fast and will not permanently stay in memory when not in use. The tradeoff between memory and CPU utilization will depend on the expected use cases.
Executable extensions and data
When an extension point is created, it is called with a zero-argument constructor. The result is that most extensions in Eclipse are pre-configured with whatever data they need with no further customization.
It is possible to pass through additional configuration data in the plugin.xml
file and have that parsed at start-up. To do this, the IExecutableExtension
interface provides a setInitializationData
method that passes in information defined statically within the plugin.xml
file.
By adding the IExecutableExtension
interface to the concrete feed parser instance, it's possible to have additional data from the plugin.xml
file passed into the class itself. This may allow the same implementation to perform in different ways based on values contained within the plugin.xml
file; for example, a limit could be placed on the parser to indicate how many feed entries would be shown in the list.
Add the IExecutableExtension
interface to the AtomFeedParser
class, and then add following code:
private int max = Integer.MAX_VALUE; public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException { if (data instanceof String) { try { max = Integer.parseInt((String) data); } catch (Exception e) { // Ignore } } }
The max
field can be used to limit the number of elements returned from the feed:
for (int i = 0; i < items.getLength() && i < max; i++)
The data can be passed in a couple of different ways. The easiest way (if it's a single value or can be represented as a string) is to pass it after the class name of the attribute with a colon. Modify the plugin.xml
as shown:
<feedParser class="com.packtpub.e4.advanced.feeds.internal.AtomFeedParser:2"/>
Note the :2
at the end of the class name. Upon initialization of the feed parser, the value will be passed in as the data
object.
Note
The value of propertyName
in this case will be class
, which is taken from the createExecutableExtension
method call previously and refers to the XML attribute class
in this entry.
Now when the application is run, Atom feeds such as http://alblue.bandlem.com/atom.xml will only return a maximum of two values. The same change can be applied to RSSFeedParser
.
Note
There are several ways of pulling configuration information out from an extension. The simplest way is to parse the string appropriately after the class name, and this suffices for most cases.
More complex cases can simply parse the IConfigurationElement
interface (shown in the next example).
There is also an ancient plug-in configuration process that can be invoked if null
is passed as an attribute name and where elements and parameters use a specific hardcoded pattern. Parameters are parsed from name/value pairs and passed into the data as a Hashtable
:
<feedParser> <parser class="class.name"> <parameter name="maxCount" value="1"> </parser> </feedParser>
This is for compatibility with older versions of Eclipse and should not be used.
Executable extension factories
Sometimes, it's not possible to add an interface to the class that requires instantiation. This is either because it doesn't make sense for the component itself to implement IExecutableExtension
, or because it's a closed source component, such as a database driver that cannot be modified.
Tip
Note that the DBFactory
example is not used or needed by the Feed
example; it's used to demonstrate the need to introduce factories where the user has no ability to change the class being instantiated. It is provided here just as an example.
This issue can be resolved by using an executable extension factory. This permits a factory to be used to instantiate the extension. For example, if a database connection was defined in an extension point, it might be similar to the following:
<database user="example" pass="pass" url="jdbc:h2:/tmp/test"/>
The desired result of this would be to provide an instantiated JDBC Connection
object of the right type. Clearly, the driver itself cannot be modified to implement the IExecutableExtension
interface; but it is possible to provide it as an extension with a factory:
public class DBFactory implements IExecutableExtension, IExecutableExtensionFactory { private String url; private String user; private String pass; public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException { url = config.getAttribute("url"); user = config.getAttribute("user"); pass = config.getAttribute("pass"); } public Object create() throws CoreException { try { return DriverManager.getConnection(url, user, pass); } catch (SQLException e) { throw new CoreException(new Status(IStatus.ERROR, "com.packtpub.e4.advanced.feeds", "Failed to get driver connection for " + url, e)); } } }
The preceding example shows another way of getting the configuration data—by direct parsing of the IConfigurationElement
class. Of course, placing user IDs and passwords hardcoded into an extension point is not good practice; the example here is used because the Connection
interface will be familiar to many readers.
Note that since the DriverManager
class does a lookup based on the URL to acquire the database connection, it will be necessary to have those driver classes available on the bundle's classpath—either as a direct import or as a bundle dependency.
Tip
It's fairly common for an IExecutableExtensionFactory
instance to also inherit IExecutableExtension
, as that is the only way of receiving data if it is required. If the factory does not need such data (for example, it is instantiating an in-memory structure, or using external file configuration), it might be possible to have an IExecutableExtensionFactory
interface that does not implement IExecutableExtension
.
Using the extension registry outside of OSGi
Although the extension registry can work outside of OSGi, it's not easy to do so. This is partly is due to the set of libraries that are required and partly because there is additional setup required in order to provide the registry with the right state.
There are a number of dependencies that need to be provided for the extension registry to work, including the Equinox Supplemental bundle. The supplemental bundle provides commonly used features such as NLS
and Debug
, which are liberally spread around the Equinox codebase and are required for running outside of Equinox. In addition (in Kepler and below), the Debug
class transitively depends on the OSGi ServiceTracker
, which means that even when running outside of OSGi, this package is required.
The minimum setup for a Java application (running outside of OSGi) to use the extension registry is as follows:
org.eclipse.equinox.registry
org.eclipse.equinox.common
(providesCoreException
)org.eclipse.equinox.supplement
(providesNLS
andDebug
)org.osgi.core
(providesServiceTracker
)
Note that if running in a non-Equinox OSGi container, org.osgi.core
will not be needed. If running in Equinox, then the supplemental bundle is not needed, as this provides a copy of the public classes and interfaces used.
In Eclipse Luna and above (org.eclipse.equinox.supplement
version 3.5.100 or higher), the dependency on org.osgi.core
has been removed so this particular dependency is no longer necessary.
Tip
The supplemental bundle can be downloaded from the Equinox downloads page at Kepler SR2 is http://download.eclipse.org/equinox/drops/R-KeplerSR2-201402211700/download.php?dropFile=org.eclipse.equinox.supplement_1.5.0.v20130812-2109.jar.
To configure the registry to run outside of OSGi, it is necessary to set up a registry instance. There is an IRegistryProvider
interface that can be used to define what registry instance should be returned when RegistryFactory.getRegistry
is called. By default, this will return null
until a registry has been set; in an OSGi runtime, this is done by the registry bundle itself starting.
To create a Registry
instance directly, there is a RegistryFactory.create
method that takes a RegistryStrategy
instance along with a couple of tokens. The tokens can be used in secure environments to prevent callers from modifying or adding to the registry. All three elements can be left as null
.
The net effect is that adding this to the start-up of a Java application, which needs the registry, will work:
public class NonOsgi { private final main(String[] args) throws Exception { RegistryFactory.setDefaultRegistryProvider( new IRegistryProvider() { private final IExtensionRegistry registry = RegistryFactory.createRegistry(null, null, null); public IExtensionRegistry getRegistry() { return registry; } }); … // register or look up contributions } }
Once the registry has been set, it can be acquired from the RegistryFactory
interface:
IExtensionRegistry reg = RegistryFactory.getRegistry();
However, unlike OSGi (where the registry automatically scans for bundles as they are inserted and registers the extension elements), in a standalone Java application this needs to be done manually.
To load a single plugin.xm
l file into the registry, it is necessary to first create a contributor (which in OSGi is the bundle, but in a Java application can be a different mechanism) and then add a contribution. The contribution itself is an InputStream
that represents the plugin.xml
file, called feeds.xml
here:
IContributor contributor = ContributorFactorySimple .createContributor("com.packtpub.e4.advanced.feeds"); reg.addContribution(Main.class.getResourceAsStream("/feeds.xml"), contributor, false, "/feeds.xml", null, null);
This loads the file feeds.xml
from the classpath and uses that to register the feeds plug-in mentioned previously. This XML file may be built in-memory or loaded from other input sources, but has the same effect and content as the plugin.xml
of the original bundle.
Tip
Unfortunately, it isn't possible to use plugin.xml
as the filename, as the org.eclipse.equinox.registry
bundle also has a file called plugin.xml
, and the standard Java getResourceAsStream
will load the first one that it sees. As a result, depending on whether your class or the registry JAR is first on the path, you might see a different result. See the Loading all extensions from the classpath section to find out how to handle this.
The extension can then be acquired as usual or via the FeedParserFactory
method defined previously.
Using the extension registry cache
In typical Eclipse usage, the extension registry remains the same between Eclipse restarts, and adding (or removing) plug-ins updates the registry appropriately. To save time at start-up, the extension registry cache is used to store its contents at shutdown, and if available, loads it at start-up.
Contributions can be persistent or non-persistent. A persistent registration is kept such that a restart of the application will have the same value; a non-persistent registration is for this JVM only and will be lost on restart. Eclipse uses this mechanism at start-up to ensure a faster start-up time and to avoid having to reparse the plugin.xml
files each time.
To take advantage of the cache, a RegistryStrategy
instance must be provided with one or more cache directories to store the content. If at least one directory is writable, the registry can save new extensions; if the list of directories is empty or all of the directories are read-only, then the registry will not persist content between restarts.
Modify the IRegistryProvider
interface in the NonOsgi
class to return a directory, and then pass that into RegistryStrategy
along with an array of false
values:
new IRegistryProvider() { private final IExtensionRegistry registry = RegistryFactory .createRegistry(getRegistryStrategy(), null, null); private RegistryStrategy getRegistryStrategy() { File cache = new File( System.getProperty("java.io.tmpdir"),"cache"); return new RegistryStrategy( new File[] { cache }, new boolean[] { false } ); }
The second parameter to RegistryStrategy
is an array of boolean
values (one per entry in the File
array) that indicates whether the cache is read-only or not. If this parameter is null
, then the cache directories are all considered read-only.
Finally, note that if using the registry in caching mode, it is necessary to stop the registry after use. This causes the data entries to be persisted to disk:
reg.stop(null);
To enable the contribution to be added persistently, the boolean persist
parameter should be specified as true
:
reg.addContribution(Main.class.getResourceAsStream("/feeds.xml"), contributor, /* false */ true, "/feeds.xml", null, null);
Now, if the contribution is added and the line is commented out, then re-running the application will load the entry from the cache.
If the cache needs to be rebuilt, then the clearRegistryCache
method of ExtensionRegistry
will need to be called at start-up. This is equivalent to passing the -clean
parameter to Eclipse at start-up. Since this is not an interface method, the call will need to be cast to the explicit ExtensionRegistry
class. Alternatively, the cache directory can be deleted prior to starting the registry, and it will be rebuilt automatically.
Loading all extensions from the classpath
The getResourceAsStream
method returns the first element found in the classpath. For applications that can span many JAR files, it's desirable to be able to find all the files with that name, not just the first one. Fortunately, ClassLoader
has a means to scan all of the individual elements with the getResources
method, which returns an enumeration of URLs from which streams can be individually obtained.
The solution is to call getResources
with an argument of plugin.xml
and then iterate through each of those JARs to instantiate the required contributions. By default, the contribution name is the name of the bundle, so it is necessary to parse out the Bundle-SymbolicName
header from the manifest. There are standard OSGi classes to do this, but the Manifest
class from the standard JDK libraries can be used.
Tip
The Bundle-SymbolicName
header can have additional metadata after the bundle name, separated by a semicolon (which delimits OSGi directives). The most common one is ;singleton=true
, but others can also exist such as ;mandatory
and ;fragment-attachment
.
Instead of adding a single feeds.xml
file into the registry, all JARs can have their bundles scanned by changing the following code in NonOsgi
:
// IContributor contributor = ContributorFactorySimple // .createContributor("com.packtpub.e4.advanced.feeds"); // reg.addContribution(Main.class.getResourceAsStream( // "/feeds.xml"), contributor, false, "/feeds.xml", null, null); Enumeration<URL> resources = getClass().getClassLoader(). getResources("plugin.xml"); while (resources.hasMoreElements()) { URL url = (URL) resources.nextElement(); String plugin_xml = url.toString(); String manifest_mf = plugin_xml.replace( "plugin.xml","META-INF/MANIFEST.MF"); Manifest manifest = new Manifest( new URL(manifest_mf).openStream()); String bsn = manifest.getMainAttributes(). getValue("Bundle-SymbolicName"); int semi = bsn.indexOf(';'); if (semi != -1) { bsn = bsn.substring(0, semi); } IContributor contributor = ContributorFactorySimple.createContributor(bsn); reg.addContribution(url.openStream(), contributor, persist, plugin_xml, null, null); }
After running this code, all JARs on the classpath will have extensions from their plugin.xml
files registered.
Unfortunately, there's a minor problem with any projects that are open in the Eclipse workbench. An open project in PDE doesn't put the plugin.xml
or MANIFEST.MF
files available on the project's classpath, which means that the classpath loading mechanism doesn't work. This is generally a problem with PDE and means that (for example) bundle.getEntry
and class.getResourceAsStream
in a PDE project differ from how they will run when exported and installed into a runtime as a JAR.
Fortunately, there is a way of fixing this: adding the plugin.xml
and META-INF/**
files as explicit sources in JDT, which copies them to the output directory. As a result, the getResource
methods work in both runtime and outside of runtime.
Right-click on the project and go to the Java Build Path. In the Source tab, add a folder by clicking on Add Folder and selecting the root of the project. Click on Edit and then add plugin.xml
and META-INF/**
as entries to be included. This is shown in the following screenshots:
Now when the application is run, plugin.xml
from the org.eclipse.equinox.registry
JAR and plugin.xml
from the com.packtpub.e4.advanced.feeds
JAR will both be registered and the default feed parsers will be able to return all of the values.