Instant JChem (IJC) is an out-of-the-box tool that allows scientists to create, manage and analyse chemical structures and their data. It serves as a desktop application that provides a convenient and straight-forward approach to the analysis of chemical and biological data. IJC is a database-centric environment that supports query and sorting functionality, and handles large volumes of data (hundreds of thousands of structures).
IJC is extendable. As a customer or 3rd party developer, you can extend IJC and build higher level features on top of the core functionality. IJC provides much of the basic framework needed, as well as the hooks for extending it with new data types. No longer will the chemical spreadsheet need to be re-invented yet again. Instead, you are able to use the form builder, possibly with your own custom view widgets, without needing to worry about infrastructural details such as persistence, security, and import/export. Instead, you can extend IJC by adding any data types that might be specific to your needs and then build an application on top of these basic data types and tools.
This tutorial gets you started extending IJC. It introduces you to the development tools that are available to you when extending IJC. It walks you through the usage of some of the basic APIs. In particular, you will learn the following:
The following sections are covered in this tutorial:
Contents:
By the end of this tutorial, you will understand the basic process of creating new extensions for IJC and you will also have been introduced to various resources that you can use to further develop your knowledge and skill in extending IJC.
Though you can use any IDE to extend IJC, this tutorial will focus on NetBeans IDE, since this IDE is tailored towards making the modules that extend IJC. At the end of this tutorial, you will have a new IJC module with a source structure that looks like this in NetBeans IDE:
Make sure the following are installed:
Before we begin creating our module, let's look at the sample that we will build in this tutorial. Once we have tried it out, we will know for ourselves what the result of this tutorial will be.
View the "Atom Count" field added to the JChem table, as shown below:
Note: In the bottom right corner of the screenshot above, you can see a progress bar, with the text "Filling data". In this tutorial, you will learn, among many other things, how to create a progress bar to enable asynchronous tasks to run in the background, so that the user interface remains enabled while the field is being added to the table.
The module that we build in this tutorial will, like your own modules, be deployed to IJC. Therefore, we will register the binary version of IJC and deploy our module to it.
The NetBeans Platform Manager appears.
If you click Next, you can assign a name to your platform.
The list of platforms now includes IJC, as shown below:
The Modules tab shows a long list of modules that are available to you now. The modules include those provided by the NetBeans Platform, as well as those provided by IJC.
Whenever we want to deploy a new module as part of the IJC distribution, we simply need to set a property on the module, as we will do in the next section. Then when we run a module, the IJC installation will start up and our module will be installed within it. If it is already installed, it will be uninstalled first, before the new version is installed. This will prove very useful for purposes of trying out our module during development.
First, you will use NetBeans IDE's New Module Project wizard to create a source structure for your module. The source structure will consist of a Java package, a layer.xml file for registering the elements of your user interface, a Bundle.properties file for localizing the module's strings, and project metadata, such as the project.xml file, where all the module's dependencies will be registered. All these items, which are mandatory for all modules, will be created by the New Module Project wizard.
The New Project wizard appears.
The New Module wizard appears.
Note: Make sure to select Instant JChem from the NetBeans Platform drop-down list, as shown above. Doing so will ensure that later, when you run the module, it will be deployed to your installation of IJC, rather than to the NetBeans Platform.
Click Next.
Click Finish.
The source structure of your module is generated and you can see the outline in the Projects window.
Our next step is to specify which APIs we want to use. Each API is provided by a module. Some of the APIs belong to the NetBeans API, while others belong to the Instant JChem API. Because our module will be deployed to the IJC binary, which itself makes use of all the NetBeans APIs and IJC APIs that we need, we can set dependencies on the modules that IJC makes available.
The Project Properties dialog box appears.
| Module | Purpose | Classes Relevant to Our Module |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
You should now see the following:
<dependency>
<code-name-base>chemaxon.jchem</code-name-base>
<build-prerequisite/>
<compile-dependency/>
<run-dependency>
<release-version>1</release-version>
<specification-version>3.2.11</specification-version>
</run-dependency>
</dependency>We have now declared our modules and are ready to use the APIs that they provide.
In this section, we use the New Action wizard to create a menu item and toolbar button that will be context-aware, which means that they will only be enabled when needed. In this tutorial's scenario, the menu item and toolbar button will only be enabled when a table, also known as an 'entity', is selected in IJC. Once we have completed the wizard, we will have a skeleton "action", which the user will be able to invoke either from the menu item or from the toolbar button, when enabled, and which we will fill out with additional functionality needed for this module.
Click Next.
Click Next.
You should now see the following:
The wizard creates a new action class and uses the layer.xml file to register it as a menu item and toolbar button. In addition, the icons are copied into the module. You should now see the following:
Note: The action class, and the folders leading up to it, each have an error badge. Open the class and notice that the line that refers to the EntityCookie is underlined in red. In this case, we need to add an import statement for the EntityCookie. Right-click in the editor and choose Fix Imports (Ctrl-Shift-I). The IDE adds the import statement. The red underline disappears:
We can immediately try out our module and see if it correctly identifies tables, which we can determine by looking at whether the menu item and toolbar button become enabled when we select a table.
Above, the table is shown in the editor area and the tab is selected. The screenshot shown prior to this one indicates that the tab is not selected and therefore the toolbar button is not enabled. However, even though the toolbar button is enabled, nothing happens when you click it because you have not defined the content of the performAction() method yet, which we will do later in this tutorial.
Before continuing with the coding of our module, let's examine the artifacts that the New Action wizard generated for us.
The generated action class looks as follows. Note that comments that briefly explain each method are found highlighted inline below.
package com.im.ijc.addfield;
import com.im.ijc.core.api.cookie.EntityCookie;
import org.openide.nodes.Node;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.actions.CookieAction;
public final class AddFieldAction extends CookieAction {
//Specifies what happens when the toolbar button
//or the menu item is clicked:
protected void performAction(Node[] activatedNodes) {
EntityCookie entityCookie = activatedNodes[0].getLookup().lookup(EntityCookie.class);
// TODO use entityCookie
}
//Specifies that our action will only be enabled
//when one table is in focus:
protected int mode() {
return CookieAction.MODE_EXACTLY_ONE;
}
//Specifies the name of the menu item:
public String getName() {
return NbBundle.getMessage(AddFieldAction.class, "CTL_AddFieldAction");
}
//Specifies that our action will only be enabled
//when an 'entity', i.e., a table,
//receives focus in IJC:
protected Class[] cookieClasses() {
return new Class[] {
EntityCookie.class
};
}
//Specifies the location of the icon
//shown in the toolbar button:
protected String iconResource() {
return "com/im/ijc/addfield/icon-16.png";
}
//Specifies the help ID for the toolbar button
//and for the action, for when F1 is pressed:
public HelpCtx getHelpCtx() {
return HelpCtx.DEFAULT_HELP;
}
//Specifies boilerplate code
//needed for every CookieAction class:
protected boolean asynchronous() {
return false;
}
}
The layer.xml file, providing the user interface registration for our module, has the following content. To help you read the main elements more easily, they have been highlighted in bold below. Notice that there is a top-level element called "filesystem", containing the sub-elements "Actions", "Menu", and "Toolbars". The "Actions" element determines where the action will be registered in the Options window, the "Menu" element determines where the action's menu item will be found, and the "Toolbars" element specifies the toolbar where the action's toolbar button will be located.
<filesystem>
<folder name="Actions">
<folder name="Edit">
<file name="com-im-ijc-addfield-AddFieldAction.instance"/>
</folder>
</folder>
<folder name="Menu">
<folder name="Edit">
<file name="com-im-ijc-addfield-AddFieldAction.shadow">
<attr name="originalFile" stringvalue="Actions/Edit/com-im-ijc-addfield-AddFieldAction.instance"/>
<attr name="position" intvalue="100"/>
</file>
</folder>
</folder>
<folder name="Toolbars">
<folder name="Edit">
<file name="com-im-ijc-addfield-AddFieldAction.shadow">
<attr name="originalFile" stringvalue="Actions/Edit/com-im-ijc-addfield-AddFieldAction.instance"/>
<attr name="position" intvalue="0"/>
</file>
</folder>
</folder>
</filesystem>
Though our action is only enabled when an entity is in focus, the concept 'entity' in IJC covers both JChem tables and standard tables. As a result, the toolbar button and menu item will also be enabled when a standard entity is in focus, as shown below:
Since we only want to deal with JChem tables, the scope of our action is currently too broad and we need to filter out the standard entities from the scope of our action.
private static DFEntity findEntity(Node[] activatedNodes) { EntityCookie entityCookie = (EntityCookie) activatedNodes[0].getLookup().lookup(EntityCookie.class); if (entityCookie != null) { return entityCookie.getItem(); } return null; }
protected boolean enable(Node[] activatedNodes) {
if (super.enable(activatedNodes)) {
DFEntity entity = findEntity(activatedNodes);
return (DIFUtilities.findCapability(entity, JChemBaseEntityCapability.class) != null);
}
return false;
}
Currently, when the toolbar button or the menu item are clicked, nothing happens. In this section, we fill out the performAction() method so that a dialog is shown when the action is invoked.
protected void performAction(Node[] activatedNodes) {
DFEntity entity = findEntity(activatedNodes);
String entityName = entity.getName();
DialogDescriptor.Confirmation dd = new DialogDescriptor.Confirmation(
NbBundle.getMessage(AddFieldAction.class,"Add_Confirm",entityName),
NbBundle.getMessage(AddFieldAction.class, "TITLE_Add_Confirm"),
DialogDescriptor.YES_NO_OPTION,
DialogDescriptor.QUESTION_MESSAGE);
Object retCode = DialogDisplayer.getDefault().notify(dd);
if (retCode == DialogDescriptor.YES_OPTION) {
JOptionPane.showMessageDialog(null, "Hurray!");
}
}
Add_Confirm=Warning: This action is an example of usage of Instant JChem APIs. \
It is enabled when some JChem table ({0} in this case) is found in current selection context. \
It takes the selected JChem table entity, creates a new field in it and for each row it fills \
atom count for the molecule in this row. Do you want to proceed?
TITLE_Add_Confirm=Please confirm
In this section, we create a field by using asynchronous tasks running in the background.
createNewField(entity);
private static Class REQUIRED_FIELD_CAPABILITY = DFFieldIntegerCapability.class;
private static Class[] UNDESIRABLE_FIELD_CAPABILITIES = { DFFieldChemicalTermsCapability.class };
private static String NEW_FIELD_NAME = NbBundle.getMessage(AddFieldAction.class, "FieldName");
private static DFField findStructureField(DFEntity entity) { for (DFField f: entity.getFields().getItems()) { DFFieldStructureCapability structCapability = (DFFieldStructureCapability) DIFUtilities.findCapability(f, DFFieldStructureCapability.class); if (structCapability != null) { return f; } } return null; }
private static void createNewField(DFEntity entity) { // To create a new field, we need to ask the entity for its fields container. // This container provides new types (DFNewType), which can be used for creating // new fields in the entity. Let's get all available new types first: List<DFNewType<DFField>> allNewTypes = entity.getFields().getNewTypes(); // We need a new type which is able to create an integer field, which means we need a DFField that has an integer-specific // capability (DFFieldIntegerCapability). We need a standard read/write database column, so we need to avoid // a new type which creates integer fields for chemical terms (DFFieldChemicalTermsCapability). // So let's use a utility method to filter new types: List<DFNewType<DFField>> appropritateNTs = DIFUtilities.findAllAppropriateNewTypes(allNewTypes, false, new Class[] { REQUIRED_FIELD_CAPABILITY }, UNDESIRABLE_FIELD_CAPABILITIES); // We can't do anything if the appropriate new type is not found: if (appropritateNTs.isEmpty()) { return; } // If there is more than one new type that creates an integer-specific // and non-chemical term field, let's use the first one: final DFNewType<DFField> nt = appropritateNTs.get(0); // We need to setup the options, which are parameters, for new field creation: DFNewTypeOptions unknownNewTypeOptions = nt.getOptions(); // We can use introspection to find out methods and properties that offer our options, but the simplest way // is to try to cast the given options to some expected well-known new type options. Some of them are predefined // by the DFNewTypeWellKnownOptions interface and serve as a bridge between the newtype provider and newtype users. // In this case we can expect NewDBField subinterface, so let's try it... if (unknownNewTypeOptions instanceof DFNewTypeWellKnownOptions.NewDBField) { DFNewTypeWellKnownOptions.NewDBField options = (DFNewTypeWellKnownOptions.NewDBField) unknownNewTypeOptions; // Setting safe name means that the name is validated and if the given name doesn't work, it is "standardized"; // for example, when the name is some DB key word or it is already used. This method call also generates // a default database column name. options.setNewDFItemNameSafe(NEW_FIELD_NAME); } String creatingFieldTaskName = NbBundle.getMessage(AddFieldAction.class, "MSG_ProgressNameCreatingField"); // The task can take some time, so let's execute it in the background, using the UIBackgroundRunnerRW utility class. // The method phase1InRequestProcessor is executed in the background, during this time the progress bar moves in the IJC main window // UIBackgroundRunnerRW also helps with locking before DDL change and unlocking afterwards. We also need to provide // a progress name and a boolean flag if the task is cancellable. UIBackgroundRunnerRW runner = new UIBackgroundRunnerRW(DIFUtilities.getLockable(entity), creatingFieldTaskName, false) { private DFField newField; public void phase1InRequestProcessor() { // Let's create the field: newField = nt.create(getEnvironment()).iterator().next(); } public void phase2InAWT() { // Phase2 is executed in AWT and it just invokes the method for filling the new field with data. fillData(newField); } }; // The task is prepared, we need to start it: runner.start(); }
private static void fillData(final DFField newField) { // Get the entity for the given field (parent object in DDL is always accessible): DFEntity entity = newField.getEntity(); // Find field which contains molecules. We can recognize it using the appropriate capability: final DFField structureField = findStructureField(entity); // Get the id, which is a unique identifier, of any DFItem, in this case the structure field. // The field id can be used when obtaining data: final String structureFieldId = structureField.getId(); // The data provider is the basic class for obtaining data from an entity (database table). // It can be found from DFSchema using a few method calls or directly using this utility method: final DFEntityDataProvider edp = DIFUtilities.findEntityDataProvider(entity); String fillingDataTaskName = NbBundle.getMessage(AddFieldAction.class, "MSG_ProgressNameFillingData"); // Filling data should be again done in the background. Currently we run in the AWT thread // (as this method was called from phase2InAWT in createNewField) // Now we use a different lockable - each EDP has its own lockable (opposite to DDL operations which shares the same // lockable object for whole DFSchema) // Filling the data is now cancellable. UIBackgroundRunnerRW runner = new UIBackgroundRunnerRW(DIFUtilities.getLockable(edp), fillingDataTaskName, true) { public void phase1InRequestProcessor() { // Through current DFFeedback we can present the current progress to user: DFFeedback feedback = getEnvironment().getFeedback(); feedback.addMessage(DFFeedback.Type.PROGRESS_MESSAGE,NbBundle.getMessage(AddFieldAction.class, "MSG_ProgressGrabbingData"), null); // Get values of primary key for all data in the table first: List ids = edp.queryForIds(DFTermExpression.ALL_DATA, SortDirective.EMPTY, getEnvironment()); Map<String,Object> valuesToUpdate = new HashMap<String,Object>(); int total = ids.size(); // Switch progress to determinate mode and start counting: feedback.switchToDeterminate(total); int counter = 0; // Iterate for all row primary key values: for (Object rowId: ids) { if (getEnvironment().isCancelled()) { throw new CancelException(NbBundle.getMessage(AddFieldAction.class, "MSG_TaskCancelled", counter, total)); } // Get the current row data... if we need only a single row data the returned Map will have only one entry... Map<Object, Map<String, Object>> allData = edp.getData(Collections.singletonList(rowId), getEnvironment()); //...the entry for the given rowId, which is again a Map: keys are ids of fields in the entity and values are the // real data in each cell in the database: Object structureValue = allData.get(rowId).get(structureFieldId); int atomCount = 0; // If the molecule object is of known type... if (structureValue instanceof MarvinStructure) { // ... we can count the atoms... Molecule mol = ((MarvinStructure) structureValue).getNative(); atomCount = mol.getAtomCount(); atomCount += mol.getImplicitHcount(); atomCount += mol.getExplicitHcount(); } // ... and update the value in the new field: valuesToUpdate.put(newField.getId(), atomCount); edp.update(rowId, valuesToUpdate, getEnvironment()); feedback.progress(counter); feedback.addMessage(DFFeedback.Type.PROGRESS_MESSAGE,NbBundle.getMessage(AddFieldAction.class, "MSG_ProgressFillingDataDetail", counter, total), null); counter++; } feedback.addMessage(DFFeedback.Type.STATUS_BAR_MESSAGE,NbBundle.getMessage(AddFieldAction.class, "StatusBar_TaskDone"), null); } public boolean phase1Cancelled(CancelException exc) { DialogDisplayer.getDefault().notify(new DialogDescriptor.Message(exc.getMessage())); return false; } public void phase2InAWT() { DialogDisplayer.getDefault().notify(new DialogDescriptor.Message(NbBundle.getMessage(AddFieldAction.class, "MSG_TaskDone"))); } }; runner.start(); }
FieldName=Atom count
MSG_ProgressNameCreatingField=Creating field
MSG_ProgressNameFillingData=Filling data
MSG_ProgressGrabbingData=Grabbing primary keys of all rows.
MSG_ProgressFillingDataDetail=Computed {0} out of {1} rows.
StatusBar_TaskDone=Add atom count action finished.
MSG_TaskCancelled=Add atom count action cancelled after {0} out of {1} rows. The rest of rows is empty.
MSG_TaskDone=Atom count field was added.
Our module is complete. Let's try it out again before we think about distributing it.
View the "Atom Count" field being added to the JChem table, and note that it gets filled, as shown below:
Note: In the bottom right corner of the screenshot above, notice the progress bar that enables asynchronous tasks to run in the background, so that the user interface remains enabled while the field is being added to the table.
Potentially, you might want to extend the functionality provided by this module. You can do so by adding further code to the module that you've created so far. However, you can also make 3rd party JAR files available to your module. Once you have done so, your module can use classes and methods provided by the 3rd party library.
Adding a 3rd party library entails that you need to create a new module that wraps the 3rd party library. Then you need to set a dependency on that wrapper module, in your own functionality module. By putting the JAR in a separate module, you ensure that when a new version of the JAR is released, you need only distribute a new version of that separate module. This keeps your updates smaller and more maintainable.
Below, we begin by creating a module suite. This is a container for the modules that make up our plugin. Our plugin will consist of our existing module, together with the wrapper module for the 3rd party library.
Take the following steps to add a 3rd party library:
Now you can use classes and methods in the library wrapper module, just as if they were defined in your own module.
Typically, a module provides a text file containing licensing conditions applicable to the module. This and similar information is set in the Project Properties dialog box, as explained in this section.
The values shown above serve the following purposes:
Most of the information above is shown in the Plugin Manager when the user installs your module, informing the user of your module's license, home page, and authors of the module.
An "NBM" file, which stands for NetBeans Module, is a special type of ZIP file that contains our sources. The NBM file can be opened just like any other ZIP file and you can inspect its content just like any other ZIP file. However, when we make the NBM file available to others, they can use IJC's Plugin Manager to install the NBM file into their copy of IJC. When the Plugin Manager is used, the NBM file is automatially unzipped and its contents are automatically placed in the correct folders, which are normally in IJC's user directory. In this section, we use the IDE to create the NBM file by invoking a single menu item.
The Output window shows that an NBM file has been created.
An "update center" consists of an XML file that describes the NBM files that you want to make available to others. The IDE can create this XML file for you, as explained in this section. Once you have it, simply place it on a server, together with the NBM files, and then make the URL to the XML file available to your users. Once they have registered the URL in their Plugin Manager, they will be able to access your NBM files and the Plugin Manager will then automatically install them.
Take the following steps to create an update center:
Click Next.
Click Finish.
The module suite is created and new nodes are added to the Projects window.
Click OK.
Browse to the 'AddField' module.
As many NBM files are created as there are modules in the module suite.
Either manually tweak the values, if necessary, or change the settings shown above, within the module project (as described in Packaging the Module), and run the 'Create NBMs' menu item again.
Note: The most important property above is 'distribution', which provides the URL to the location of the NBM file. By default, the assumption is made that the NBM file will be available in the same location and folder structure as the XML file. Hence, as shown above, only the name of the NBM file itself is given. But the value could be something like 'http://foo/bla/com-im-ijc-addfield.nbm' instead, while the XML file could be on a completely different server.
Congratulations, you have completed your first module for IJC. IJC is now extended with a new feature and you have been shown how to prepare and package it for distribution.
To continue learning about the JChem APIs, see the following tutorials: