How to Create an Action Annotation for NetBeans Platform Applications

Let's use org.openide.filesystems.annotations.LayerGeneratingProcessor for the first time. We will use it to process an @Action annotation which we will be able to set on ActionListeners in NetBeans Platform applications, as shown yesterday.

Here's the definition of my @Action annotation, with Javadoc for "path()", so that you can see how, in yesterday's blog entry, the second screenshot was able to show Javadoc.

package org.demo.action.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Action {

    int position() default Integer.MAX_VALUE;

    String displayName();

    /\*\*
     \* The path to the folder where this action will be registered.
     \* The menu item and/or toolbar button will be registered
     \* in the same folder. For example, if "Edit" is returned,
     \* the action will be registered in (at least) "Actions/Edit",
     \* as well as, optionally, "Menu/Edit" and "Toolbars/Edit".
     \* @return String (default is "File")
     \*/
    String path() default "File";

    String iconBase() default "";

    boolean menuBar() default false;

    boolean toolBar() default false;
    
}

Take note that the @Target is set to ElementType.TYPE. That means that the @Action annotation will be settable on the class level, i.e., as opposed to method level or field level or something else. Also note that @Retention is set to RetentionPolicy.SOURCE, which means that the the @Action annotation will be processed at compile time, i.e., as opposed to runtime.

So, now, I can annotate classes like this:

package org.demo.action;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import org.demo.action.annotation.Action;

@Action(position = 1, displayName = "#key",
menuBar = true, toolBar = true, iconBase = "org/demo/action/icon.png")
public class DemoActionListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
        System.out.println("hello world");
    }
}

When I build the module that contains the above ActionListener... guess what? In build/classes/META-INF, I see an XML file named "generated-layer.xml", which has all of the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.2//EN" "http://www.netbeans.org/dtds/filesystem-1_2.dtd">
<filesystem>
    <folder name="Actions">
        <folder name="File">
            <file name="org-demo-action-DemoActionListener.instance">
                <attr name="delegate" newvalue="org.demo.action.DemoActionListener"/>
                <attr bundlevalue="org.demo.action.Bundle#key" name="displayName"/>
                <attr name="iconBase" stringvalue="org/demo/action/icon.png"/>
                <attr methodvalue="org.openide.awt.Actions.alwaysEnabled" name="instanceCreate"/>
            </file>
        </folder>
    </folder>
    <folder name="Menu">
        <folder name="File">
            <file name="org-demo-action-DemoActionListener.shadow">
                <attr name="originalFile" stringvalue="Actions/File/org-demo-action-DemoActionListener.instance"/>
                <attr intvalue="1" name="position"/>
            </file>
        </folder>
    </folder>
    <folder name="Toolbars">
        <folder name="File">
            <file name="org-demo-action-DemoActionListener.shadow">
                <attr name="originalFile" stringvalue="Actions/File/org-demo-action-DemoActionListener.instance"/>
                <attr intvalue="1" name="position"/>
            </file>
        </folder>
    </folder>
</filesystem>

So... ALL of the XML above, i.e., the file as well as the content, was generated when I built the module containing the ActionListener shown earlier. I.e., the annotations on that specific ActionListener resulted in the above specific XML content in build/classes/META-INF. (And if I had 10 ActionListeners or 100 ActionListeners or any other number, they'd all be processed in the same way, if I add an @Action annotation to the class declaration, as shown above.)

All of this is made possible by... the annotation processor that processes the @Action annotation. This particular annotation processor uses several NetBeans API annotation processor classes, which enables the XML layer file to be created, with the content shown above.

And here it is:

package org.demo.action.annotation.impl;

import java.util.Set;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.demo.action.annotation.Action;
import org.openide.filesystems.annotations.LayerBuilder.File;
import org.openide.filesystems.annotations.LayerGeneratingProcessor;
import org.openide.filesystems.annotations.LayerGenerationException;
import org.openide.util.lookup.ServiceProvider;

@ServiceProvider(service = Processor.class)
@SupportedAnnotationTypes("org.demo.action.annotation.Action")
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class ActionProcessor extends LayerGeneratingProcessor {

    @Override
    protected boolean handleProcess(
            Set set, RoundEnvironment env) 
            throws LayerGenerationException {
        Elements elements = processingEnv.getElementUtils();

        for (Element e : env.getElementsAnnotatedWith(Action.class)) {

            TypeElement clazz = (TypeElement) e;
            Action action = clazz.getAnnotation(Action.class);
            String teName = elements.getBinaryName(clazz).toString();

            File f = layer(e).file(
                    "Actions/" + action.path() + teName.replace('.', '-') + ".instance");
            f.newvalue(
                    "delegate",
                    teName);
            f.bundlevalue(
                    "displayName",
                    action.displayName());
            f.stringvalue(
                    "iconBase",
                    action.iconBase());
            f.methodvalue(
                    "instanceCreate",
                    "org.openide.awt.Actions",
                    "alwaysEnabled");
            f.write();

            if (action.menuBar() == true && action.toolBar() == true) {
                writeDisplayLocation(e, teName, action, "Menu/");
                writeDisplayLocation(e, teName, action, "Toolbars/");
            } else if (action.menuBar() == true && action.toolBar() == false) {
                writeDisplayLocation(e, teName, action, "Menu/");
            } else if (action.menuBar() == false && action.toolBar() == true) {
                writeDisplayLocation(e, teName, action, "Toolbars/");
            }

        }

        return true;
    }

    private void writeDisplayLocation(Element e, String teName, Action action, String loc) {
        File f1 = layer(e).shadowFile(teName, loc + action.path(), teName.replace('.', '-'));
        f1.stringvalue(
                "originalFile",
                "Actions/" + action.path() + teName.replace('.', '-') + ".instance");
        f1.intvalue(
                "position",
                action.position());
        f1.write();
    }

}

By putting the @Action annotation class and the ActionProcessor class into a separate module, I am able to reuse the @Action annotation in any module I like... by setting a dependency from the module/s containing my ActionListeners onto the module providing the @Action annotation. So long as I have exposed the package containing the @Action annotation, I can use it, like any other annotation.

And the processor, which is in the same module as the @Action annotation, will process any class that makes use of the @Action annotation... at compile time, hence creating the XML layer file which is then available at runtime.

Go here for Jaroslav Tulach's more detailed version of the above. (I wrote the above before looking at his version, I promise! In fact, I wrote it in the train from Bonn to Amsterdam yesterday.)

Comments:

Mine and yours version are similar. I take it as a proof that writing compile time annotations processors is a piece of cake!

Posted by Jaroslav Tulach on August 16, 2009 at 08:45 PM PDT #

f.newvalue("delegate", teName) could more easily be written as f.instanceAttribute("delegate", ActionListener.class) which also handles static methods and does some error checking for you.

The 3-way if-clause could more sensibly be written as two independent if-clauses.

f1.intvalue("position", action.position()) is wrong. Use f1.position(action.position()).

The call to shadowFile plus manual set of "originalFile" attr is also wrong. I think you meant simply shadowFile("Actions/" + action.path() + teName.replace('.', '-') + ".instance", loc + action.path(), null). In fact explicitly computing the target file's path here is brittle and unnecessary, since you already created that file and can just use getPath now.

BTW since the various methods use the builder pattern, you do not need a local variable to represent the File in most cases.

Currently there is not a convenience method to get a useful name for a file, which is irritating since instanceFile picks a good name but you want to use instanceAttribute here. So you currently do need to compute teName (in the future I hope this will become unnecessary). Still, your example could be stripped down to the following, after teName is calculated:

File f = layer(e).file("Actions/" + action.path() + teName.replace('.', '-') + ".instance").instanceAttribute("delegate", ActionListener.class).bundlevalue("displayName", action.displayName()).stringvalue("iconBase", action.iconBase()).methodvalue("instanceCreate", "org.openide.awt.Actions", "alwaysEnabled");
f.write();
if (action.menuBar() == true) {
layer(e).shadowFile(f.getPath(), "Menu/" + action.path(), null).position(action.position()).write();
if (action.toolBar() == true) {
layer(e).shadowFile(f.getPath(), "Toolbars/" + action.path(), null).position(action.position()).write();
}

Of course having separate attributes to turn on a menu item and to give its path (ditto for toolbars) is a little clumsy, and assuming that the same path will work for the actions pool, menu, and toolbar is not likely to work for long. Certainly assuming that the same position will work for both menu and toolbar is unrealistic. So the annotation definition would have to be reworked to be usable.

There is of course the idea to define a general action GUI registration annotation in a supported API, but there are a lot of tricky details to work out first, so I don't know if we can expect anything in 6.8.

Posted by Jesse Glick on August 17, 2009 at 02:01 AM PDT #

This looks pretty cool... Will the Action annotation be soon part of the platform?

Posted by Thomas Wuerthinger on August 17, 2009 at 02:46 PM PDT #

http://netbeans.org/bugzilla/show_bug.cgi?id=183794

Posted by Jesse Glick on August 04, 2010 at 10:23 AM PDT #

So to clarify my last comment: @ActionRegistration will be part of the NB Platform 7.0. It should be easy to switch to it: there is an editor hint available in your layer.xml to replace the manual registration with the annotation.

Posted by Jesse Glick on October 26, 2010 at 02:23 AM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
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 2014
SunMonTueWedThuFriSat
  
12
13
14
24
25
26
27
28
29
30
   
       
Today