Dealing with jInfer and NetBeans Platform
Target audience: jInfer developers. Anyone who needs to interact with jInfer or NetBeans while hacking our framework.
This tutorial deals with a few important but pretty specific parts of interface between your module (or any logic you implement) and either jInfer or NetBeans Platform. Not nearly everything regarding interaction with NBP is mentioned here, please refer to the relevant FAQ.
This tutorial assumes that you are a seasoned Java developer. Having
experience with programming in some kind of framework (NetBeans Platform
above all) will help you a lot.
Make sure you have read the article on
architecture, data structures and inference process to understand what you
will be implementing. Having read the documentation for
remaining modules will help you too.
Also, before starting this tutorial, make sure you can
build jInfer from sources.
Overview
- Module visibility
- Error handling
- Interruptions
- Dialogs
- Configuration - options, preferences
- Rule display
- Console output, logging
- RunningProject class
- Module selection
Module visibility
Each NBP module allows to set some packages as public, which means their content will be available to other modules. That means, if module A declares a dependency on module B, it will be able to use only classes from those packages that were set as public in module B.
Important notice: Public packages do not affect Lookup mechanism, i.e. class annotated with@ServiceProvider
does not need to be in a public package in order to be looked up.
There are two ways to set packages visible, first is to set it manually in
module's project.xml
file located in
ModulePath/nbproject/ folder.
// This is a portion of example project.xml file. <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://www.netbeans.org/ns/project/1"> <type>org.netbeans.modules.apisupport.project</type> <configuration> <data xmlns="http://www.netbeans.org/ns/nb-module-project/3"> .... <public-packages> <package>some.example.package.name</package> </public-packages> </data> </configuration> </project>
Another way to set public packages is through GUI:
- Right-click the module, select Properties.
- In API Versioning, there is a Public Packages section.
- Check all packages that should be public.
Error handling
Each run of inference is encapsulated in a try-catch
block,
so it is safe to throw any exception in the inference process.
Each thrown exception will be caught, get logged, presented to the user and
inference will stop. However if module uses threads which throw exceptions,
it is caught by NBP, instead of our code. Because of this its reasonable to
catch this exception in your module and re-throw it in the right thread.
Interruptions
In jInfer user can stop the inference at any moment of its run. For this reason, modules have to check in every time-consuming place (such as long loops) whether this is the case. Example below shows how to check the interruption from user and what to do to correctly stop the inference.
Example:for (forever) { if (Thread.interrupted()) { throw new InterruptedException(); } doStuff(); }
Dialogs
Creation of open/save dialogs or message windows is done using standard NBP API.
For dialogs you can use the standard Java JFileChooser
,
but NBP FileChooserBuilder
is more convenient. This class creates dialog which remembers last-used
directory according to a key passed into its constructor.
// The default dir to use if no value is stored File home = new File(System.getProperty("user.home")); // Now build a file chooser and invoke the dialog in one line of code File toAdd = new FileChooserBuilder(CallingClass.class) .setTitle("Some dialog title") .setDefaultWorkingDirectory(home).showOpenDialog(); // Result will be null if the user clicked cancel or closed the dialog w/o OK if (toAdd != null) { //do something }
For message windows the DialogDisplayer
NBP API
class is used. To determine which type of message will be shown
(Confirmation dialog, message dialog...),
NotifyDescriptor
is used. Following is a small example, which creates standard message
notification. For more information please look at
NBP Dialogs API FAQ.
// Creates standard information message with text "Hello world" NotifyDescriptor nd = new NotifyDescriptor .Message("Hello world", NotifyDescriptor.INFORMATION_MESSAGE); DialogDisplayer.getDefault().notify(nd);
Configuration - options, preferences
Option panels located in Tools > Options menu are a standard part of NBP API. We created a jInfer option sub-category to place panels with configuration valid across all jInfer projects. Description of the few step to create your own option panel follows. For more information, please visit NetBeans Options tutorial.
- Create options window:
- Right click Source Packages in your module, select New > Other.
- Under Categories, select Module Development. Under File Types, select Options Panel. Click Next.
- Keep checked Create Secondary Panel and choose jInfer as the Primary Panel. Fill in remaining fields. Click Next.
- Fill in Class Name Prefix and Package and click Finish.
- Design newly created ClassNamePrefixPanel.java.
- Implement
store()
andload()
methods according to comments inside.
Both store()
and load()
methods use
NbPreferences
class. Example below shows how to store and load Preferences.
public void store() { // Saves the state of checkBox into Preferences for ExampleClass // class in property with name "PropertyName" NbPreferences.forModule(ExampleClass.class) .putBoolean("PropertyName", someCheckBox.isSelected()); } public void load() { // Get from ExampleClass class preferences the property with name // "PropertyName". Second parameter of method getBoolean is used // as default if no property with defined name is saved in preferences. someCheckBox.setSelected(NbPreferences.forModule(ExampleClass.class) .getBoolean("PropertyName", true)); }
Second type of preferences used in jInfer is Project properties. Each jInfer
project has its properties panel accessible in its content menu and
these properties are applied for each project separately. To implement this
kind of preferences, it is necessary to implement
PropertiesPanelProvider
interface and extend the
AbstractPropertiesPanel
class. Each Project properties window has category tree, where each category
represents a separate panel with properties. This category is declared in the
provider interface, properties panel itself is defined in class extending
AbstractPropertiesPanel
.
PropertiesPanelProvider
As was mentioned above, PropertiesPanelProvider
defines
category in Project properties windows. In the example below, we'll try to
explain how to create a simple provider and what each method is responsible for.
public class ExampleProvider implemenets PropertiesPanelProvider { // Programmatic name of this provider. public String getName() { return "ExampleProvider"; } // User-friendly name - this name will be displayed in category tree. public String getDisplayName() { return "Example Category"; } // Priority of category. Higher the number, higher // will the category be in the tree. If two categories have // same priorities, they are sorted according // to their names. public int getPriority() { return 0; } // Returns properties panel defined for this category. public AbstractPropertiesPanel getPanel(final Properties properties) { return new ExamplePropertiesPanel(properties); } // Defines parent category of this category. If null is returned, this is top // level category. In other cases id (programmatic name) of parent category // must be returned. public String getParent() { return null; } // Optional. For each category, list of virtual categories can be defined. // Instead of properties panel, VirtualCategoryPanel only informs of type and // installed number of modules of a certain type. public ListgetSubCategories() { return null; } }
AbstractPropertiesPanel
This class represents the visual component of properties category. You can design it in whatever way you like. For proper functionality, a few steps must be followed.
-
Call
super(Properties)
in constructor: this causesProperties
instance to be saved intoproperties
protected field and you can use it inload()
andstore()
methods. -
Implement
load()
method: In this method values previously saved intoProperties
instance provided to this panel are loaded into components in this panel. -
Implement
store()
method: Here the values gathered from components in this panel are saved intoProperties
.
// This is only a part of the actual class... public class ExamplePanel extends AbstractPropertiesPanel { public ExamplePanel(final Properties properties) { super(properties); } public abstract void store() { properties.setProperty("exampleKey", someTextField.getText()); } public abstract void load() { String propValue = properties.getProperty("exampleKey", "default value"); someTextField.setText(propValue); } }
Rule display
Rule displayer is a component used for visualization of rules in any step of inference process. If rule displayers bundled with jInfer are insufficient, you can implement a rule displayer on your own. You just need to follow these few easy steps.
-
Implement
cz.cuni.mff.ksi.jinfer.base.interfaces.RuleDisplayer
-
RuleDisplayer
interface extendsNamedModule
, so you need to override its methods too. -
Implement
createDisplayer(name, rules)
method which creates displayer window responsible for displaying rules.
-
-
Annotate this class with
@ServiceProvider(service = RuleDisplayer.class)
.
Important notice: never forget to clone the rules you want
to display. Rule displayer usually runs in its own thread, and accessing the
rules that are being simplified at the same time might lead to weird results.
You can use CloneHelper
to clone the rules.
@ServiceProvider(service = RuleDisplayer.class) public final class ExampleRuleDisplayer implements RuleDisplayer { @Override public void createDisplayer(String panelName, Listrules) { // Creates standard NBP TopComponent in which are rules displayed. ExampleRDTopComp topComponent = ExampleRDTopComp.findInstance(); if (!topComponent.isOpened()) { topComponent.open(); } topComponent.createRuleDisplayer(panelName, rules); } @Override public String getName() { return "ExampleDisplayer"; } @Override public String getDisplayName() { return "Example rule displayer"; } @Override public String getModuleDescription() { return getDisplayName(); } }
Example of invocation with cloning:
RuleDisplayerHelper.showRulesAsync("Panel name", new CloneHelper().cloneGrammar(grammar), true);
Console output, logging
To print to console output, NBP provides
IOProvider
and InputOutput
classes. IOProvider
's method getIO(String, boolean)
returns InputOutput
instance, which can be used to obtain
Reader
/OutputWriter
to read/write into output window.
// Creates new output window, where first parameter is a name and second // is flag determining whether a new window should be created even if there is // already a window with the same name. InputOutput ioResult = IOProvider.getDefault().getIO("Example", true); ioResult.getOut().println("Hello world"); // After writing everything into output window, close the output. ioResult.getOut().close();
Logging is done in jInfer using Log4j tool. If you are not familiar with Log4j, example below shows simple usage. All logged messages are saved in a standard log file placed in UserHome/.jinfer/ folder and printed into "jInfer" output window in the application. For each of these two places a log level can be set in jInfer options.
Example:// Creates Logger instance for this particular class private static final Logger LOG = Logger.getLogger(Example.class); ... // Log a message with info level LOG.info("Some message with info log level"); // Log an error message LOG.error("OMG, error occurred!");
RunningProject
class
This class provides access to information regarding currently running inference.
It is a singleton, corresponding with the fact that at a given time, at most
one inference (project) can be running in the whole NB. Consequently, all relevant methods
are synchronized.
NB project corresponding to the running inference can be retrieved by
calling getActiveProject()
. Its properties can be retrieved by
calling getActiveProjectProps()
. This
method will work also if there is no running project - it just retrieves an
empty instance of Preferences
, which is useful for example in JUnit
tests.
Properties properties = RunningProject.getActiveProjectProps( DTDExportPropertiesPanel.NAME);
Aside from keeping information about the currently running project, this class
keeps information about the capabilities of the following module in the inference
chain. This can be retrieved by calling the getNextModuleCaps()
method.
Again, if this information is not available, empty Capabilities
instance will be returned.
if (!RunningProject.getNextModuleCaps().getCapabilities().contains( Capabilities.CAN_HANDLE_COMPLEX_REGEXPS)) { ... expand rules ... }
Module selection
This part of the tutorial will show the full process of implementing a choice between two submodules in an inference module, for example simplifier. Similar principles apply one level higher, when dealing with inference modules themselves.
Interface
First of all, we need an interface encapsulating the new submodule. It has to
extend NamedModule
interface.
public interface Foo extends NamedModule { String bar(); }
Implementations
Second, we need some implementations of this interface. At least two, otherwise
it's boring.
They will have to implement all required methods and register themselves as
service providers for Foo
.
@ServiceProvider(service = Foo.class) public class Impl1 implements Foo { // NamedModule stuff @Override public String getName() { return "Impl1"; // unix-style name } @Override public String getDisplayName() { return "Foo implementation number 1"; // user-friendly name } @Override public String getModuleDescription() { return getDisplayName(); // irrelevant now } // logic from Foo @Override public String bar() { return "Hello world from Impl1!"; } }
Properties panel
Now we have to create a properties panel. Refer to an
earlier part of this document for general
reference. Here we assume a combo box already present on the properties panel,
with the correct renderer set (ProjectPropsComboRenderer
).
We need to do 2 things: while loading the properties panel, fill it with
names of all implementations and select the currently chosen one (or default).
While saving, we have to get the name of
the selected implementation and save it.
@Override public final void load() { combo.setModel(new DefaultComboBoxModel( ModuleSelectionHelper.lookupImpls(Foo.class).toArray())); combo.setSelectedItem(ModuleSelectionHelper.lookupImpl(Foo.class, properties.getProperty( "selected.foo.implementation", // property name "Impl1"))); // default property value // - same as Impl1.getName()! }
Example of panel save code:
@Override public void store() { properties.setProperty("selected.foo.implementation", ((NamedModule) combo.getSelectedItem()).getName()); }
Example of combo box renderer setting:
combo.setRenderer(new ProjectPropsComboRenderer(combo.getRenderer()));
Invocation
Finally, there will be a place (in our example simplifier) where we actually
want to load the properties of the running project and lookup the selected
implementation of Foo
.
Properties p = RunningProject. getActiveProjectProps("ExampleProvider"); // the name provided in // properties panel provider Foo foo = ModuleSelectionHelper.lookupImpl(Foo.class, p.getProperty("selected.foo.implementation", "Impl1")); println(foo.bar());
A few things should be noted:
- Module names (
getName()
) have to be unique. - It is a good practice to store property names and default values as constants, for example in super module - do a reference search in our code to find out how we do it.
getUserDescription()
should work in modules that have submodules as follows: it should somehow return the name of the module and names of all selected submodules to indicate inner workings of this module. For example My Simplifier (Clustering Logic A, Regexper #3).