Tuesday Apr 29, 2008

Introducing "Feature on Demand"

As your application gets larger, the end user experiences two problems: the download size increases and performance slows down. Not necessarily so, but the likelihood of these problems increases as time goes by. That's where modular systems come in handy, because they let the end user choose the modules they want while discarding all those they don't need. However, that process is problematic. How does the end user know which module provides which features? And then there's the problem of the ui that you provide for making features available, which is often not optimal for all your users. Sure, you could let the application automatically install all features and all updates to all features. That might be useful for many of your users. However, not all users want all features and all updates. On the one hand, these users would like to be able to select which features (and which updates to features) they want, but they also want the process to be really simple.

Welcome to "feature on demand". That's a new NetBeans Platform concept that's in the process of being evolved. It aims to enable you to provide the user with a really lightweight application (i.e., very small download, very small footprint, very small number of modules, very small everything). However, despite the lightness of the application, all/most of its ui is already available. Then, whenever the end user wants to use a feature in the application, such as a particular window, the application will automatically install that window, in the background. How? By making a menu item (or other entry point) available which, when invoked, causes the related module to download in the background, install itself automatically into the application, and immediately make its ui elements (such as a new window) available to the end user.

Potentially three parties are involved in this process:

  • The end user. I.e., granny Smith at home, filling in her tax returns in an application created on top of the NetBeans Platform.

  • The "feature on demand" service consumer. This is the developer who makes use of an existing entry point in the application by plugging into it. For example, let's say the developer writes a module and puts it into an update center. The module provides a new window in the application. In a separate module that is part of the application's official distribution (i.e., or that is installed automatically, or in some very simple way) the developer specifies that the module should be installed on demand, i.e., only when granny Smith invokes the related menu item. This module, i.e., the module consuming an entry point, is very small (just one or two XML files, as you can see in the example screenshot below), whereas the module (or multiple modules, including external libraries) providing all the functionality is much larger. Hence, distributing entry point consumers with your application is much more economical than distributing fully fledged features. In effect, you're distributing fake ui (such as a menu item that appears to invoke a window, while in fact it also installs it), while the real feature will be installed when the fake ui is used.

  • The "feature on demand" service provider. By default, "feature on demand" entry points exist (at least, currently) for new actions and new project types. That means that the service consumer can make use of these two entry points out of the box. However, potentially, you'd want additional entry points for your own features. Possibly your feature is not related to a menu item or project type. For example, when granny Smith tries to hyperlink in NetBeans IDE from one location to another, the module that provides that feature could be installed in the background, if the service provider provides such an entry point to the service consumer. The service provider is not concerned with specific modules, i.e., the service provider doesn't know what modules the service consumers will want to make available. The service provider simply makes entry points into the application available. This person will have a very technical knowledge of the application, while the service consumer needs to do nothing other than provide XML files that define how the entry point should be consumed in a particular case. The service provider and the service consumer could be the same person. And, of course, granny Smith at home could be all three at once, although this is less likely.

The end user, i.e., granny Smith, only gets involved in this whole cycle when she selects the menu item (or whatever the service provider makes available to the service consumer to let granny Smith demand the feature). But what about the service consumer? How does the service consumer consume the entry point? Let's say the service consumer wants to make the Module Manager available to granny Smith, but only when granny Smith selects the menu item to invoke it. Note that the Module Manager is a window in the IDE, provided by a module that's in the Update Center in 6.0 and 6.1, but never installed by default. Here's all that the service consumer needs to do to provide a fake "Module Manager" menu item in the Window menu which, when clicked, will install the Module Manager in the background:

Here's the content of the "modulemanager.xml" file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd">
<filesystem>
    //We want to contribute a new menu item to the application:
    <folder name="Menu">
        //The new menu item will be within the Window menu:
        <folder name="Window">
            //The unique identifier of the menu item:
            <file name="org-yourorghere-modulemanager-ModuleManagerAction.instance">
                //The reference to the API to which we delegate the creation of the menu item:
                <attr name="instanceCreate" methodvalue="org.netbeans.spi.actions.support.Factory.delegate"/>
                //The reference to the service provider's action that will be invoked when the menu item is created:
                <attr name="delegate" methodvalue="org.netbeans.modules.autoupdate.featureondemand.api.Factory.newAction"/>
                //The module that will be installed when the menu item is selected:
                <attr name="codeName" stringvalue="org.netbeans.modules.modulemanager"/>
                //The localization bundle that contains the display name of the menu item:
                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.yourorghere.modulemanager.Bundle"/>
                //The key in the localization bundle that defines the display name of the menu item:
                <attr name="ActionName" stringvalue="org-yourorghere-modulemanager-ModuleManagerAction" />
                //The position of the menu item within the Window menu:
                <attr name="position" intvalue="10"/>
            </file>
        </folder>
    </folder>
</filesystem>

The above looks a lot like a layer.xml file, doesn't it? That's because that's what it is. It is known as the "delegate layer file", which is injected into the System FileSystem under certain conditions. Which conditions? The conditions are specified in the layer.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN" "http://www.netbeans.org/dtds/filesystem-1_1.dtd">
<filesystem>
    //The official "feature on demand" folder in the System FileSystem:
    <folder name="FeaturesOnDemand">
        //The unique name of this entry in the "feature on demand" folder:
        <file name="org-yourorghere-modulemanager-ModuleManagerAction.instance">
            //The API class that holds the delegate layer file, with related information:
            <attr name="instanceCreate" methodvalue="org.netbeans.modules.autoupdate.featureondemand.api.FeatureInfo.create"/>
            //The presence of this plugin determines whether the delegate layer will be injected
            //i.e., if this plugin is not installed, the menu item will be shown, and vice versa,
            //this plugin could potentially be different to the plugin that the menu item will install:
            <attr name="codeName" stringvalue="org.netbeans.modules.modulemanager"/>
            //The location of the delegate layer file:
            <attr name="delegateLayer" urlvalue="nbresloc:/org/yourorghere/modulemanager/modulemanager.xml"/>
        </file>
    </folder>
</filesystem>

So, now, granny Smith, filling in her tax returns, would see "Module Manager" in the Window menu (if the Module Manager plugin isn't already installed). She would, if she selects the Module Manager menu item, see that the related plugin installs and then immediately makes the Module Manager available. Until that point, the Module Manager will not be installed in the application and thus will not be weighing down the application, in terms of download size, footprint, and performance. By the way, all of this assumes that the plugin that will be installed on demand is actually available in one of the registered update centers.

Everything required for implementing "Feature on Demand" is already available in the "contrib" module in the NetBeans sources. For more information on this, together with several demos, including how to provide a new entry point, come to the JavaOne BOF "Toward a Consumer IDE: Get What You Want When You Want It (BOF 5091)", on Wednesday 18.30-19.20. I will be presenting this BOF together with the engineer who conceptualized and implemented "Feature on Demand", the NetBeans update center guru Jiri Rechtacek.

Monday Apr 28, 2008

org.openide.filesystems.FileUtil.findBrother

The org.openide.filesystems.FileUtil.findBrother method is typically used by the Matisse GUI Builder so that if a file named "xyz.java" is in the same folder as a file named "xyz.frm", the two are merged together and opened as one in the editor. Specifically, FileUtil.findBrother takes a FileObject and a file extension, which is the file extension of the "brother" file.

There are several other conceivable scenarios where this functionality might be handy, such as Wicket, which typically has "xyz.java" in the same folder as "xyz.html". The latter provides the markup for the former. Below you can see my point, the Java files that have a matching HTML file (i.e., the same name) have a Wicket icon merged with their Java icon:

So, first create a new DataObject for text/x-java—and make sure it will install before the standard Java DataObject. Then, use the DataLoader to branch the DataObject, depending on whether a "brother" is found:

@Override
protected MultiDataObject createMultiObject(FileObject primaryFile) throws DataObjectExistsException, IOException {
    FileObject bro = FileUtil.findBrother(primaryFile, "html");
    if (null != bro) {
        //Return our own data object:
        return new WicketDataObject(primaryFile, this);
    }
    //Return the standard data object:
    return new JavaDataObject(primaryFile, this);
}

Finally, in the Node, use org.openide.util.Utilities.mergeImages to merge a small image on top of the standard Java icon:

private static final String WICKET_ICON_BASE = 
         "org/netbeans/findbrotherdemo/wicket_8x8.png";
private static final String JAVA_ICON_BASE =   
         "org/netbeans/modules/java/resources/class.gif";
 
@Override
public Image getIcon(int arg0) {
    Image wicket = Utilities.loadImage(WICKET_ICON_BASE);
    Image java = Utilities.loadImage(JAVA_ICON_BASE);
    Image result = Utilities.mergeImages(java, wicket, 8, 8);
    return result;
}

Install the module and then Java files with HTML brothers will have our Wicket icon merged with their Java icon.

Reorganized & Simplified Wicket Support

In preparation for a JavaOne demo, I've simplified the NetBeans/Wicket support, from a ui and user perspective. This is what the Frameworks panel now looks like, after I removed some superfluous options (so that now the header panel is always created, while the useless option for the dummy pojo is removed) and changed some default names:

And when the user finishes the wizard, they will see exactly this, i.e., the source package folder will be open automatically and the HomePage class will be open in the editor, because that's probably the first place where you'll start coding. A default model setting is defined in the generated HomePage class and the org.apache.wicket.markup.html.resources.StyleSheetReference class is used in the BasePage class, to provide localized CSS support. Notice below also that the default names of the generated files have been changed and simplified, so that it's easier to see what's what, especially if you have some Wicket background:

For example, as you can see, pages and panels are easily distinguishable, now, because the name of the generated file (by default, anyway) contains the related info in this regard. Finally, no index.jsp is created, for the first time. The IDE's Frameworks support creates an index.jsp file by default, if no welcome file element is defined in the web.xml file by the module. So, I defined a welcome file element (even though it isn't used by Wicket) and so now the index.jsp is no longer created.

I need to do a bit more work on the module, such as upgrading the libraries to Wicket 3.3 (which fixes at least one important Ajax-related bug that I am aware of) and I'm hoping to have committed all my changes to CVS by the time JavaOne begins.

Sunday Apr 27, 2008

Using Spring to Enable the Print Menu Item

Recently I blogged about Spring integration into the NetBeans module system. Here's how you would use that integration to enable the Print menu item for a TopComponent. In effect, what this example shows is how to use a Spring configuration file to extend a TopComponent's Lookup to include a PrintCookie:

  1. Implement PrintCookie, which requires a dependency on Nodes API:

    package org.yourorghere.nbspringdemo1;
    
    import javax.swing.JOptionPane;
    import org.openide.cookies.PrintCookie;
    
    public class PrintImpl implements PrintCookie {
    
        @Override
        public void print() {
            JOptionPane.showMessageDialog(null, "I am printing...");
        }
    
    }

  2. Create a bean for your PrintCookie in your Spring app-config.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans 
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
        <bean id="printCookie" class="org.yourorghere.nbspringdemo1.PrintImpl"/>
    </beans>

  3. Add to the TopComponent's constructor:

    //Bring in Spring:
    String[] contextPaths = new String[]{"org/yourorghere/nbspringdemo1/app-context.xml"};
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(contextPaths);
    
    //Convert Spring to Lookup:
    Lookup lookup = NbSpring.create(ctx);
    
    //Potentially, look up your JPanel and add to TopComponent:
    //Item<JPanel> item = lookup.lookupItem(new Template<JPanel>(JPanel.class, null, null));
    //JPanel foo = item.getInstance();
    //add(foo, java.awt.BorderLayout.CENTER);
    
    //Add node for PrintCookie:
    AbstractNode myNode = new AbstractNode(Children.LEAF, lookup);
    ProxyLookup proxyLookup = new ProxyLookup(Lookups.singleton(myNode), myNode.getLookup());
    associateLookup(proxyLookup);

That's it. Now the Print menu item is enabled whenever the TopComponent is activated. You could narrow it down further, basing it on whether something in the TopComponent has changed.

Thanks to Jaroslav Tulach for showing me how to do this.

Saturday Apr 26, 2008

cismet Developers Will Code for Food!

The "Will Code for Food" competition takes us to... a small software company named cismet based in Saarland, a small state in Germany. The guys from cismet are the ones who created the "Log4J Logging Viewer plugin for NetBeans IDE". It is also known as BeanMill. On their Partners page, you can see that cismet is one of the NetBeans partners.

"Thanks for the nice idea," writes Sebastian Puhl, one of the cismet developers, "we had fun making the pictures for the competition." The whole series is in cismet's Flickr account. My personal favorite is this one:

Remember that the 1st Annual "Will Code For Food" Competition will end on Friday, 1 August 2008, and that the winner will receive a signed copy of Jaroslav Tulach's upcoming book on API design. Submissions to geertjan DOT wielenga AT sun DOT com.

Friday Apr 25, 2008

NetBeans Lookup Example

Lookup example, producing a new message automatically every 2 seconds:

  • Provider module:

    public final class Selection {
    
        private Selection() {
        }
    
        private static MyLookup LKP = new MyLookup();
    
        //Make the Lookup accessible:
        public static Lookup getSelection() {
            return LKP;
        }
    
        private static final class MyLookup extends ProxyLookup
                implements Runnable {
    
            private static ScheduledExecutorService EX = 
                    Executors.newSingleThreadScheduledExecutor();
    
            public MyLookup() {
                EX.schedule(this, 2000, TimeUnit.MILLISECONDS);
            }
    
            private int i;
    
            @Override
            public void run() {
                //Add to the Lookup a new MyHello:
                setLookups(Lookups.singleton(new MyHello(i++)));
                EX.schedule(this, 2000, TimeUnit.MILLISECONDS);
            }
    
        }
    
        private static final class MyHello implements HelloProvider {
    
            private String text;
    
            public MyHello(int i) {
                text = i % 2 == 0 ? "Hello from Tom" : "Hello from Jerry";
            }
    
            public String sayHello() {
                return text;
            }
    
        }
        
    }

    public interface HelloProvider {
        public String sayHello();
    }

  • Consumer module, with dependency on the provider module:

    final class HelloTopComponent extends TopComponent implements LookupListener {
    
        private static HelloTopComponent instance;
        private static final String PREFERRED_ID = "HelloTopComponent";
        private Result result;
    
        private HelloTopComponent() {
            
            ...
            ...
            ...
    
            //We have a dependency on the provider module,
            //where we can access Selection.getSelection():
            Lookup lookup = Selection.getSelection();
    
            //Get the HelloProvider from the result:
            result = lookup.lookupResult(HelloProvider.class);
            
            //Add LookupListener on the result:
            result.addLookupListener(this);
    
            //Call result changed:
            resultChanged(null);
            
        }
    
        StringBuilder sb = new StringBuilder();
        int i;
    
        @Override
        public void resultChanged(LookupEvent arg0) {
            long mills = System.currentTimeMillis();
            Collection<? extends HelloProvider> instances = result.allInstances();
            for (HelloProvider helloProvider : instances) {
                String hello = helloProvider.sayHello();
                sb.append(mills + ": " + hello + "\\n");
                jTextArea1.setText(sb.toString());
            }
        }
    
       ...
       ...
       ...

Result:

Thursday Apr 24, 2008

Vincent Cantin: "Will Show Hidden Folders For Food"

The first entry for the 1st Annual "Will Code For Food" Competition:

Possibly the above is a reference to The Mystery of the Hidden Folders in the Favorites Window and on the thread on which that blog entry is based.

So now the flood gates are open... more "code for food" photos are welcome at geertjan DOT wielenga AT sun DOT com.

Tuesday Apr 22, 2008

Hello Code Generator

I click Alt-Insert and I see this:

Here's the code:

import com.sun.source.util.TreePath;
import java.io.IOException;
import java.util.Collections;
import javax.swing.JOptionPane;
import javax.swing.text.JTextComponent;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.modules.java.editor.codegen.CodeGenerator;

public class HelloGenerator implements CodeGenerator {

    public static class Factory implements CodeGenerator.Factory {

        public Factory() {
        }

        @Override
        public Iterable<? extends CodeGenerator> create
                (CompilationController controller, TreePath path) throws IOException {
            return Collections.singleton(new HelloGenerator());
        }
    }

    @Override
    public String getDisplayName() {
        return "Hello World";
    }

    @Override
    public void invoke(JTextComponent arg0) {
        JOptionPane.showMessageDialog(null, "Hello World!");
    }

}

Doesn't do anything yet, but gives you a starting point. Register it like this:

<filesystem>
    <folder name="Editors">
        <folder name="text">
            <folder name="x-java">
                <folder name="codegenerators">
                    <file name="org-netbeans-modules-my-codegen-HelloGenerator$Factory.instance">
                        <attr name="position" intvalue="10"/>
                    </file>
                </folder>
            </folder>
        </folder>
    </folder>
</filesystem>

Dependencies: Javac API Wrapper, Java Editor (implementation dependency, because the above is not a public API yet), and Java Source.

Monday Apr 21, 2008

Converting Spring to Lookup

In the screenshot below, the content of the "Spring Window" is injected via a Spring configuration file:

The highlighted classes above are the Swing components and behavior that Spring injects into the TopComponent (except for 'SpringAction', which opens the window). In the editor area above, the following constructor is shown:

private SpringTopComponent() {

    initComponents();

    setName(NbBundle.getMessage(SpringTopComponent.class, "CTL_SpringTopComponent"));
    setToolTipText(NbBundle.getMessage(SpringTopComponent.class, "HINT_SpringTopComponent"));

    String[] contextPaths = new String[]{"org/netbeans/nbspringdemo/app-context.xml"};
    ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(contextPaths);

    Lookup lookup = NbSpring.create(ctx);
    Item item = lookup.lookupItem(new Template(JPanel.class, null, null));
    JPanel foo = (JPanel) ctx.getBean(item.getId()); 

    add(foo, java.awt.BorderLayout.CENTER);

}

The line in bold above is possible as a result of the new Spring/NetBeans API that is in 'contrib', as highlighted in my blog yesterday. That line is the thing that makes this possible at all. Without it, i.e., without being able to convert my Spring configuration file to Lookup, none of this would be possible. And what does my Spring configuration file look like? Exactly this:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
           
    <bean id="mainPanel" class="org.netbeans.nbspringdemo.MyJPanel" init-method="init">
        <property name="axis">
            <value>1</value>
        </property>
        <property name="panelComponents">
            <list>
                <ref bean="textField1"/>
                <ref bean="textField2"/>
                <ref bean="textField3"/>
                <ref bean="buttonPanel"/>
            </list>
        </property>
    </bean>

    <bean id="buttonPanel" class="org.netbeans.nbspringdemo.MyJPanel" init-method="init">
        <property name="axis">
            <value>0</value>
        </property>
        <property name="panelComponents">
            <list>
                <ref bean="button1"/>
            </list>
        </property>
    </bean>

    <bean id="textField1" class="org.netbeans.nbspringdemo.MyJTextField" init-method="init">
        <property name="text">
            <value>hello 1</value>
        </property>
        <property name="rColor">
            <value>255</value>
        </property>
        <property name="gColor">
            <value>51</value>
        </property>
        <property name="bColor">
            <value>102</value>
        </property>
    </bean>

    <bean id="textField2" class="org.netbeans.nbspringdemo.MyJTextField" init-method="init">
        <property name="text">
            <value>hello 2</value>
        </property>
        <property name="rColor">
            <value>0</value>
        </property>
        <property name="gColor">
            <value>100</value>
        </property>
        <property name="bColor">
            <value>0</value>
        </property>
    </bean>

    <bean id="textField3" class="javax.swing.JTextField">
        <property name="text">
            <value>goodbye world</value>
        </property>
    </bean>

    <bean id="button1" class="org.netbeans.nbspringdemo.MyJButton" init-method="init">
        <property name="actionListener">
            <ref bean="myButtonActionListener"/>
        </property>
        <property name="text">
            <value>Click me!</value>
        </property>
    </bean>

    <bean id="myButtonActionListener" class="org.netbeans.nbspringdemo.MyActionListener"/>

</beans>

That's the Spring configuration file that I discussed in Spring: How to Create Decoupled Swing Components on JavaLobby. The definition of the classes, i.e., the JPanel, JButton, and JTextField, are also described in that article. Read the comments to that article to see some use cases where you might want to assemble your user interface via decoupled Swing components and Spring.

Pretty cool that this is now also possible on the NetBeans Platform. I am not advocating this approach, I am merely pointing out that this is possible.

In other news. Read this blog entry about NetBeans Day Fortaleza!

A Picture That Speaks 1000 Words

Sunday Apr 20, 2008

The Mystery of the Hidden Folders in the Favorites Window

How is that possible..? What did I do to include the hidden folders in the Favorites window?

Answer: Tools | Options | Advanced Options | IDE Configuration | System | System Settings | Ignored Files.

Thursday Apr 17, 2008

1st Annual "Will Code For Food" Competition

NetBeans "enfant terrible" Jaroslav Tulach is writing a book on API design. It's looking pretty good and will certainly be interesting. Aside from teaching you everything you'll ever want to read about API design, it also presents an interesting history of several of the NetBeans APIs. You'll read why and how design decisions were made in NetBeans. You'll learn how to avoid the mistakes and benefit from all the wisdom of the API designers at NetBeans. Some great people are reviewing the book, so it should end up being a very good thing at the end.

Somewhere in the book, Jarda has a picture of the "Will Code HTML For Food" guy. it illustrates a point he's making in the book, about how programmers are needed everywhere, often regardless of skill. However, the publisher told him that the quality of the pic isn't very good, so yesterday Jan Chalupa took some pics of ourselves, so that we'd have a range of options to pick from, to replace the original low quality one. It was quite fun and we ended up with dozens of pics. Here are my personal favorites, one of each of the models that took part (Jaroslav Tulach, Tomas Stupka, David Simonek, and me):

The point of showing the above gallery of sad coders is that... they are here to inspire you because you can win a copy of Jarda's book, once it is released (sometime this year, probably). To win, get a pic such as the above taken of yourself. You need to look pathetic and sad. Or maybe not pathetic or sad, but edgy and toughened by life having punched you in the face. If your eyes are slightly crazed, reddened, and bulging, that's good too. Preferably you'll be displaying some weird or funny mismatch in clothing, corresponding to your supposed level of mental disarray. The background can be important too—if there's lots of graffiti in the background, for example, you could add a sense of desperation to the scene. Standing under a bridge also works. Rain would help as well. You'll need to be holding up a cardboard sign saying "Will Code HTML For Food" (or claiming you'll do something else programming-related for food). Then send it to me (geertjan DOT wielenga AT sun DOT com) and when the book is released the most pathetic picture will win a copy, signed by Jarda himself. Watch this space for further announcements and pics of early participants in this competition.

Wednesday Apr 16, 2008

org.openide.filesystems.FileChangeListener

How to listen to changes to a file, via the related NetBeans FileObject, and change the DataNode's display name, tooltip, and icon as a result:

public class DemoDataNode extends DataNode implements FileChangeListener {

    //The objects we are dealing with:
    private static final String ICON1 = "/org/netbeans/demofiletype/demo-1.png";
    private static final String ICON2 = "/org/netbeans/demofiletype/demo-2.png";
    DemoDataObject obj;
    Date date;
    String displayName;
    String tooltip;
    Image icon;
    int count = 0;

    //Constructor:
    public DemoDataNode(DemoDataObject obj) {
        super(obj, Children.LEAF);
        this.obj = obj;
        date = new Date();
    }

    //Constructor for receiving the lookup:
    DemoDataNode(DemoDataObject obj, Lookup lookup) {
        super(obj, Children.LEAF, lookup);
        //Add file change listener to the FileObject:
        obj.getPrimaryFile().addFileChangeListener(this);
        //Set default icon:
        setIconBaseWithExtension(ICON1);
        //Set default tooltip:
        setShortDescription("Hello world!");
    }

    @Override
    public String getDisplayName() {
        if (null != displayName) {
            return displayName;
        }
        return super.getDisplayName();
    }

    @Override
    public String getShortDescription() {
        if (null != tooltip) {
            return tooltip;
        }
        return super.getShortDescription();
    }

    @Override
    public Image getIcon(int arg0) {
        if (null != icon) {
            return icon;
        }
        return super.getIcon(arg0);
    }

    //When the file changes...
    @Override
    public void fileChanged(FileEvent arg0) {

        //Increment the count:
        count = count + 1;
        //Depending on the modulus, switch the icon:
        if (count % 2 == 1) {
            icon = Utilities.loadImage(ICON1);
        } else {
            icon = Utilities.loadImage(ICON2);
        }
        
        //Get the milliseconds and format it:
        long mills = System.currentTimeMillis();
        DateFormat dateFormatter = DateFormat.getDateTimeInstance(
                DateFormat.LONG,
                DateFormat.LONG);
        String formatted = dateFormatter.format(mills);

        //Save the current display name:
        String oldDisplayName = displayName;

        //Save the current tooltip:
        String oldShortDescription = tooltip;

        //Set the new display name:
        displayName = "Change " + count + " (" + formatted + ")";

        //Set the new tooltip:
        tooltip = formatted;

        //Fire change events on the node,
        //which will immediately refresh it with the new values:
        fireDisplayNameChange(oldDisplayName, displayName);
        fireShortDescriptionChange(oldShortDescription, tooltip);
        fireIconChange();
    }

    @Override
    public void fileFolderCreated(FileEvent arg0) {}
    @Override
    public void fileDataCreated(FileEvent arg0) {}
    @Override
    public void fileDeleted(FileEvent arg0) {}
    @Override
    public void fileRenamed(FileRenameEvent arg0) {}
    @Override
    public void fileAttributeChanged(FileAttributeEvent arg0) {}

}

When you change a document associated with the DataNode and DataObject referred to here, and you then save the document, the display name, tooltip, and icon will change.

Tuesday Apr 15, 2008

Statistics: They Can't Be Serious

I'm always sceptical about statistics, on many different levels. But, principally, my issue with them is not so much in the area of gathering (although there are MANY questions to be asked there too), but in the area of interpretation. Now, I'm not the first person to suggest that interpretation renders statistics meaningless, but it's not often that the evidence is as strong as it is with the latest set of statistics I've read about. Here's the interpretation of a set of statistics, as reported in today's Guardian:

Click here to read the whole story. However, in today's Dutch Volkskrant (as reported here), commenting on the same set of statistics, the article is headlined "John McEnroe Was Usually Wrong":

The evidence here is clear. These reporters had exactly the same statistics. But they interpreted them in completely opposite directions. On top of that, one even wonders if they read the same statistics because in the Dutch report, the line judges were right in 61% of cases, while in the Guardian the line judges were wrong in 40% of them. But... that's the same thing!!! Yet... interpreted in such a way that completely different meanings are rendered. To the Guardian, the statistics support John McEnroe's objections; to the Volkskrant, the statistics undermine them.

If there's one thing that these statistics prove, it is that it pays to be multilingual and it pays to read more than one newspaper. Especially when it comes to statistics. Still, it would be nice to know whether John McEnroe was mostly right or mostly wrong. At the time, I thought he was mostly wrong, but only because I was 12 at the time and assumed that authority has special access to truth. Since then, I've learned that that's really a pretty bad assumption. How cool would it be if history were to prove that John McEnroe had been right all along? Even though he probably never thought so himself and merely enjoyed throwing tantrums on TV?

Monday Apr 14, 2008

Step Onto the NetBeans Platform... in Japanese!

The 4-part NetBeans Platform "Selection Management Series", by Tim Boudreau, is now available in Japanese:

Here's the intro:

Note: The ultimate NetBeans plugin quick start is also available in Japanese:

http://platform.netbeans.org/tutorials/60/nbm-google_ja.html

Thanks very much to Masaki Katakai and the community member who worked on this translation!

org.openide.util.ChangeSupport

I'm learning about org.openide.util.ChangeSupport. Some notes below.

In the visual panel:

private final ChangeSupport changeSupport = new ChangeSupport(this);

private final DocumentListener docListener = new DocumentListener() {
    public void insertUpdate(DocumentEvent e) {
        fireChange();
    }

    public void removeUpdate(DocumentEvent e) {
        fireChange();
    }

    public void changedUpdate(DocumentEvent e) {
        fireChange();
    }
    private void fireChange() {
        changeSupport.fireChange();
    }
};

In the visual panel constructor:

field1.getDocument().addDocumentListener(docListener);
field2.getDocument().addDocumentListener(docListener);
changeSupport.addChangeListener(panel);

In the wizard panel, implement ChangeListener and then:

private ChangeSupport changeSupport = new ChangeSupport(this);

@Override
public final void addChangeListener(ChangeListener l) {
    changeSupport.addChangeListener(l);
}

@Override
public final void removeChangeListener(ChangeListener l) {
    changeSupport.removeChangeListener(l);
}

public void stateChanged(ChangeEvent e) {
    changeSupport.fireChange();
}

Then an experiment with DataObject and a DataNode. This in the DataObject:

private final ChangeSupport changeSupport = new ChangeSupport(this);

private final FileChangeListener fileListener = new FileChangeListener() {

    public void fileFolderCreated(FileEvent arg0) {
    }

    public void fileDataCreated(FileEvent arg0) {
    }

    public void fileChanged(FileEvent arg0) {
        changeSupport.fireChange();
    }

    public void fileDeleted(FileEvent arg0) {
    }

    public void fileRenamed(FileRenameEvent arg0) {
    }

    public void fileAttributeChanged(FileAttributeEvent arg0) {
    }
};

And in the constructor of the DataObject:

getPrimaryFile().addFileChangeListener(fileListener);
changeSupport.addChangeListener(createNodeDelegate());

This in the DataNode:

public class DemoDataNode extends DataNode implements ChangeListener {

    private ChangeSupport changeSupport = new ChangeSupport(this);
    DemoDataObject obj;
    Date date;

    public DemoDataNode(DemoDataObject obj) {
        super(obj, Children.LEAF);
        this.obj = obj;
        date = new Date();
        changeSupport.addChangeListener(this);
    }

    DemoDataNode(DemoDataObject obj, Lookup lookup) {
        super(obj, Children.LEAF, lookup);
        setName(obj.getPrimaryFile().getName());
    }

    public void stateChanged(ChangeEvent e) {
        long mills = System.currentTimeMillis();
        DateFormat dateFormatter = DateFormat.getDateTimeInstance(
                DateFormat.LONG,
                DateFormat.LONG);
        String formatted = dateFormatter.format(mills);
        setName("Changed: " + formatted);
        changeSupport.fireChange();
    }

}

Something is wrong because I can only change the name, not the display name or the icon. Probably need to do something with fireIconChange and fireDisplayNameChange on the Node.

Update. Fixed the problem. Some of the code above is wrong. Will blog about this soon.

Saturday Apr 12, 2008

Expandable File Nodes Displaying Parsed File Content

The only thing that should have been, potentially, interesting about yesterday's screenshot of a config file editor was that explorer view that showed an expanded node representing a file, with subnodes matching the content of the file. So, here I'll show how to create that effect, in this case for project.xml files:

I believe that knowing how to do this is going to be extremely useful for a lot of developers, especially those providing some kind of support for XML files, such as if you're creating web framework support, for example, or anything else involving XML files. Here are the steps, with all the code:

  1. Create a module project.

  2. Use the New File wizard, specifying the XML file's MIME type and namespace. When you finish the wizard, look in the MIME resolver. In my case, i.e., for project.xml files, I needed exactly this content in the MIME resolver, note especially the section in bold:

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    To change this template, choose Tools | Templates
    and open the template in the editor.
    -->
    <!DOCTYPE MIME-resolver PUBLIC "-//NetBeans//DTD MIME Resolver 1.0//EN" 
                                   "http://www.netbeans.org/dtds/mime-resolver-1_0.dtd">
    <MIME-resolver>
        <file>
            <ext name="xml"/>
            <resolver mime="text/x-project+xml">
                <xml-rule>
                    <element ns="http://www.netbeans.org/ns/project/1"/>
                </xml-rule>
            </resolver>
        </file>
    </MIME-resolver>
    

  3. Next, let's use the DataObject class to parse the file and expose the result. Add the following to the generated DataObject class:

    private Map<String, List<String>> entries;
    
    //We will call this method from our DataNode.
    //When we do so, we parse the project.xml file
    //and return org.w3c.dom.Node names to the DataNode:
    synchronized Map<String, List<String>> getEntries() {
        if (entries == null) {
            parse();
        }
        return entries;
    }
    
    private void parse() {
        try {
            entries = new LinkedHashMap<String, List<String>>();
            List sectionEntries = null;
            BufferedReader br = null;
            //Use the FileObject retrieved from the DataObject,
            //via DataObject.getPrimaryFile(), to get the input stream:
            br = new BufferedReader(new InputStreamReader(getPrimaryFile().getInputStream()));
            InputSource source = new InputSource(br);
            //You could use any kind of parser, depending on your file type,
            //though for XML files you can use the NetBeans IDE org.openide.xml.XMLUtil class
            //to convert your input source to a org.w3c.dom.Document object:
            org.w3c.dom.Document doc = XMLUtil.parse(source, false, false, null, null);
            org.w3c.dom.NodeList list = doc.getElementsByTagName("\*");
            int length = list.getLength();
            for (int i = 0; i < length; i++) {
                org.w3c.dom.Node mainNode = list.item(i);
                String value = mainNode.getNodeName();
                //For purposes of this example, we simply put
                //the name of the node in our linked hashmap:
                entries.put(value, sectionEntries);
            }
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        } catch (SAXException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

  4. Next, in the generated DataNode, lets create a controller for triggering the parsing and for creating a child node per node name:

    static class SectionChildren extends Children.Keys {
    
        private ProjectDataObject obj;
        private Lookup lookup;
    
        private SectionChildren(ProjectDataObject obj, Lookup lookup) {
            this.obj = obj;
            this.lookup = lookup;
        }
    
        //Called the first time that a list of nodes is needed.
        //An example of this is when the node is expanded.
        @Override
        protected void addNotify() {
            setKeys(obj.getEntries().keySet());
        }
    
        //Called when the user collapses a node and starts working on 
        //something else. The NetBeans Platform will notice that the list 
        //of nodes is no longer needed, and it will free up the memory that 
        //is no longer being used. 
        @Override
        protected void removeNotify() {
            setKeys(Collections.emptyList());
        }
     
        //Called whenever a child node needs to be constructed.
        @Override
        protected Node[] createNodes(String key) {
            return new Node[]{new SectionNode(key, obj.getEntries().get(key), lookup)};
        }
    }
    
    static class SectionNode extends AbstractNode {
    
        SectionNode(String name, java.util.List entries, Lookup lookup) {
            super(Children.LEAF);
            setName(name);
            setIconBaseWithExtension(ENTRY_IMAGE_ICON_BASE);
        }
    
    }

    For further info on the above code, see NetBeans System Properties Module Tutorial.

  5. Finally, you might be wondering how to trigger the above code. In other words, when/how should you determine that new child nodes should be created? Answer: rewrite the DataNode constructors:

    public ProjectDataNode(ProjectDataObject obj) {
        super(obj, new SectionChildren(obj, null));
        setIconBaseWithExtension(SECTION_IMAGE_ICON_BASE);
    }
    
    ProjectDataNode(ProjectDataObject obj, Lookup lookup) {
        super(obj, new SectionChildren(obj, lookup), lookup);
        setIconBaseWithExtension(SECTION_IMAGE_ICON_BASE);
    }

    The bits in bold above are what I added to trigger the creating of the child nodes.

And that's all. Now you can install your module and expand the node of the file type that you're interested in. Using the parse method above, you can parse the content in any way you like, so that when the DataNode creates new section nodes, the data that you've parsed will be used to set the display name (or something else) on the child nodes. Here, for example, I've parsed the project.xml file in such a way that all the module dependencies appear as the display names of the child nodes:

You can also add child nodes below the existing child nodes, and so on.

Configuration File Editor

Friday Apr 11, 2008

Injecting a Layer File into a NetBeans Platform Application

I tried the How to change menus, etc. after login? scenario today: "Since version 7.1 there is a way to change the content of system file system in a dynamic way. As system file systems contains various definitions (in NetBeans Platform menus, toolbars, layout of windows, etc.) it de-facto allows global change to these settings for example when user logs into some system."

The outline of what you need to do is described there, nothing I can really add. It is one new and cool way in which one module can contribute to an application. You start by creating a new module, add the XML layer defining the items you want to register in the system, then implement FileSystem, and export it to META-INF/services.

One scenario is that of a user logging into an application. On successful login, the layer file is injected and whatever is defined in the layer file is added to the application. I added a small touch—whatever the user sets as their user name determines the text in a new menu item that is added via the injected layer file. That's possible via the NbPreferences class, which can store preferences in the application's user directory.

Here's the whole module:

In the Installer, the login dialog is shown (as described elsewhere in this blog):

package org.yourorghere.addedsfs;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.net.MalformedURLException;
import java.net.URL;
import javax.swing.JButton;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.modules.ModuleInstall;
import org.openide.util.Exceptions;
import org.openide.util.NbPreferences;
import org.xml.sax.SAXException;

public class Installer extends ModuleInstall {

    JButton ok = new JButton();
    JButton cancel = new JButton();
    NotifyDescriptor.InputLine msg;

    public Installer() {
        ok.addActionListener(new OkActionListener());
    }

    private class OkActionListener implements ActionListener {
        //In real life, you'd have some login validation here!
        public void actionPerformed(ActionEvent e) {
            try {
                //Create a preference "USER_KEY", with value set to the input text:
                NbPreferences.forModule(Installer.class).put("USER_KEY", msg.getInputText());
                //Specify the XML file that defines the layer you want to inject:
                URL u = new URL("nbresloc:/org/yourorghere/addedsfs/newLayer.xml");
                //Pass the URL to the LoginFileSystem:
                LoginFileSystem.assignURL(u);
            } catch (SAXException ex) {
                Exceptions.printStackTrace(ex);
            } catch (MalformedURLException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }

    @Override
    public void restored() {
        ok.setText("OK");
        cancel.setText("Cancel");
        msg = new NotifyDescriptor.InputLine("Login:", "User name: ",
                NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.QUESTION_MESSAGE);
        msg.setOptions(new Object[]{ok, cancel});
        DialogDisplayer.getDefault().notifyLater(msg);
    }

}

And here's the LoginFileSystem (which is called by means of LoginFileSystem.assignURL(u) above):

package org.yourorghere.addedsfs;

import java.net.URL;
import org.openide.filesystems.MultiFileSystem;
import org.openide.filesystems.XMLFileSystem;
import org.xml.sax.SAXException;

public class LoginFileSystem extends MultiFileSystem {

    private static LoginFileSystem INSTANCE;

    public LoginFileSystem() {
        // let's create the filesystem empty, because the user
        // is not yet logged in
        INSTANCE = this;
    }

    public static void assignURL(URL u) throws SAXException {

        //   Alternatively:
        //   Lookup lookup = Lookup.getDefault();
        //   LoginFileSystem INSTANCE = lookup.lookup(LoginFileSystem.class);

        INSTANCE.setDelegates(new XMLFileSystem(u));
    }
    
}

So then the URL that points to the newLayer.xml is used to create a new XMLFileSystem. The above class should be exported to META-INF/services, following the JDK 6 java.util.ServiceLoader approach.

And what about the preference that the user entered? Here you see the definition of the TestAction.getName method:

public String getName() {
    Preferences pref = NbPreferences.forModule(TestAction.class);
    String name = pref.get("USER_KEY", "");
    return NbBundle.getMessage(TestAction.class, "CTL_TestAction") + name + "!";
}

So first some text (like "Hello") is retrieved from the bundle file, appended with the name that the user entered, appended with an exclamation mark. And the above menu item only appears if the login succeeds, which you'd need to make possible via hooking up your database of users to the module so that whatever the user enters can be verified against your database.

In other news. What technology evangelists really do for a living.

Thursday Apr 10, 2008

JFugue in an Applet Deployed in a JNLP File

Here is a JEditorPane in an applet. I can type in JFugue notation and press Play and then the JFugue API is used to play the notes in the background via the JDK 6 SwingWorker class, so that you can scroll in the JEditorPane while the music is playing:

That's just a demo of any old applet. That's not the point of this blog entry. The point is the deployment of the notoriously clunky applet technology. Let's take a look at the applet's tags in the JSP file that launches it. Take note especially of line 21, which sets the JNLP file that actually launches the applet:

Also take note of the fact that you can set JVM arguments... on a per-applet basis! And here's the JNLP file, with line 11 specifying the location of the JAR, which used to be done in the applet tag, above. So, clearly, the JNLP file has taken over the responsibility of loading the applet JAR:

And performance? Couldn't be better. What about those wacky dialogs that you'd get whenever you load an applet? And the freezing of the browser? Forget about those, file them away as history. Seriously. None of that's anywhere to be seen anymore. All you need is JDK 6 Update 10 (Beta, or some dev build), together with FireFox 3 (also Beta, or some dev build). All this and more, together with a list of demos you can look at once you're all set up, in Applets Reloaded: An Overview of New Tags and Demos on Javalobby.

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2008 »
SunMonTueWedThuFriSat
  
2
4
5
13
18
19
23
30
   
       
Today