Ioannis Kostaras1, Constantin Drabo2, Josh Juneau3, Sven Reimers4, Mario Schröder5 and Geertjan Wielenga6
(1)
The Hague, South Holland, The Netherlands
(2)
Ouagadougou, Burkina Faso
(3)
Chicago, IL, USA
(4)
Salem, Germany
(5)
Berlin, Germany
(6)
Amsterdam, The Netherlands
Apache NetBeans is a modular IDE, and therefore, it is very extensible. Not only is it easy to extend the IDE, but it is also very easy to develop entire desktop rich applications utilizing the NetBeans Platform. In this chapter, we will discuss how to build modules a.k.a. “Plugins” so that the IDE can be extended to encompass new functionality. For instance, new programming languages are being created all the time, and it is easy to extend Apache NetBeans to support new languages by developing a new plugin.
Modules are typically added to the IDE as plugins. In Apache NetBeans, the Plugins menu allows one to manage the modules that are available, installed, or downloaded to the machine, such that they can be loaded or unloaded to affect the way that Apache NetBeans appears or functions. There is an online portal (https://netbeans.apache.org/plugins/index.html) containing plugins that have been developed by the community. One can visit the portal to search available plugins for download. If you develop a plugin, you can make it available on the plugin portal for all to use.
In this chapter we will walk through a tutorial about creating a plugin for NetBeans. We will focus on a blockchain use case. See the source code repository of the book as a reference.
Purpose of the Plugin
We are going to create a NetBeans plugin for a blockchain implementation. Apart from support to set up a new blockchain project from scratch, you will also learn how to create features to support a new language:
recognize the model file
syntax highlighting
error detection
code completion
navigation view of the model file
You will also see how to sign and share the plugin with other users of NetBeans.
Introduction to Blockchain
A blockchain is a decentralized peer-to-peer network. Each participant of this network has a copy of a shared ledger, where data can only be appended by digitally signed transactions. The blockchain guarantees the immutability of the ledger.
A block contains an amount of transactions and some metadata. The transactions and the block are unique, identified by a hash. The block also contains a hash of the chronological previous block. The first block in the chain, which has no predecessor, is called Genesis-Block. Together those blocks build the chain. Figure 11-1 shows two blocks with their hashes and the hash of the previous block.
A blockchain can be distinguished between public and private. A typical example of a public blockchain is Ethereum. A public blockchain has built in mechanisms, Proof of Work or Proof of Stake, to prevent fraudulent actions. An example of a private blockchain is Hyperledger. This open source project is an effort to advance blockchain technologies, hosted by the Linux Foundation. The term private means that the access to the network is restricted. Every node has to authenticate itself, so there is no need to have the abovementioned mechanisms against fraudulent attacks and the sensitive information stays within the network. Check out the website www.hyperledger.org if you would like to dive deeper into Hyperledger.
When the authors started to write this book, the recommended way to set up a new Hyperledger project was to use the Composer:https://hyperledger.github.io/composer/latest/. A Hyperledger project contains a model file, which is written in the Hyperledger Composer Language. Those files end with a cto suffix, and we will refer to them as cto files in this chapter. The language defines resources, which are used in the business network:
asset: represents a valuable object, for instance, a house;
participant: stands for the parties involved in the business;
transaction: submitted by a participant, it is a state change that affects the asset;
enum: a collection of values as you know it from Java;
event: can be emitted from transaction functions, and applications can subscribe to events.
The language has a collection of primitive types for field declaration in the resources:
Boolean: a Boolean value, either true or false;
String: an UTF8 encoded string;
Integer: 32-bit signed number;
Long: 64-bit signed number;
Double: double precision 64-bit numeric value;
DateTime: an ISO-8601 compatible time instance, with optional time zone and UTC offset.
The Hyperledger Composer Language does not define the rules for a transaction; this logic is part of another file, written in JavaScript.
You should also install the Hyperledger Composer Playground, which provides a user interface for the configuration, deployment, and testing of a business network.
? Do you want to generate an empty template network? No: generate a populated sample network
create package.json
create README.md
create models/org.example.biznet.cto
create permissions.acl
create .eslintrc.yml
create features/sample.feature
create features/support/index.js
create test/logic.js
create lib/logic.js
Create a New Module
Under the hood we are using Maven as a build tool. Start a new Module with: “File” ➤ “New Project” ➤ “Java with Maven” ➤ “NetBeans Module”. Figure 11-2 shows NetBeans’ project wizard.
Next, give your project a meaningful name like ‘ledger’ and then select your target platform, you can start with RELEASE82. Click on Finish to complete the action.
We will add test dependencies. Select the project node ‘Test Dependencies’, right-click, and choose ‘Add Dependency’. This will bring up the dialog, shown in Figure 11-3.
Change the scope to test, untick ‘Only NetBeans’, and use the query field for the following dependencies:
org.junit.jupiter:junit-jupiter-api:5.5.1
org.junit.jupiter:junit-jupiter-engine:5.5.1
org.hamcrest:hamcrest-core:2.1
org.mockito:mockito-junit-jupiter:2.28.2
To enable an incubating feature in Mockito, which creates mocks for final classes, add a new file src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker with a single line:
mock-maker-inline
For more information about the Mockito library, see the following link:
All Maven dependencies that are related to NetBeans will have a tag that shows the version which you are using to build the plugin. It’s advisable to add a new property that holds this version, which gives you an easy way to change the version later.
The business network archive contains several files. For instance, there is a JavaScript file that is used to define a business rule that applies to a transaction. A model file defines the basic concept involved in the business. To avoid creating each one of those files manually for a new project, we are going to create a new project type, which will appear in the wizard for projects.
We start with a new Node.js Application, which will serve as the source folder for our template. So again, start the wizard and create a new project as shown in Figure 11-4.
Remove the directory private under nbproject, as well as the main.js. Copy the source files from the business network, which we created in the previous section, into the src directory of this project.
To have support for publishing the business network from NetBeans, edit the script section of the package.json in the IDE:
"scripts": {
"publish": "mkdirp ./dist && composer archive create --sourceType dir --sourceName . -a ./dist/ledger.bna",
"pretest": "npm run lint",
"lint": "eslint .",
"test": "nyc mocha -t 0 test/*.js && cucumber-js"
}
Switch to the Files window in the IDE, and create a new .gitignore file with this content:
dist/
node_modules/
Edit the project.properties within the nbproject folder.
The folder for the zip file should look like this directory structure:
.
├── README.md
├── nbproject
│ ├── project.properties
│ └── project.xml
├── features
│ ├── sample.feature
│ └── support
│ └── index.js
├── lib
│ └── logic.js
├── models
│ └── org.example.biznet.cto
├── package.json
├── permissions.acl
└── test
└── logic.js
Let’s build a template, which is based on this project. Select the project in NetBeans’ Projects window, and right-click. Then create a new project template: “New File” ➤ “Module Development” ➤ “Project Template.” See Figure 11-5.
Select the previously created Node.js project (Figure 11-6).
Click “Next,” enter some meaningful name, use a package with the name “template,” and click Finish. See Figure 11-7.
This process will add some dependencies to the pom.xml, create several new classes, some resources, and also a zip file with the content from the other Node.js project.
The starting point for the project template is the annotation TemplateRegistration for the HyperledgerWizardIterator. Let’s change the icon there.
public class HyperledgerWizardIterator implements WizardDescriptor./*Progress*/InstantiatingIterator {
...
You can also extend the HyperledgerDescription.html to provide some more information to the user of the plugin.
When we run the plugin, we can already see a new project template for Hyperledger. It allows you to create a new project from scratch.
File Support
However, when we open the cto file within the model folder, we realize that there is no support for this file yet. Let’s create the foundation, so that NetBeans recognizes this file type.
Choose the menu “File” ➤ “New File” ➤ “Module Development” ➤ “File Type.” This will bring up the wizard as shown in Figure 11-8.
It will create support for our new file type. Figure 11-9 shows the next step of the wizard. Define the MIME type and file extension and click “Next” to continue. Figure 11-10 shows the last step. There we enter “Cto” for the class name prefix, and select an icon and package. Please make sure to deselect the check box “Use Multiview.” Then click on “Finish.”
Next, create a new interface that holds some constants that we are going to use while creating more features.
You can exchange the string values for the mime type and icon path in the CtoDataObject with the reference to the constants in the interface.
@MIMEResolver.ExtensionRegistration(
displayName = "#LBL_Cto_LOADER",
mimeType = FileType.MIME,
extension = {"cto", "CTO"},
position=120
)
@DataObject.Registration(
mimeType = FileType.MIME,
iconBase = FileType.ICON,
displayName = "#LBL_Cto_LOADER",
position = 300
)
When you run the plugin and use the Favorites Window to open the previous created file, you will see that the new file type is now recognized by NetBeans, and it is indicated using the previously added icon.
But there is no syntax coloring and auto-completion available. So let’s work on that.
Syntax Highlighting
The language support contains a lexer and a parser. The lexer takes the source and converts it into tokens. The parser creates a syntax tree based on those tokens. There are several tools available to create a lexer/parser, but in this tutorial, we will use ANTLR. There is plenty of documentation available about ANTLR, so it should be relatively easy to get yourself on track when you write new grammar.
For this highlighting, we use the lexer only. So, we need the lexer grammar file. This file contains the tokens that are specific for the language. You can copy the file CtoLexer.g4 from the ANTLR grammar repository: https://github.com/antlr/grammars-v4/tree/master/cto.
It’s advisable to place the file in a subdirectory of the resource folder in you project, for example, org.netbeans.modules.hyperledger.cto.grammar.
We also need to adapt the Maven pom.xml. Add the ANTLR plugin to the build section, and the ANTLR runtime and NetBeans’ Lexer API to the dependency section. The version of the plugin should match the version of the ANTLR dependency. The following snippet shows the ANTLR plugin and runtime dependencies in the pom.xml.
After a successful build, we will have a CtoLexer.java in target/generated-sources.
Next, we need to set up a service that provides the language. Therefore, we will create a new class CtoLanguageProvider, which extends the LanguageProvider from NetBeans:
...
@ServiceProvider(service=LanguageProvider.class)
public class CtoLanguageProvider extends LanguageProvider{
private final Supplier<Language<?>> supplier = () -> new CtoLanguageHierarchy().language();
public LanguageEmbedding<?> findLanguageEmbedding(Token<?> token,
LanguagePath lp, InputAttributes ia) {
return null;
}
}
The class CtoLanguageHierarchy is a definition of the cto language. The method createTokenIds will return all tokens, based on the ANTLR grammar file. It also provides an implementation of the Lexer interface, which is a mapping for characters, read from an input to the cto tokens.
...
public class CtoLanguageHierarchy extends LanguageHierarchy<CtoTokenId> {
The NameMapping interface uses a static method to map an int to a String, using the CtoLexer.VOCABULARY under the hood. The vocabulary contains some characters that we want to avoid later on; therefore, the method map removes unwanted characters from the string.
public interface NameMapping {
public static String map(int type) {
String name = CtoLexer.VOCABULARY.getDisplayName(tokenType);
return name.replaceAll("^'|'$", "");
}
}
The class CtoTokenId is a very simple implementation of the interface org.netbeans.api.lexer.TokenId from NetBeans’ Lexer API.
public class CtoTokenId implements TokenId{
private final String name;
private final String primaryCategory;
private final int id;
public CtoTokenId(String name, String primaryCategory, int id) {
this.name = name;
this.primaryCategory = primaryCategory;
this.id = id;
}
@Override
public String name() {
return name;
}
@Override
public int ordinal() {
return id;
}
@Override
public String primaryCategory() {
return primaryCategory;
}
}
The class CtoEditorLexer is the connection between the Lexer API and ANTLR. It converts an ANTLR token to the CtoTokenId. As you can see, the mapping from an id to a token is passed to the constructor. We will provide a second constructor that is used for test purposes. This way we can easily use a mock to supply a token during testing.
public final class CtoEditorLexer implements Lexer<CtoTokenId> {
private final LexerRestartInfo<CtoTokenId> info;
private final Map<Integer, CtoTokenId> idToToken;
private final Function<CtoTokenId, Token<CtoTokenId>> tokenFactory;
private final Supplier<org.antlr.v4.runtime.Token> tokenSupplier;
public CtoEditorLexer(LexerRestartInfo<CtoTokenId> info, Map<Integer, CtoTokenId> idToToken) {
this(info, idToToken, new TokenSupplier(info.input()));
private static class TokenSupplier implements Supplier<org.antlr.v4.runtime.Token> {
private final CtoLexer lexer;
TokenSupplier(LexerInput input) {
CharStream stream = new LexerCharStream(input);
lexer = new CtoLexer(stream);
}
@Override
public org.antlr.v4.runtime.Token get() {
return lexer.nextToken();
}
}
}
We need a way to feed the platform’s input into the CtoLexer, which was generated by ANTLR. This lexer accepts an interface of org.antlr.v4.runtime.CharStream, so let’s implement it. The following source is based on the org.antlr.v4.runtime.ANTLRInputStream.
final class LexerCharStream implements CharStream {
private final static String NAME = "CtoChar";
private final LexerInput input;
private final Deque<Integer> markers = new ArrayDeque<>();
public LexerCharStream(LexerInput input) {
this.input = input;
}
@Override
public String getText(Interval interval) {
Objects.requireNonNull(interval, "Interval may not be null");
Let’s create our first unit test with JUnit 5. Select the enum TokenTaxonomy in the Projects window, right-click, and choose “Tools” ➤ “Create/Update Tests”. Figure 11-11 shows the dialog to create or update a test.
Implement the test as follows:
public class TokenTaxonomyTest {
@Test
@DisplayName("It should return a list of keyword tokens.")
public void tokens_Keywords() {
List<CtoTokenId> result = TokenTaxonomy.INSTANCE.tokens(Category.KEYWORD);
assertThat(result.isEmpty(), is(false));
}
}
We are done with the Java source code in the context of the syntax highlighting. However, some pieces of the puzzle are still missing. First, we have to provide an xml file that maps a value of the enum Category to a color. So create a fontColors.xml in src/main/resources/org/netbeans/modules/hyperledger/cto and use the following content:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontscolors PUBLIC "-//NetBeans//DTD Editor Fonts and Colors settings 1.1//EN"
Provide a translation for those values in the bundle file of this module in src/main/resources/org/netbeans/modules/hyperledger/Bundle.properties:
KEYWORD=Keyword
TYPE=Primitive Type
FIELD=Field
COMMENT=Comment
SEPARATOR=Separator
VALUE=Value
We need to register this mapping and provide a preview for the Options Dialog, so that the user can have a quick preview of the colors when he is changing them. This is done in the NetBeans layer system. Please add a layer.xml file in the same directory as the Bundle.properties with 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">
As you can see, we also registered a sample cto file in the layer.xml for the color preview. Therefore, let’s add this file in the directory src/main/resources/org/netbeans/modules/hyperledger/cto.
/*
* model file sample
*/
namespace org.basic.sample
import foo.bar
// asset
@foo("arg", 2)
abstract asset SampleAsset identified by assetId{
o String assetId
o Integer [] cols
--> SampleParticipant owner
}
When we run the plugin, we will have a syntax highlighting in the editor (see Figure 11-13) as well a way to change the colors in the Option Dialog (see Figure 11-14).
Error Hints
The syntax highlighting gives us a little clue if the typed word is a keyword or not. However, it doesn’t tell us what should be actually typed when there is a syntax error. Here we can make use of the NetBeans error hints and the parser, generated by ANTLR. The parser allows us to attach an implementation of a listener, which collects all the syntax errors. Those syntax errors can be used for the error hints.
First, we add some new dependencies to the pom.xml file:
The Parsing API will act as a proxy for the ANTLR parser and process the parser result.
We need to define a new grammar file in the same directory where grammar for the lexer is. It is called CtoParser.g4. It contains rules to recognize resources in the cto file. The following snippet shows the beginning of this file with the entry point modelUnit. For the complete content, see the file in the source repository at GitHub: https://github.com/antlr/grammars-v4/blob/master/cto/CtoLexer.g4.
The Maven plugin will generate the CtoParser.java file. It is visible in the Project View under Generated Sources.
Next, we have to create a bunch of new classes. Let’s start with a new class that creates the parser. Notice the MimeRegistration annotation. This registers our factory to the mime lookup of NetBeans.
@MimeRegistration(mimeType = FileType.MIME, service = ParserFactory.class)
public class CtoParserFactory extends ParserFactory {
@Override
public Parser createParser(Collection<Snapshot> coll) {
return new CtoProxyParser(ParserProvider.INSTANCE);
}
}
The ParserProvider implements the Function interface and creates a new CtoParser for a given text.
public enum ParserProvider implements Function<String, CtoParser> {
INSTANCE;
@Override
public CtoParser apply(String text) {
CharStream input = CharStreams.fromString(text);
Lexer lexer = new CtoLexer(input);
TokenStream tokenStream = new CommonTokenStream(lexer);
return new CtoParser(tokenStream);
}
}
The class CtoProxyParser is a proxy for the ANTLR CtoParser. It parses a snapshot of the editor content and provides the parser result; in this case it contains a list of syntax errors.
public class CtoProxyParser extends Parser {
private final Function<String, CtoParser> parserProvider;
private CtoParserResult parserResult;
public CtoProxyParser(Function<String, CtoParser> parserProvider) {
parserResult = new CtoParserResult(snapshot, errors);
}
@Override
public Result getResult(Task task) throws ParseException {
return parserResult;
}
@Override
public void addChangeListener(ChangeListener cl) {
}
@Override
public void removeChangeListener(ChangeListener cl) {
}
public static class CtoParserResult extends Parser.Result {
private boolean valid = true;
private final List<SyntaxError> errors;
public CtoParserResult(Snapshot snapshot, List<SyntaxError> errors) {
super(snapshot);
this.errors = errors;
}
public List<SyntaxError> getErrors() {
return errors;
}
@Override
protected void invalidate() {
valid = false;
}
public boolean isValid() {
return valid;
}
}
}
The SyntaxError class is very simple. It holds the message and corresponding line.
public final class SyntaxError {
private final String message;
private final int line;
public SyntaxError(String message, int line) {
this.message = message;
this.line = line;
}
public String getMessage() {
return message;
}
public int getLine() {
return line;
}
}
Whereas the ErrorParserListener collects all syntax errors.
public class ErrorParserListener extends BaseErrorListener{
private final List<SyntaxError> syntaxErrors = new ArrayList<>();
public List<SyntaxError> getSyntaxErrors() {
return syntaxErrors;
}
@Override
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
syntaxErrors.add(new SyntaxError(msg, line));
}
}
So when we have all the syntax errors, what are we going to do with them? We need to tell the editor to mark those lines. Let’s do that in a task with low priority. NetBeans provides the org.netbeans.modules.parsing.spi.ParserResultTask that processes our result when the parser is finished. The NotificationResultTask uses the syntax errors from the parser and supplies them to the HintsController, which shows the error hints in the editor.
public class NotificationResultTask extends ParserResultTask {
private static final String LAYER = "cto";
@Override
public void run(Parser.Result result, SchedulerEvent se) {
public Class<? extends Scheduler> getSchedulerClass() {
return Scheduler.EDITOR_SENSITIVE_TASK_SCHEDULER;
}
@Override
public void cancel() {
}
}
The call to the HintsController is placed in an extra method, because this way we can verify it with a spy in the unit test. So let’s create a new test for this class:
@ExtendWith(MockitoExtension.class)
public class NotificationResultTaskTest {
@Mock
private Snapshot snapshot;
@Mock
private Source source;
@Mock
private Document document;
@Spy
private NotificationResultTask classUnderTest;
@Test
@DisplayName("It should set errors in the HintsController")
return new AsyncCompletionTask(new CtoCompletionQuery(), jtc);
}
return null;
}
@Override
public int getAutoQueryTypes(JTextComponent jtc, String string) {
return 0;
}
}
If the query type is a completion query, then a new task is returned. This allows us to execute our query asynchronously.
For the method getAutoQueryTypes, we will return 0. That means the code completion will pop up only when the user presses the right keys.
The actual work is done in the CtoCompletionQuery. This class adds the items to the suggestions and also filters while the user types. Our class extends from AsyncCompletionQuery. So we need to implement the query method where we add the items. We will use the keywords and primitive types as suggestions. Some items of the keyword category will have an icon to indicate that those types are essential for the business network definition.
final class CtoCompletionQuery extends AsyncCompletionQuery {
private static final String ICON_PATH = "org/netbeans/modules/hyperledger/cto/%s";
There is a filter for the completion items. It returns an optional typed string from the document, as well as the location where to insert the value for the item, which was selected from the pop-up menu. Create a new interface with the following content:
interface CompletionFilter {
char SPC = ' ';
static class FilterResult {
Optional<String> filter = empty();
Pair<Integer, Integer> location;
}
FilterResult filter(Document document, int offset);
static class FilterImpl implements CompletionFilter {
@Override
public FilterResult filter(Document document, int offset) {
private int firstRowNotWhitespace(StyledDocument doc, int offset)
throws BadLocationException {
Element paragraph = doc.getParagraphElement(offset);
int start = paragraph.getStartOffset();
int end = paragraph.getEndOffset();
while (start + 1 < end) {
if (doc.getText(start, 1).charAt(0) != SPC) {
break;
}
start++;
}
return start;
}
private int indexOfWhitespace(char[] line) {
for(int i = line.length - 1; i > -1; i--) {
if (Character.isWhitespace(line[i])) {
return i;
}
}
return -1;
}
}
}
We distinguish between a completion item for primitives and other keywords. You have already seen the available primitive types in the introduction to Hyperledger.
public class PrimitiveTypeCompletionItem extends AbstractCompletionItem{
public PrimitiveTypeCompletionItem(String name, Pair<Integer, Integer> location) {
super(name, location);
}
@Override
public int getSortPriority() {
//higher value means that items will appear at the end of the popup
return 200;
}
@Override
protected ImageIcon getIcon() {
return null;
}
}
Some keyword items have an icon, which shows that they are essential for the definition of the model. If they do have an icon, then their sort priority is higher, and they will end up at the head of the suggested collection.
@NbBundle.Messages({
"asset=Asset is an class definition that represent something valuable which is exchanged within the network.",
"participant=Participant is a member of the network that may hold the asset.",
"transaction=Transaction is the process when an assets changes the owner, e.g. from one participant to another."
})
public class KeywordCompletionItem extends AbstractCompletionItem {
private final ImageIcon icon;
public KeywordCompletionItem(Optional<String> iconPath, String name, Pair<Integer, Integer> location) {
super(name, location);
icon = iconPath.map(path -> new ImageIcon(ImageUtilities.loadImage(path))).orElse(null);
}
@Override
public int getSortPriority() {
return (icon != null) ? 50 : 100;
}
@Override
protected ImageIcon getIcon() {
return icon;
}
}
Both item types extend the AbstractCompletionItem, which provides a basic implementation of the interface org.netbeans.spi.editor.completion.CompletionItem.
static class Documentation implements CompletionDocumentation {
private final String message;
private final URL docUrl;
public Documentation(String message, URL docUrl) {
this.message = message;
this.docUrl = docUrl;
}
@Override
public String getText() {
return message;
}
@Override
public URL getURL() {
return docUrl;
}
@Override
public CompletionDocumentation resolveLink(String string) {
return null;
}
@Override
public Action getGotoSourceAction() {
return null;
}
}
}
It’s time to run the plugin. If you invoke the code completion in the editor, NetBeans will suggest a term to complete your input, as shown in Figure 11-16.
Navigator Panel
The navigator panel gives the user a quick overview of the content in the editor window, that is, a bird’s eye-view. A typical example is the navigator for a Java source file. Let’s create such a view for the cto file.
First, we need to add two new Maven dependencies to the pom.xml.
Let’s define a model class that represents a resource from the cto file. It has the name, the type, and an offset for its occurrence:
public class CtoResource {
private final String name;
private final int type;
private final int offset;
public CtoResource(String name, int type, int offset) {
this.name = name;
this.type = type;
this.offset = offset;
}
public String getName() {
return name;
}
public int getType() {
return type;
}
public int getOffset() {
return offset;
}
}
The starting point for the navigator is to implement the interface org.netbeans.spi.navigator.NavigatorPanel and register it with the annotation NavigatorPanel.Registration.
@NbBundle.Messages({
"CTO_NAV_NAME=Composer Model",
"CTO_NAV_HINT=Overview of the resource definitions of the file."
})
@NavigatorPanel.Registration(mimeType = FileType.MIME, position = 500, displayName = "#CTO_NAV_NAME")
public class CtoNavigatorPanel implements NavigatorPanel {
private static final RequestProcessor RP = new RequestProcessor(CtoNavigatorPanel.class.getName(), 1);
private final JComponent view = new MembersView();
private Optional<RootNode> rootNode = empty();
private Lookup.Result<DataObject> selection;
private final LookupListener selectionListener = ev -> {
The class MembersView is a Swing component that contains the org.openide.explorer.view.BeanTreeView from the Explorer API and connects it with the org.openide.explorer.ExplorerManager and org.openide.util.Lookup.
final class MembersView extends JPanel implements ExplorerManager.Provider, Lookup.Provider {
private final ExplorerManager manager;
private final Lookup lookup;
private final BeanTreeView view;
MembersView() {
this.manager = new ExplorerManager();
this.lookup = ExplorerUtils.createLookup(manager, new ActionMap());
The optional root node in the class CtoNavigatorPanel is the parent of the resources from the cto file. When the node is created, it takes the name from the file itself, but it will be updated with the namespace after parsing the content.
final class RootNode extends DataNode {
private final MembersFactory factory;
RootNode(DataObject obj) {
super(obj, Children.LEAF);
factory = new MembersFactory(this);
setIconBaseWithExtension(FileType.ICON);
setChildren(Children.create(factory, true));
}
MembersFactory getFactory() {
return factory;
}
}
The class ChildNode is the visualization of a resource from the cto file. The class also provides an action that allows the user to navigate to the source in the editor. For this feature we use the NbDocument utility class:
public final class ChildNode extends AbstractNode{
@StaticResource
private static final String ICON = "org/netbeans/modules/hyperledger/cto/blue.png";
private static final String MEMBER = "%s : %s";
private static final RequestProcessor RP = new RequestProcessor();
private final DataObject dataObject;
private final CtoResource resource;
private final Action openAction = new AbstractAction() {
The construction of the children is delegated to a factory that extends org.openide.nodes.ChildFactory. The children will be created with the help of the CtoParser. Please create a new class with the following content:
final class MembersFactory extends ChildFactory<CtoResource> {
private Collection<CtoResource> resources = new ArrayList<>();
private final DataNode root;
private final FileChangeAdapter adapter = new FileChangeAdapter() {
The ParserListener can be attached to the parser. It is listening to events that occur when the parser enters or exits a rule, which were previously defined in the grammar file. The listener will collect the name and type of the rule, which we want to display in the navigator.
public final class ParserListener extends CtoParserBaseListener {
private final List<CtoResource> resources = new ArrayList<>();
public List<CtoResource> getResources() {
return resources;
}
private void addNode(TerminalNode node, int type, int offset) {
if (node != null && !(node instanceof ErrorNode)) {
addNode(node.getText(), type, offset);
}
}
private void addNode(String text, int type, int offset) {
When we open the model file now, the navigator window (Figure 11-17) will display an overview of the resources from the file.
But if you change one of these names in the editor, you will notice that it has no immediate effect on the navigator. Only after saving the file is the content of the navigator updated. We need to listen to the changes in the editor and fire an event that forces the navigator to update the nodes. One way is to look for the currently opened editor and attach a document listener there. But that means a lot of boilerplate code. However, we already created a way to process the text from the editor in the syntax highlighting section. Let’s extend it for the result from the parser listener, and then deliver it to the factory for the nodes in the navigator window.
First, open the class CtoProxyParser and add the resources to the parser result:
public static class CtoParserResult extends Parser.Result {
private boolean valid = true;
private final List<CtoResource> resources;
private final List<SyntaxError> errors;
public CtoParserResult(Snapshot snapshot, List<CtoResource> resources, List<SyntaxError> errors) {
super(snapshot);
this.resources = resources;
this.errors = errors;
}
public List<CtoResource> getResources() {
if (!valid) {
return emptyList();
}
return resources;
}
Extend the parse method of the CtoProxyParser, by using the parser listener and pass the result from the listener to the class that we updated:
parserResult = new CtoParserResult(snapshot, resources, errors);
}
We need a solution to send those resources to the nodes factory. Let’s use NetBeans Lookup for this purpose. Therefore, create a new enum that implements a Lookup.Provider. It is advisable to place the enum in a file LookupContext.java within a completely new package, since it will act as a broker between unrelated classes or packages.
public enum LookupContext implements Lookup.Provider{
INSTANCE;
private final InstanceContent content;
private final Lookup lookup;
private LookupContext() {
this.content = new InstanceContent();
this.lookup = new AbstractLookup(content);
}
@Override
public Lookup getLookup() {
return lookup;
}
public void add(Object inst) {
content.add(inst);
}
public void remove(Object inst) {
content.remove(inst);
}
}
We will use this LookupContext in NotificationResultTask.java, which was created for the error hints. Add the following lines to the run method of the class:
@Override
public void run(Parser.Result result, SchedulerEvent se) {
Run the plugin, and you will see the effect in the navigator, while you change the name of the asset in the editor.
Signing and Sharing a Plugin
Once a plugin has been created, it can be shared with the community through the Apache NetBeans Plugin Portal. Plugins that are made available via the Plugin Portal can be installed by anyone in the community. The Plugin Portal is available online using a web browser in the URL http://netbeans-vm.apache.org/pluginportal/,1 and it is also accessible within the Apache NetBeans IDE by choosing “Tools” ➤ “Plugins.”
In order to share the plugin, it must be signed with a certificate, either self-signed or verified by a certificate authority. The signing process is much the same as with any Java JAR archive that is going to be distributed. There are a couple of different ways to sign a JAR or NBM file, those being manual or by integrating the signing process into a build. In this section, we will cover incorporating the process into the Maven build.
Signing the Project
It is possible to sign an ANT- or Maven-based project for distribution using the Java keytool utility to create a keystore and then updating the build file to incorporate the keystore into the build. In this example, the Maven-based POM file for the project will be updated to include a generated keystore. To begin, generate a keystore by opening a command line or terminal and traversing inside of the project directory. Once in the directory, issue the keytool command to generate the keystore.
Be sure to set the keystore and key password to the same value. Ensure that the nbproject/private directory is not distributed along with the archive or committed to a version control system.
Once the keystore has been generated and saved into a directory such as nbproject/private, the project POM file can be updated to include the nbm-maven-plugin configuration that will be required to sign the NBM file when generated. Simply add the following configuration to the nbm-maven-plugin plugin section.
If you have a single plugin to package for distribution, the easiest way to create a binary file is to generate an NBM. There are a few ways to create a signed NBM. To begin, if you are not planning to sign the NBM so that it can be included in the plugin center, simply generate the NBM via Apache NetBeans. To do so, use the following procedure within Apache NetBeans:
Right-click on the Hyperledger Support project and choose “Create NBM” (Figure 11-18). The IDE will compile and package the project into a distributable binary NBM file. The file will appear within the project “build” directory once generated.
To generate a signed NBM, the Maven build can be completed via the command line or terminal by traversing inside of the project directory and issuing the following command:
Once the NBM has been signed and packaged, it is ready to be loaded into the plugin portal. Use the following steps to upload the plugin to the portal.
Click the “Add Plugin” button within the main menu.
5.
Provide the pertinent information (groupId and artifactId values) for your plugin and click “Add Plugin” button. The two values are contained in maven-metadata.xml file. All other information should be added in automatically. It is a good practice to add a homepage and thumbnail for your plugin. Select “Save Plugin”.
6.
Select the appropriate Apache NetBeans version for your plugin by choosing an appropriate version within the “My Plugins” page, which will open the “Version Management” page.
7.
Select the appropriate compatible Apache NetBeans version, provide optional release information, and choose “Save Plugin Version”.
In this chapter we walked through several guides about how to create a NetBeans plugin with Maven for a new language. You have seen how easy it is to create a template for a new project, how to recognize a new file type, and achieve syntax highlighting and error hint with the help of ANTLR. We also touched on the code completion and navigator window. NetBeans’ lookup API was used for communication between packages. In the end, you have seen the necessary steps to sign and publish the plugin.
In the next chapter you will see why NetBeans was shifted under the umbrella of the Apache Software Foundation and how you can contribute to the project.