This section details the steps involved in creating a contribution that will
extend the classpathmodels
extension point allowing you to create
arbitrarily complex classloader hierarchies. This tutorial will not provide a
detailed introduction to eclipse plugins.
classpathhelper defines an extension point (classpathhelper.eclipse.ui.classpathmodels
)
that is intended to allow plugins to specify an object that can create
its own ClassLoaders.
The core API in classpath helper is the CGClassLoader
class
from the classpathhelper.api
plugin. CGClassLoader is a
standard classloader that also provides the detailed dependency and class
detail information required by the classpath helper views. CGClassLoader
can be configured to model most classloading requirements and can also be
used in a hierarchy of CGClassLoaders to simulate complex classloading
environments (such as a J2EE application server).
The actual CGClassLoader
is provided to the UI via
a IClassPathModel
instance. The IClassPathModel is
the object that is actually contributed to the UI extension point.
For the purposes of this tutorial we will define a very basic classloading example, that uses a standard classpath string as its input. To complete this example we need to supply the following.
plugin.xml
as an extension of the org.eclipse.ui.preferencePages
extension point.IClassPathModel
that will supply
instances of CGClassLoader
.plugin.xml
as an extension of the classpathhelper.eclipse.ui.classpathmodels
extension point.classpathhelper.eclipse.basicmodel
plugin.
Creating preferences pages in eclipse is well documented. For our purposes we need to create a page that can accept the classpath string we will be using as our model. Using preferences has the advantage that we can register listeners that can respond to changes to the preference value.
First we define a shared constants interface to provide a common reference for constants names. (Comments are excluded for brevity but exist in source files).
package classpathhelper.eclipse.basicmodel; public class PreferencesConstants { public final static String BASIC_CLASSPATH_LABEL_KEY = "classpath.label"; public final static String BASIC_CLASSPATH_PREF = "BasicClasspath"; private static final String BUNDLE_NAME = "classpathhelper.eclipse.basicmodel.messages"; private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle .getBundle(BUNDLE_NAME); public static String getString(String key) { try { return RESOURCE_BUNDLE.getString(key); } catch (MissingResourceException e) { return '!' + key + '!'; } } }The corresponding messages.properties would look as follows:
classpath.label=Classpath
When creating a preference it is a good practice to provide
default values. This is done by overriding the initializeDefaultPluginPreferences
method within your plugin class.
protected void initializeDefaultPluginPreferences() { plugin.getPluginPreferences().setDefault(PreferencesConstants.BASIC_CLASSPATH_PREF, ""); }
Second we define the preference page. As is standard with eclipse preferences page. We extend the PreferencePage base class and implement IWorkbenchPreferencePage. We also implement our PreferencesConstants interface to obtain easy references to the constants. (Comments removed for brevity but exist in source files).
package classpathhelper.eclipse.basicmodel; import org.eclipse.jface.preference.FieldEditor; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.preference.StringFieldEditor; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; public class PreferencesPage extends PreferencePage implements IWorkbenchPreferencePage {
Next we define the string field editor that will handle the classpath string, as well as hooking this classes preferences store to the preference store for this plugin.
private StringFieldEditor classpath; public PreferencesPage() { setPreferenceStore(BMPlugin.getDefault().getPreferenceStore()); }
The createContents method is used to layout the controls. In our case this is just the single text field.
protected Control createContents(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); composite.setLayout(new GridLayout(1, true)); GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true); composite.setLayoutData(gd); classpath = new StringFieldEditor(PreferencesConstants.BASIC_CLASSPATH_PREF, PreferencesConstants.getString(PreferencesConstants.BASIC_CLASSPATH_LABEL_KEY), composite); classpath.setPreferenceStore(getPreferenceStore()); classpath.setPage(this); classpath.load(); return parent; }
The remaining methods init
and performOk
a straightforward as we just want to store the new value. The PreferenceStore
automatically handles firing a preferences change event.
public void init(IWorkbench workbench) { } public boolean performOk() { classpath.store(); return true; }
As is standard in eclipse. To introduce the preference page we just
coded we need to define the extension in our plugin.xml
.
Note the category, which will place our preference page underneith
the classpath helper preferences entry.
<extension point="org.eclipse.ui.preferencePages"> <page name="Simple" category="classpathhelper.eclipse.ui.preferences.PreferencesPage" class="classpathhelper.eclipse.basicmodel.PreferencesPage" id="classpathhelper.eclipse.basicmodel.PreferencesPage"> </page> </extension>
After defining the preference page in our plugin, we should have a page similar to the follow: Note how this page is accessible off of the Classpath Helper area.
IClassPathModel
IClassPathModel
is really the core interface that makes
the classloader available to views. The basic interface only contains
a few methods. (Comments removed for brevity).
package classpathhelper.eclipse.ui; public interface IClassPathModel { public void startClassPathListening(); public void stopClassPathListening(); public void reset(); public void addClassPathChangeListener(IClassPathChangeListener changeListener); public boolean removeClassPathChangeListener(IClassPathChangeListener changeListener); }
The start and stop methods are lifecycle methods which allow the model to create resources and release them when the model is nolonger active (such as when no views are open).
The remaining two methods are simply standard listener related methods that the views use to attach to the model.
The classpathhelper.eclipse.ui
plugin also provides a
helper base class AbstractClassPathModel
which implements
the listener registration methods. The relavent methods within
AbstractClassPathModel are as follows:
package classpathhelper.eclipse.ui; import classpathhelper.CGClassLoader; public abstract class AbstractClassPathModel implements IClassPathModel { protected synchronized void fireClassPathChangeEvent(CGClassLoader classLoader) { ... } protected abstract CGClassLoader getCurrentClassLoader();
The method fireClassPathModelChangeEvent
is intended to be
used by subclasses to notify their listeners of a new ClassLoader.
The abstract method getCurrentClassLoader
is used to retrieve
the current classloader so that newly registered listeners can receive
an update event (without needing to fire a new event to all listeners). The
current classloader should be the same object that was last fired as part
of a classpath change event.
Our implementation of IClassPathModel extends AbstractClassPathModel and also implements two listener interfaces.
package classpathhelper.eclipse.basicmodel; import java.io.File; import java.util.StringTokenizer; import classpathhelper.CGClassLoader; import classpathhelper.eclipse.ui.AbstractClassPathModel; import classpathhelper.filechangelistener.FileChangeMonitor; import classpathhelper.filechangelistener.FileChangedEvent; import classpathhelper.filechangelistener.IFileChangeListener; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; public class BasicClassPathModel extends AbstractClassPathModel implements IFileChangeListener, IPropertyChangeListener {
The two listener interfaces represent callbacks that we will use to
fire a new classloader event. IPropertyChangeListener
is a
standard eclipse callback for any changes related to preferences. Since
we earlier created a preference page we can use this interface to detect
a change that would require us to create a new ClassLoader.
IFileChangeListener
is a callback from a helper utilitly
defined in classpathhelper.api
. The underlying utility will
inform us of any file change events that could make our classloader obsolete.
In this case we re-create the classloader with the same path and fire
it as a new classloader. This allows us to respond to environment changes
such as a directory getting repopulated due to compilation or a jar being
removed or updated.
The bulk of our classes intelligence is handled in the single method
updateClassLoader
we will tie in calls to this method later.
But for now we can see how we create a file monitor to watch all the files
on the classpath as well as the basic parsing logic to tokenize the
classpath string (formatted in standard classpath format). This method
also fires the new classloader event.
private CGClassLoader currentClassLoader = null; private FileChangeMonitor fileMonitor = null; protected void updateClassLoader(String classpath) { if ( fileMonitor != null ) fileMonitor.cancel(); // file monitor looks for changes to any files // on the classpath and will fire an event. fileMonitor = new FileChangeMonitor(30000, 2); fileMonitor.addFileChangeListener(this); StringTokenizer tok = new StringTokenizer(classpath, File.pathSeparator); while ( tok.hasMoreTokens() ) { String element = tok.nextToken(); File fileElem = new File(element); fileMonitor.addSource(element, fileElem.exists() && fileElem.isDirectory()); } fileMonitor.startMonitor(); currentClassLoader = new CGClassLoader(classpath); fireClassPathChangeEvent( currentClassLoader ); }
With this method in place the remainder of the class can be expressed in terms of simple implementations. The start method simply attaches this class as a property change listener, while calling or previously defined update method to create the initial classloader.
The stop method detaches this class as a preferences listener and clears/cancels the related state.
public void startClassPathListening() { BMPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this); updateClassLoader(BMPlugin.getClassPathPreference()); } public void stopClassPathListening() { BMPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this); currentClassLoader = null; if ( fileMonitor != null ) { fileMonitor.cancel(); fileMonitor = null; } }
Our two callback methods from the two listener interfaces are straight forward as well. The fileChanged callback simply stops and starts this listener, which releases any prior classloader and then simply starts monitoring a new classloader.
The propertyChange callback reacts to the new classpath value by calling
updateClassLoader
which will fire the new class loader event.
public void fileChanged(FileChangeMonitor monitor, FileChangedEvent[] evts) { stopClassPathListening(); // starting the classpath listening will resend a classpath event. startClassPathListening(); } public void propertyChange(PropertyChangeEvent evt) { Object newValue = evt.getNewValue(); if ( newValue != null && newValue instanceof String ) updateClassLoader((String) newValue); }
As is standard with extensions in eclipse we must also indicate
in our plugin.xml
how we are contributing. The label
and description are used in the UI to indicate which ClassPathModel
is currently being used.
<extension point="classpathhelper.eclipse.ui.classpathmodels"> <model class="classpathhelper.eclipse.basicmodel.BasicClassPathModel" id="classpathhelper.eclipse.basicmodel.classpathmodel" name="classpathhelper.eclipse.basicmodel.classpathmodel" label="%model.name" description="%model.description" /> </extension>
Where the tokens model.name and model.description are supplied in a properties file.
model.name=Simple model.description=Classpath provided as string
After coding the BasicClassPathModel and contributing it, ClassPath Helper will automatically show it in the list of available classpath models.
Eclipse provides good documentation on contributing help. This section will merely provide a quick overview of the steps for contributing help.
To start contributing help, you define your help contents, typically in a
standard HTML file. In the Basic Classpath model, the help HTML is defined in
the help/using.html
file.
Defining a TOC
To contribute help you must next define a Table-of-Contents file that defines the structure of your help.
<toc label="classpathhelper.eclipse.tomcat.listener.ui JavaDocs" link_to="../classpathhelper.eclipse.ui.help/toc.xml#using"> <topic label="Using the Simple ClasspathHelper model" href="help/using.html" /> </toc>
Notice the reference to the #using
anchor, this allows the
help to get properly inserted within the Classpath Helper contents.
To contribute help you must contribute to the org.eclipse.help.toc
extension point.
<extension point="org.eclipse.help.toc"> <toc file="toc.xml" /> </extension>