Getting Started with Extending IJC

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.

Required Software

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:

Exploring the Sample

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.

  1. Start up IJC and open a JChem entity.

  2. Select the "Add Atom count field" menu item under the Help menu.

    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.

Setting Up an Environment for Extending IJC

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.

  1. In NetBeans IDE, choose NetBeans Platforms under the Tools menu.

    The NetBeans Platform Manager appears.

  2. Click Add Platform and browse to the top level folder of your IJC installation directory. When you do so, the Next and Finish buttons are enabled, as shown below:

    If you click Next, you can assign a name to your platform.

  3. Click Finish.

    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.

Creating the Module Source Structure from Scratch

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.

  1. Go to the 'File' menu in the main menu bar and choose 'New Project'.

    The New Project wizard appears.

  2. In the 'Categories' list, choose 'NetBeans Modules'. Under 'Projects', choose 'Module'. Click Next.

    The New Module wizard appears.

  3. In the first panel, type 'AddField' in 'Project Name', as shown below:

    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.

  4. In the second panel, type 'com.im.ijc.addfield' in 'Code Name Base', which uniquely defines your module, as shown below:

    Click Finish.

    The source structure of your module is generated and you can see the outline in the Projects window.

Setting Dependencies on Other Modules

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.

  1. Right-click the 'AddField' module project in the Projects window and choose Properties.

    The Project Properties dialog box appears.

  2. In the Project Properties dialog box, choose Libraries. Notice that the NetBeans Platform drop-down list shows the IJC installation that you registered earlier. Now we can select the modules we need, provided by IJC. This will let us build our own module "on top of" IJC.

  3. In the Libraries panel, click Add, next to the Module Dependencies list, and choose the following modules. While doing so, note the Classes column because there you can see the classes we will be using from the related API provided by the module:

    Module Purpose Classes Relevant to Our Module
    • Provides miscellaneous general classes, especially relating to the handling of user notifications and the displaying of dialogs and wizards.
    • DialogDescriptor
    • DialogDisplayer
    • DIF (Discovery Informatics Framework)
    • Provides the data model and persistence tiers for applications that need to use chemical and biological data.
    • DFFieldIntegerCapability
    • DFFieldStructureCapability
    • JChemBaseEntityCapability
    • DFEntity
    • DFField
    • DFEntityDataProvider
    • DFTermExpression
    • DFFeedback
    • DFNewType
    • DFNewTypeOptions,
    • DFNewTypeWellKnownOptions
    • SortDirective
    • DIFUtilities
    • MarvinStructure
    • CancelException
    • UIBackgroundRunnerRW
    • Provides the basic user interface for Instant JChem and through its API it provides the ability to extend the IJC user interface.
    • EntityCookie
    • Provides access to ChemAxon's Java toolkits that let you perform sophisticated chemistry and database operations in Java code.
    • Molecule
    • Provides support for NetBeans Platform design patterns, called 'cookies', which are used to add behavior to existing data objects and nodes.
    • CookieAction
    • Node
    • Provides a set of utility classes covering general infrastructure points in the NetBeans Platform.
    • HelpCtx
    • NbBundle

    You should now see the following:

  4. Click OK.

  5. Now open the Project Metadata file in the Important Files node. Notice that all the dependencies have been declared as entries similar to the one below, as a result of the steps you have taken in this section.
    <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.

Creating the Context-Aware Toolbar Button and Menu Item

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.

  1. Right-click the module project node in the Projects window and choose New > Action.

  2. In the first panel, specify that you want to create a conditionally enabled action that will be sensitive to entity cookies, which represent tables in IJC, as shown here:

    Click Next.

  3. In the next panel, specify that the action will appear in the Edit menu and in the Edit toolbar, as shown below:

    Click Next.

  4. In the final panel, type 'AddFieldAction' in 'Class Name'. Type 'Add Field' in 'Display Name'.

  5. In 'Icon', browse to a 16x16 pixel icon, which will be displayed in the toolbar button. If there is a 24x24 pixel icon with the same name scheme, the IDE will recognize it. For example, if the first icon is named 'icon-16.png', the IDE will assume the second icon is named 'icon-24.png'. Ideally, you will have a small icon (16x16 pixels) and a large icon (24x24 pixels) available. If not, here are two icons that you can use for this purpose:

  6. Finally, specify the package in which the new action class will be created by the wizard.

    You should now see the following:

  7. Click Finish.

    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:

Running the Module

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.

  1. Right-click the module and choose Run. Because we registered an installation of IJC as our platform, at the start of this tutorial, as described in Setting Up an Environment for Extending IJC, IJC starts up and the module is installed within it.

  2. Look in the toolbar and notice that you have a new toolbar button (on the extreme left), displaying the icon that you specified earlier in this tutorial. Under the Edit menu, notice that you have a new 'Add Field' menu item. By default both are disabled. For example, the toolbar button looks as follows, by default (here the new toolbar button is on the extreme left of the toolbar):

  3. Open a table and give its tab in the editor area the focus with your mouse or with the keyboard. Notice that the menu item and toolbar button immediately become enabled. For example, the toolbar button now looks as follows, which means it is enabled and you can click it:

    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.

Examining the Generated Code

Before continuing with the coding of our module, let's examine the artifacts that the New Action wizard generated for us.

Examining the Action Class

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;
    }
    
}

Examining the Layer File

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>

Narrowing the Scope of the Action

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.

  1. We begin by creating a utility method for finding all entities.
    private static DFEntity findEntity(Node[] activatedNodes) {
        EntityCookie entityCookie = (EntityCookie) activatedNodes[0].getLookup().lookup(EntityCookie.class);
        if (entityCookie != null) {
            return entityCookie.getItem();
        }
        return null;
    }
  2. Once we have identified an entity, we can use the CookieAction.enable() method to return true only if certain conditions are met. And only if true is returned will our action be enabled. In this case we will enable our action only when the super class is enabled and when the entity contains JChemBaseEntityCapability, which means it is a JChem entity:
    protected boolean enable(Node[] activatedNodes) {
        if (super.enable(activatedNodes)) {
            DFEntity entity = findEntity(activatedNodes);
            return (DIFUtilities.findCapability(entity, JChemBaseEntityCapability.class) != null);
        }
        return false;
    }

  3. Run the module again.

  4. Notice that this time the action is not enabled when a standard entity is in focus. Instead, the action is only enabled when a JChem entity is in focus.

Showing a Dialog

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.

  1. Fill out the performAction method as follows:
    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!");
        }
         
    }

  2. Add the following strings to the Bundle.properties file:
    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

  3. Run the module again, invoke the action, and notice the following:

  4. Click "Yes" and notice that a "Hurray!" message is returned. In the next section, we will replace the "Hurray!" message with meaningful code for adding a field to the JChem table.

Creating the Field

In this section, we create a field by using asynchronous tasks running in the background.

  1. Replace the line that creates the "Hurray!" message with the following line:
    createNewField(entity);

  2. Declare our variables, which we will learn about later:
    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");

  3. Add a utility method for identifying a field in a JChem table:

    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;
    }

  4. Next, we will add code for creating a new field, which in IJC's case is a database column in the specified entity, which is a database table. Then we will invoke a method that fills all the rows with data.
    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();
    
    }

  5. Now we need to fill data.
    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();
    
    }

  6. Add the following strings to the Bundle.properties file:
    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.

Trying Out the Module

Our module is complete. Let's try it out again before we think about distributing it.

  1. Run the module again, as described in Running the Module.

  2. Open a JChem entity.

  3. Click the new toolbar button or menu item.

    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.

Adding 3rd Party Libraries

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:

  1. Go to the 'File' menu in the main menu bar and choose 'New Project'. The New Project wizard appears. In the 'Categories' list, choose 'NetBeans Modules'. Under 'Projects', choose 'Module Suite'. Click Next. Name the suite whatever you like and specify where it should be created. Click Finish.
  2. Right-click the module suite's Modules node and add the existing module.
  3. Right-click the module suite's Modules node and choose 'Add New Library'. When you complete this wizard, you will have a module that contains one or more JARs. (Use Shift-Click and Ctrl-Click to select more than one JAR.)
  4. Right-click the functionality module, choose Properties, and add a dependency on the library wrapper module, using the Libraries tab to do so. Click Finish

Now you can use classes and methods in the library wrapper module, just as if they were defined in your own module.

Packaging the 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.

  1. Right-click the project and choose Properties.

  2. In the Project Properties dialog box, choose Packaging. You should now see the following:

    The values shown above serve the following purposes:

    • Needs Restart on Install. Specifies that after installation of the module, IJC needs to be restarted. This may be applicable when you need the entire application to be refreshed in order for the module to function correctly.
    • License. Specifies a text file containing the module's licensing conditions.
    • Home Page. Specifies a URL to the module's home page, which could contain information such as a list of features provided by the module.
    • Author. Specifies the name or names of the module's authors.

    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.

  3. All the settings in the Packaging page are optional. Depending on your needs, fill out the settings that you consider relevant.

Creating and Distributing the NBM File

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.

  1. Right-click the module and choose Create NBM.

    The Output window shows that an NBM file has been created.

  2. Open the Files window and notice that you have an NBM file in the 'build' folder, as shown below:

  3. Make the NBM file available to others via, for example, an update center. Alternatively, you can simply send the NBM file via e-mail.

Creating an Update Center

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:

  1. In the New Project wizard, choose Module Suite Project, as shown below:

    Click Next.

  2. Fill in a name for the suite, as shown below:

    Click Finish.

    The module suite is created and new nodes are added to the Projects window.

  3. Right-click the module suite, choose Properties, and select the Libraries panel in the Project Properties dialog box. Set your IJC installation as the NetBeans Platform and make sure all the checkboxes are checked, as shown below:

    Click OK.

  4. Right-click the module suite's Modules node and choose 'Add Existing...', as shown below:

    Browse to the 'AddField' module.

  5. Once the 'AddField' module is added, right-click the module suite and choose 'Create NBMs', as shown below:

    As many NBM files are created as there are modules in the module suite.

  6. In the Files window (Ctrl-2), expand the module suite's 'build' folder, and notice that the AddField NBM file is found there, together with an XML file called 'updates.xml'. This is the XML file that describes all the NBM files that you want to make available. Since you currently only have one file, only one file is described:

    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.

  7. Upload the XML file and the NBM file(s) to a server of your choice. Let your users know the URL to the XML file, which they must then register in the Plugin Manager.

Conclusion

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: