X

Geertjan's Blog

  • June 27, 2012

Annotation Processor for Superclass Sensitive Actions

Geertjan Wielenga
Product Manager

Someone creating superclass sensitive actions should need to specify only the following things:

  • The condition under which the popup menu item should be available, i.e., the condition under which the action is relevant. And, for superclass sensitive actions, the condition is the name of a superclass. I.e., if I'm creating an action that should only be invokable if the class implements "org.openide.windows.TopComponent",  then that fully qualified name is the condition.
  • The position in the list of Java class popup menus where the new menu item should be found, relative to the existing menu items.
  • The display name.
  • The path to the action folder where the new action is registered in the Central Registry.
  • The code that should be executed when the action is invoked.

In other words, the code for the enablement (which, in this case, means the visibility of the popup menu item when you right-click on the Java class) should be handled generically, under the hood, and not every time all over again in each action that needs this special kind of enablement.

So, here's the usage of my newly created @SuperclassBasedActionAnnotation, where you should note that the DataObject must be in the Lookup, since the action will only be available to be invoked when you right-click on a Java source file (i.e., text/x-java) in an explorer view:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import org.netbeans.sbas.annotations.SuperclassBasedActionAnnotation;
import org.openide.awt.StatusDisplayer;
import org.openide.loaders.DataObject;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
@SuperclassBasedActionAnnotation(
position=30,
displayName="#CTL_BrandTopComponentAction",
path="File",
type="org.openide.windows.TopComponent")
@NbBundle.Messages("CTL_BrandTopComponentAction=Brand")
public class BrandTopComponentAction implements ActionListener {
private final DataObject context;
public BrandTopComponentAction() {
context = Utilities.actionsGlobalContext().lookup(DataObject.class);
}
@Override
public void actionPerformed(ActionEvent ev) {
String message = context.getPrimaryFile().getPath();
StatusDisplayer.getDefault().setStatusText(message);
}
}

That implies I've created (in a separate module to where it is used) a new annotation. Here's the definition:

package org.netbeans.sbas.annotations;
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 SuperclassBasedActionAnnotation {
String type();
String path();
int position();
String displayName();
}

And here's the processor:

package org.netbeans.sbas.annotations;
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.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.netbeans.sbas.annotations.SuperclassBasedActionAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class SuperclassBasedActionProcessor extends LayerGeneratingProcessor {
@Override
protected boolean handleProcess(Set annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
Elements elements = processingEnv.getElementUtils();
for (Element e : roundEnv.getElementsAnnotatedWith(SuperclassBasedActionAnnotation.class)) {
TypeElement clazz = (TypeElement) e;
SuperclassBasedActionAnnotation mpm = clazz.getAnnotation(SuperclassBasedActionAnnotation.class);
String teName = elements.getBinaryName(clazz).toString();
String originalFile = "Actions/" + mpm.path() + "/" + teName.replace('.', '-') + ".instance";
File actionFile = layer(e).file(
originalFile).
bundlevalue("displayName", mpm.displayName()).
methodvalue("instanceCreate", "org.netbeans.sbas.annotations.SuperclassSensitiveAction", "create").
stringvalue("type", mpm.type()).
newvalue("delegate", teName);
actionFile.write();
File javaPopupFile = layer(e).file(
"Loaders/text/x-java/Actions/" + teName.replace('.', '-') + ".shadow").
stringvalue("originalFile", originalFile).
intvalue("position", mpm.position());
javaPopupFile.write();
}
return true;
}
}

The "SuperclassSensitiveAction" referred to in the code above is unchanged from how I had it in yesterday's blog entry.

When I build the module containing two action listeners that use my new annotation, the generated layer file looks as follows, which is identical to the layer file entries I hard coded yesterday:

<folder name="Actions">
<folder name="File">
<file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance">
<attr name="displayName" stringvalue="Process Action Listener"/>
<attr methodvalue="org.netbeans.sbas.annotations.SuperclassSensitiveAction.create" name="instanceCreate"/>
<attr name="type" stringvalue="java.awt.event.ActionListener"/>
<attr name="delegate" newvalue="org.netbeans.sbas.impl.ActionListenerSensitiveAction"/>
</file>
<file name="org-netbeans-sbas-impl-BrandTopComponentAction.instance">
<attr bundlevalue="org.netbeans.sbas.impl.Bundle#CTL_BrandTopComponentAction" name="displayName"/>
<attr methodvalue="org.netbeans.sbas.annotations.SuperclassSensitiveAction.create" name="instanceCreate"/>
<attr name="type" stringvalue="org.openide.windows.TopComponent"/>
<attr name="delegate" newvalue="org.netbeans.sbas.impl.BrandTopComponentAction"/>
</file>
</folder>
</folder>
<folder name="Loaders">
<folder name="text">
<folder name="x-java">
<folder name="Actions">
<file name="org-netbeans-sbas-impl-ActionListenerSensitiveAction.shadow">
<attr name="originalFile" stringvalue="Actions/File/org-netbeans-sbas-impl-ActionListenerSensitiveAction.instance"/>
<attr intvalue="10" name="position"/>
</file>
<file name="org-netbeans-sbas-impl-BrandTopComponentAction.shadow">
<attr name="originalFile" stringvalue="Actions/File/org-netbeans-sbas-impl-BrandTopComponentAction.instance"/>
<attr intvalue="30" name="position"/>
</file>
</folder>
</folder>
</folder>
</folder>

Join the discussion

Comments ( 6 )
  • Michal Margiel Friday, July 6, 2012

    Hello,

    I followed you example but I can't get ANY custom processor running.. Trying to put some "debug strings" in the code but nothing... I just see:

    Note: Attempting to workaround javac bug #6512707

    Note: eu.margiel.SuperclassBasedActionProcessor to be registered as a javax.annotation.processing.Processor

    warning: No processor claimed any of these annotations: [javax.annotation.processing.SupportedSourceVersion, javax.annotation.processing.SupportedAnnotationTypes, eu.margiel.SuperclassBasedActionAnnotation]

    is there any precondition that I am missing or anything like this?


  • Geertjan Friday, July 6, 2012

    The annotation needs to be defined in one module. In another module, use the annotation. Don't define and use within the same module.


  • Michal Margiel Friday, July 6, 2012

    now it worked. thanks, I think that this info should be somwhere inside this post (I couldn't find it anywhere).

    anyway - I have one more question, I wanna use my custom LayerGeneratingProcessor to generate Top Menu (so layer.xml) accordingly to User roles - which can change while using our app on some actions.

    Is it possible to regenerate layer.xml during runtime? Now we are doing that by loading multiple layer files - but I hate this solution since it is very messy. I would like to have kind of builder where in one place I would like to define which menus are available to which roles.

    Something like...

    @MenuBuilder

    public class MyMenuBuilder{

    public Menu create(){

    return new Menu()

    .item(new MenuItem("My Item", Action1.class).forRoles(ADMIN))

    .item(new MenuItem("My Item 2", Action1.class).forRoles(CONTRIBUTOR));

    }

    }


  • Michal Margiel Tuesday, July 10, 2012

    Any idea how I can achieve that?


  • Geertjan Tuesday, July 10, 2012

    Don't think that's possible. I'd recommend writing to dev@platform.netbeans.org with this and other questions, sorry, don't know the answer to this.


  • listyo Monday, July 30, 2012

    I still confused about your opinion "When I build the module containing two action listeners that use my new annotation, the generated layer file looks as follows".

    Can you describe what this exactly,because I have try your example but I can't get it running,this makes me wonder how it can works,since I have tried for 2 nights,

    regard,

    listyo

    http://www.advantoday.com


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.