IJC Renderer Tutorial

Overview

This tutorial shows how to create renderers for IJC. A renderer controls the formatting of a field type used to display data. For example, a renderer can set the alignment and font of integers, which can be different to the alignment and font of floats, because renderers are specified per field type.

The following sections are covered in this tutorial:

Contents:


Prerequisite Knowledge

This tutorial assumes that you have worked through Getting Started with Extending IJC and that you are familiar with the concepts and instructions described therein. As before, you will use NetBeans IDE to create the sample that will extend IJC. As a result, you are assumed to be in possession of the required software and to know how to use NetBeans IDE to:

No time will be spent on the above topics in this tutorial.

Exploring the Sample

Before we begin, let's look at the sample that we will build in this tutorial.

  1. Start IJC and open a JChem table.

  2. Right-click a header in the table and choose "Customize widget settings", as shown here:

    The Table Widget Settings dialog opens.

  3. Look in the "Cell Renderer" drop-down list, which is the first item under "Column properties". You should now see the following:

    Here you see different renderers. One of them is called "Coloring Renderer (IJC API Example)". This is the renderer that we will create in this tutorial.

  4. Choose the Coloring Renderer.

    You now see the following:

  5. Click Close.

    The related column now has green/red cells, depending on whether the value is above or below the split value in the previous dialog:

Now that you are familiar with the feature that we will create in this tutorial, let's begin to set up our module source structure.

Creating the Module Source Structure from Scratch

In this section, we use the New Module wizard, described in Getting Started with Extending IJC, to create a source structure.

  1. Use the New Module template and type 'ColoringRenderer' in 'Project Name'. Click Next.

  2. In the second panel, type 'com.im.ijc.coloringrenderer' in 'Code Name Base', which uniquely defines your module. Click Finish.

You now have a source structure for your module.

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 is part of the IJC application, which itself makes use of all the NetBeans APIs and IJC APIs that we need, we can set dependencies on the modules that are available to the application.

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

  2. In the Project Properties dialog box, choose Libraries.

  3. From the NetBeans Platform drop-down list, make sure that your IJC installation is selected, as explained in Getting Started with Extending IJC.

  4. Click Add Dependency and choose the following modules from the list and 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
    • 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 the data model and persistence tiers for applications that need to use chemical and biological data.
    • Molecule
    • Provides the basic set of visual components, known as widgets, which can be used in the form designer. All widgets implement APIs provided by the Instant JChem Core module and are plugged into the system using declarative XML layers. Some widgets share the rendering of data values and so make use of the UIWidget Renderers module, which contains shared renderer implementations.
    • to be done
    • Provides a set of utility classes covering general infrastructure points in the NetBeans Platform.
    • HelpCtx
    • NbBundle

    You should now see the following:

  5. Click OK.

Creating the Renderer Factory

First, let's begin by creating an implementation of RendererFactory. This interface creates a renderer, depending on the level of suitability of the renderer for the field. Suitability is determined by the RendererFactory.getSuitabilityFor() method. The suitability level is an int between 0 (not supported) and 100 (best renderer for the given field). Below, if the field is an integer or a float, the suitability of the renderer is set to 30. As a result, it will most likely not be the default renderer, because other renderers are likely to have higher suitability levels.

Of the three types of widgets available, we will make our renderer applicable to table cells and IJC widget texts.

  1. Create a Java class called ColoringRendererFactory.java.

  2. Replace the default code with the following:
    public class ColoringRendererFactory implements RendererFactory {
    
        public int getSuitabilityFor(DFField field, RendererFactory.RendererType type) {
            if (type.equals(RendererFactory.RendererType.TableCell) || type.equals(RendererFactory.RendererType.WidgetText)) {
                DFFieldDataTypeCapability cap = (DFFieldDataTypeCapability) DIFUtilities.findCapability(field, DFFieldDataTypeCapability.class);
                if ((cap instanceof DFFieldIntegerCapability) || (cap instanceof DFFieldFloatCapability)) {
                    return 30;
                }
            }
            return UNSUPPORTED;
        }
    
        public TableCellRenderer createTableCellRenderer() {
            return (TableCellRenderer) new ColoringRendererFactory();
        }
    
        public IJCWidgetRenderer createWidgetRenderer() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        public IJCWidgetTextRenderer createWidgetTextRenderer() {
            return (IJCWidgetTextRenderer) new ColoringRendererFactory();
        }
    }

Registering the Renderer Factory

Add the following to the layer.xml file. These entries create a new folder called "IJC", with a subfolder called "RendererFactories". Within that folder, we register our implementation of the RendererFactory interface.

<folder name="IJC">
            
    <folder name="RendererFactories">
        <file name="ColoringRendererFactory.instance">
            <attr name="instanceClass" stringvalue="com.im.ijc.examples.ijcrenderers.ColoringRendererFactory"/>
            <attr name="position" intvalue="100"/>
        </file>
    </folder>

</folder>

Creating the Renderer

Let's create the renderer.

  1. Create a Java class called ColoringRenderer.java.

  2. Let the class extend DefaultTableCellRenderer and implement IJCWidgetTextRenderer, IJCWidgetTextRenderer, and TextFormatInfo.

    The IDE prompts you to let it create import statements and skeleton abstract methods. Let it do so and notice that the IDE creates the following skeleton class:

    public class ColoringRenderer extends DefaultTableCellRenderer implements IJCWidgetTextRenderer, IJCWidgetTextRenderer.TextFormatInfo {
    
        public ColoringRenderer() {
        }
    
        public TextFormatInfo getTextFormatInfo(Object value) {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        public String getValueAsText() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        public boolean getLineWrap() {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
    }

  3. Fill out the generated methods as follows:

    public TextFormatInfo getTextFormatInfo(Object value) {
        setValue(value);
        return this;
    }
    
    public String getValueAsText() {
        return getText();
    }
    
    public boolean getLineWrap() {
        return false;
    }

  4. Declare the following properties and fields at the top of the class:

    public static final String PROP_SPLIT_VALUE = "splitValue";
    public static final String PROP_LOWER_COLOR = "lowerColor";
    public static final String PROP_HIGHER_COLOR = "higherColor";
        
    private float splitValue;
    private Color lowerColor;
    private Color higherColor;

  5. Create getters and setters for your three new fields:

    public void setSplitValue(float splitValue) {
        float oldValue = this.splitValue;
        this.splitValue = splitValue;
        firePropertyChange(PROP_SPLIT_VALUE, oldValue, splitValue);
    }
    
    public float getSplitValue() {
        return splitValue;
    }
    
    public void setLowerColor(Color lowerColor) {
        if (lowerColor == null) {
            throw new IllegalArgumentException();
        }
    
        Color oldValue = this.lowerColor;
        this.lowerColor = lowerColor;
        firePropertyChange(PROP_LOWER_COLOR, oldValue, lowerColor);
    }
    
    public Color getLowerColor() {
        return lowerColor;
    }
    
    public void setHigherColor(Color higherColor) {
        if (higherColor == null) {
            throw new IllegalArgumentException();
        }
    
        Color oldValue = this.higherColor;
        this.higherColor = higherColor;
        firePropertyChange(PROP_HIGHER_COLOR, oldValue, higherColor);
    }
    
    public Color getHigherColor() {
        return higherColor;
    }

  6. Initialize the fields, using the constructor:

    public ColoringRenderer() {
        splitValue = 0f;
        lowerColor = new Color(200, 255, 200);
        higherColor = new Color(255, 200, 200);
    }

  7. Add the following two methods:

    public Component getTableCellRendererComponent(
            JTable table, Object value,
            boolean isSelected, boolean hasFocus,
            int row, int column) {
        Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    
        setValue(value);
        return this;
    }
    
    public void setValue(Object value) {
        super.setValue(value);
    
        if (value instanceof Number) {
            Number n = (Number) value;
            boolean under = (n.floatValue() < splitValue);
            setBackground(under ? lowerColor : higherColor);
        }
    }

  8. Now, at the end of this section, check that your code is as follows:
    public class ColoringRenderer extends DefaultTableCellRenderer implements IJCWidgetTextRenderer, IJCWidgetTextRenderer.TextFormatInfo {
    
        public static final String PROP_SPLIT_VALUE = "splitValue";
        public static final String PROP_LOWER_COLOR = "lowerColor";
        public static final String PROP_HIGHER_COLOR = "higherColor";
        
        private float splitValue;
        private Color lowerColor;
        private Color higherColor;
        
        public ColoringRenderer() {
            splitValue = 0f;
            lowerColor = new Color(200, 255, 200);
            higherColor = new Color(255, 200, 200);
        }
        
        public Component getTableCellRendererComponent(
                JTable table, Object value,
                boolean isSelected, boolean hasFocus,
                int row, int column) {
            Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    
            setValue(value);
            return this;
        }
    
        public void setValue(Object value) {
            super.setValue(value);
    
            if (value instanceof Number) {
                Number n = (Number) value;
                boolean under = (n.floatValue() < splitValue);
                setBackground(under ? lowerColor : higherColor);
            }
        }
        
        public void setSplitValue(float splitValue) {
            float oldValue = this.splitValue;
            this.splitValue = splitValue;
            firePropertyChange(PROP_SPLIT_VALUE, oldValue, splitValue);
        }
    
        public float getSplitValue() {
            return splitValue;
        }
        
        public void setLowerColor(Color lowerColor) {
            if (lowerColor == null) {
                throw new IllegalArgumentException();
            }
            
            Color oldValue = this.lowerColor;
            this.lowerColor = lowerColor;
            firePropertyChange(PROP_LOWER_COLOR, oldValue, lowerColor);
        }
        
        public Color getLowerColor() {
            return lowerColor;
        }
        
        public void setHigherColor(Color higherColor) {
            if (higherColor == null) {
                throw new IllegalArgumentException();
            }
            
            Color oldValue = this.higherColor;
            this.higherColor = higherColor;
            firePropertyChange(PROP_HIGHER_COLOR, oldValue, higherColor);
        }
        
        public Color getHigherColor() {
            return higherColor;
        }
    
        public TextFormatInfo getTextFormatInfo(Object value) {
            setValue(value);
            return this;
        }
    
        public String getValueAsText() {
            return getText();
        }
    }
    

Creating the Bean Info

Let's create the bean info class.

  1. Create a new Java class called ColoringRendererBeanInfo.java.

  2. Replace the default code with the following:
    public class ColoringRendererBeanInfo extends SimpleBeanInfo {
    
        private static final int PROPERTY_splitValue = 0;
        private static final int PROPERTY_lowerColor = 1;
        private static final int PROPERTY_higherColor = 2;
    
        public ColoringRendererBeanInfo() {
        }
    
        private static BeanDescriptor getBdescriptor() {
            BeanDescriptor beanDescriptor = new BeanDescriptor(ColoringRenderer.class, null);
            beanDescriptor.setDisplayName(NbBundle.getMessage(ColoringRendererBeanInfo.class, "TIT_ColoringRenderer"));
            return beanDescriptor;
        }
    
        private static PropertyDescriptor[] getPdescriptor(){
            PropertyDescriptor[] properties = new PropertyDescriptor[3];
        
            try {
                properties[PROPERTY_splitValue] = new PropertyDescriptor ( "splitValue", ColoringRenderer.class, "getSplitValue", "setSplitValue" ); // NOI18N
                properties[PROPERTY_splitValue].setDisplayName ( NbBundle.getMessage(ColoringRendererBeanInfo.class, "NAME_SplitValue") );
                properties[PROPERTY_lowerColor] = new PropertyDescriptor ( "lowerColor", ColoringRenderer.class, "getLowerColor", "setLowerColor" ); // NOI18N
                properties[PROPERTY_lowerColor].setDisplayName ( NbBundle.getMessage(ColoringRendererBeanInfo.class,"NAME_LowerColor") );
                properties[PROPERTY_higherColor] = new PropertyDescriptor ( "higherColor", ColoringRenderer.class, "getHigherColor", "setHigherColor" ); // NOI18N
                properties[PROPERTY_higherColor].setDisplayName ( NbBundle.getMessage(ColoringRendererBeanInfo.class,"NAME_HigherColor") );
            }
            catch(IntrospectionException e) {
                e.printStackTrace();
            }
            return properties;     
        }
        
        private static EventSetDescriptor[] getEdescriptor(){
            EventSetDescriptor[] eventSets = new EventSetDescriptor[0];
            return eventSets;     
        }
        
        private static MethodDescriptor[] getMdescriptor(){
            MethodDescriptor[] methods = new MethodDescriptor[0];
            return methods;     
        }
        
        private static final int defaultPropertyIndex = -1;
        private static final int defaultEventIndex = -1;
        
        public BeanDescriptor getBeanDescriptor() {
            return getBdescriptor();
        }
        
        public PropertyDescriptor[] getPropertyDescriptors() {
            return getPdescriptor();
        }
        
        public EventSetDescriptor[] getEventSetDescriptors() {
            return getEdescriptor();
        }
        
        public MethodDescriptor[] getMethodDescriptors() {
            return getMdescriptor();
        }
        
        public int getDefaultPropertyIndex() {
            return defaultPropertyIndex;
        }
        
        public int getDefaultEventIndex() {
            return defaultEventIndex;
        }
    }

  3. Add the following strings to the Bundle.properties file:
    TIT_ColoringRenderer=Coloring Renderer (IJC API Example)
    NAME_SplitValue=The split value
    NAME_LowerColor=Color under the value
    NAME_HigherColor=Color above the value

Persisting the Renderer

Add the highlighted entries below to the IJC folder in the layer.xml:

<folder name="IJC">

    <folder name="RendererFactories">
        <file name="ColoringRendererFactory.instance">
            <attr name="instanceClass" stringvalue="com.im.ijc.examples.ijcrenderers.ColoringRendererFactory"/>
            <attr name="position" intvalue="100"/>
        </file>
    </folder>

    <folder name="persistence">

        <folder name="classes">
            <file name="com_im_ijc_examples_ijcrenderers_ColoringRenderer.instance">
                <attr name="implClass" stringvalue="com.im.ijc.renderers.xml.CellRendererPersister"/>
                <attr name="instanceCreate" methodvalue="com.im.ijc.core.xml.beans.FSPersisterFactory.create"/>
                <attr name="class" stringvalue="com.im.ijc.examples.ijcrenderers.ColoringRenderer"/>
                <attr name="localname" stringvalue="coloringRenderer"/>
                <attr name="namespace" stringvalue="http://informaticsmatters.com/xml/ijc/1.0"/>
                <attr name="position" intvalue="1500"/>
            </file>
        </folder>

        <folder name="xmlns">
            <folder name="http___informaticsmatters_com_xml_ijc_1_0">
                <file name="coloringRenderer.instance">
                    <attr name="implClass" stringvalue="com.im.ijc.renderers.xml.CellRendererPersister"/>
                    <attr name="instanceCreate" methodvalue="com.im.ijc.core.xml.beans.FSPersisterFactory.create"/>
                    <attr name="class" stringvalue="com.im.ijc.examples.ijcrenderers.ColoringRenderer"/>
                    <attr name="localname" stringvalue="coloringRenderer"/>
                    <attr name="namespace" stringvalue="http://informaticsmatters.com/xml/ijc/1.0"/>
                    <attr name="position" intvalue="600"/>
                </file>
            </folder>
        </folder>

    </folder>

</folder>

The new persistence folder that you need to add, as shown above, contains registration entries for writer-factories and reader-factories that store or read UI object settings. When a JavaBean is to be written, IJC looks up a writer (persister), using the class name (obj.getClass().getName()) to write the state to XML format. If such a persister is not registered, the persister for the superclass is tried, etc. A Beaninfo-based Persister is registered for j.o.Object, so each Object is ultimately written, but the object's class name is stored in the XML file, and all such objects create the element "ijc:simpleBean" in the XML.

Therefore, if possible, you should register the persister for a class, in order to get a nice XML element with a tag composed of "namespace":"localName", which looks a better. But, mainly, the reason you need to do this is to give Java Class names (which are implementation details that may change from release to release) "abstract" XML element names. You are free to change the implementation classes, provided that a Persister is registered to the element xmlns:localName in a future IJC release and assuming you will still be able to read old data.

There is boilerplate support for persisting (reading/writing) Beans implemented in the FSPersisterFactory class, which gets the BeanInfo for the persisted object and stores properties described by the BeanInfo, using BeanInfo-published get and set methods. Primitive values are stored as attributes, Object values are saved (read) using Persister for the type declared in the BeanInfo (or the actual type). Therefore, for simple objects, the simple declaration shown above is fine. For this basic support, these attributes are necessary:

For writing Beans, the Persister must be registered as follows:

/persistence/classes/class-name-with-dots-replaced-by-underscores

For reading Beans, the Persister must be registered as follows:

/persistence/xmlns/namespace-with-nonalphanum-replaced-by-underscores/localname-with-replaced-nonalphanum

You are recommended to register these using .instance DataObjects (using the .instance extension and "instanceCreate"/"class" attributes), since this is what is directly supported by the NetBeans core.

This Coloring Renderer example has a BeanInfo class that declares three properties. One of them is a 'float' type (a primitive). There is also a common Persister for the java.awt.Color class available, which encodes the Color object as an attribute that contains a hexadecimal color value. In other words, the FSPersisterFactory, as declared above, will follow the BeanInfo, serialize (or read) the "splitValue" as an attribute, and the "lowColor" and "highColor" using the Color persister. In effect, the Color Renderer is serialized as something like this:

<renderer splitValue="1.0" higherColor="#ffffff" lowerColor="#00000"/>

Above, the namespaces and implementation detail attributes are omitted for clarity.

Conclusion

Congratulations, you have created your first renderer for IJC. You simply need to run the module, as described earlier, and then IJC is extended with your new feature and you can use it and distribute it to your users.