© Ioannis Kostaras, Constantin Drabo, Josh Juneau, Sven Reimers, Mario Schröder, Geertjan Wielenga 2020
I. Kostaras et al.Pro Apache NetBeanshttps://doi.org/10.1007/978-1-4842-5370-0_11

11. Writing a Plugin for NetBeans

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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig1_HTML.jpg
Figure 11-1

How blocks are connected in the blockchain

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.

Preparation for Hyperledger

To work with Hyperledger properly, we need to install some tools. Please ensure that your machine fulfills the prerequisites, described on https://hyperledger.github.io/composer/latest/installing/installing-prereqs.html.

The part for installing VSCode can be skipped, since we are developing our own editor.

When finished with those steps, please install the Hyperledger Composer Command Line Tool globally, the application generator, and Yeoman:
npm install -g [email protected]
npm install -g [email protected]
npm install -g yo
You should also install the Hyperledger Composer Playground, which provides a user interface for the configuration, deployment, and testing of a business network.
npm install -g [email protected]
Hyperledger Fabric is the runtime for the business networks. There are scripts to install this runtime. Download into a directory of your choice:
mkdir ~/fabric-dev-servers && cd ~/fabric-dev-servers
curl -O https://raw.githubusercontent.com/hyperledger/composer-tools/master/packages/fabric-dev-servers/fabric-dev-servers.tar.gz
tar -xvf fabric-dev-servers.tar.gz
Execute the following commands:
export FABRIC_VERSION=hlfv12
./downloadFabric.sh

You have finished preparing your environment to work with Hyperledger.

For more details, consult the installation instructions:

https://hyperledger.github.io/composer/latest/installing/development-tools.html.

Let’s create a new skeleton business network with Yeoman in the console. This business network will be used during the development of the plugin.
yo hyperledger-composer:businessnetwork
? Business network name: ledger
? Description: sample
? Author name: admin
? Author email: [email protected]
? License: Apache-2.0
? Namespace: org.example.biznet
? 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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig2_HTML.jpg
Figure 11-2

Wizard to create a new NetBeans module with Maven

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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig3_HTML.jpg
Figure 11-3

Dialog to add dependencies

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:

http://static.javadoc.io/org.mockito/mockito-core/2.24.0/org/mockito/Mockito.html.

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.
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <netbeans.version>RELEASE82</netbeans.version>
    <antlr.version>4.7.1</antlr.version>
</properties>

Project Template

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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig4_HTML.jpg
Figure 11-4

Wizard with Node.js Application

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.
auxiliary.org-netbeans-modules-javascript-nodejs.enabled=true
run.as=node.js
files.encoding=UTF-8
source.folder=
Change the name-tag in the project.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:="http://www.netbeans.org/ns/project/1">
  <type>org.netbeans.modules.web.clientproject</type>
  <configuration>
    <data xmlns:="http://www.netbeans.org/ns/clientside-project/1">
      <name>Hyperledger Business Network</name>
    </data>
  </configuration>
</project>
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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig5_HTML.jpg
Figure 11-5

Wizard to create a Project Template

Select the previously created Node.js project (Figure 11-6).
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig6_HTML.jpg
Figure 11-6

Wizard to select a Project Template

Click “Next,” enter some meaningful name, use a package with the name “template,” and click Finish. See Figure 11-7.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig7_HTML.jpg
Figure 11-7

Wizard to set name and location of the Project Template

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.
...
@TemplateRegistration(folder = "Project/ClientSide",
      displayName = "#Hyperledger_displayName",
      description = "HyperledgerDescription.html",
      iconBase = "org/netbeans/modules/hyperledger/template/blockchain.png",
      content = "HyperledgerProject.zip")
@Messages("Hyperledger_displayName=Hyperledger")
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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig8_HTML.jpg
Figure 11-8

Wizard for new file type

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.”
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig9_HTML.jpg
Figure 11-9

Step 2 to edit File Recognition

../images/479166_1_En_11_Chapter/479166_1_En_11_Fig10_HTML.jpg
Figure 11-10

Step 3 for Name, Icon, and Location

Next, create a new interface that holds some constants that we are going to use while creating more features.
package org.netbeans.modules.hyperledger.cto;
import org.netbeans.api.annotations.common.StaticResource;
public interface FileType {
      @StaticResource
      String ICON = "org/netbeans/modules/hyperledger/cto/value_16x16.png";
      String MIME = "text/cto";
}
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 .
...
<build>
  <plugins>
    <plugin>
      <groupId>org.antlr</groupId>
      <artifactId>antlr4-maven-plugin</artifactId>
      <version>${antlr.version}</version>
      <executions>
        <execution>
          <id>antlr</id>
          <goals>
            <goal>antlr4</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
...
<dependency>
  <groupId>org.antlr</groupId>
  <artifactId>antlr4-runtime</artifactId>
  <version>${antlr.version}</version>
</dependency>
<dependency>
  <groupId>org.netbeans.api</groupId>
  <artifactId>org-netbeans-modules-lexer</artifactId>
  <version>${netbeans.version}</version>
</dependency>
...

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();
      @Override
      public Language<?> findLanguage(String mime) {
            return (FileType.MIME.equals(mime)) ? supplier.get() : null;
      }
      @Override
      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> {
      private final List<CtoTokenId> tokens;
      private final Map<Integer, CtoTokenId> idToToken;
      public CtoLanguageHierarchy() {
            tokens = TokenTaxonomy.getDefault().allTokens();
            idToToken = TokenTaxonomy.getDefault().getIdTokenMap();
      }
      @Override
      protected Collection<CtoTokenId> createTokenIds() {
            return tokens;
      }
      @Override
      protected Lexer<CtoTokenId> createLexer(LexerRestartInfo<CtoTokenId> info) {
            return new CtoEditorLexer(info, idToToken);
      }
      @Override
      protected String mimeType() {
            return FileType.MIME;
      }
}
The TokenTaxonomy provides a classification for the tokens, so that we can later on choose a different styling for a group of tokens.
public enum TokenTaxonomy {
    INSTANCE;
    private final List<CtoTokenId> tokens;
    private TokenTaxonomy() {
        tokens = new ArrayList<>();
        int max = CtoLexer.VOCABULARY.getMaxTokenType() + 1;
        for (int i = 1; i < max; i++) {
            CtoTokenId token = new CtoTokenId(NameMapping.map(i), getCategory(i), i);
            tokens.add(token);
        }
    }
    private String getCategory(int token) {
        Function<Integer, Category> mapping = t -> {
            if (t < CtoLexer.BOOLEAN) {
                return Category.KEYWORD;
            } else if (t < CtoLexer.LPAREN) {
                return Category.TYPE;
            } else if (t < CtoLexer.REF) {
                return Category.SEPARATOR;
            } else if (t < CtoLexer.DECIMAL_LITERAL) {
                return Category.FIELD;
            } else if (t < CtoLexer.WS || t == CtoLexer.CHAR_LITERAL || t == CtoLexer.STRING_LITERAL) {
                return Category.VALUE;
            } else if (t == CtoLexer.COMMENT || t == CtoLexer.LINE_COMMENT) {
                return Category.COMMENT;
            }
            return Category.TEXT;
        };
        return mapping.apply(token).name();
    }
    public List<CtoTokenId> allTokens() {
        return tokens;
    }
    public List<CtoTokenId> tokens(Category category) {
        return tokens.stream().filter(t -> category.name().equals(t.primaryCategory())).collect(toList());
    }
    public Map<Integer, CtoTokenId> getIdTokenMap() {
        return tokens.stream().collect(toMap(CtoTokenId::ordinal, t -> t));
    }
}
The category itself is just another enum used for the grouping:
public enum Category {
    KEYWORD, TYPE, FIELD, SEPARATOR, VALUE, COMMENT, TEXT;
}
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()));
    }
    CtoEditorLexer(LexerRestartInfo<CtoTokenId> info, Map<Integer, CtoTokenId> idToToken,
            Supplier<org.antlr.v4.runtime.Token> tokenSupplier) {
        this.info = info;
        this.idToToken = idToToken;
        this.tokenSupplier = tokenSupplier;
        this.tokenFactory = id -> info.tokenFactory().createToken(id);
    }
    @Override
    public Token<CtoTokenId> nextToken() {
        Token<CtoTokenId> createdToken = null;
        org.antlr.v4.runtime.Token token = tokenSupplier.get();
        int type = token.getType();
        if (type != -1) {
            createdToken = createToken(type);
        } else if (info.input().readLength() > 0) {
            createdToken = createToken(CtoLexer.WS);
        }
        return createdToken;
    }
    private Token<CtoTokenId> createToken(int type) {
        Function<Integer, CtoTokenId> mapping = idToToken::get;
        return mapping.andThen(tokenFactory).apply(type);
    }
    @Override
    public Object state() {
        return null;
    }
    @Override
    public void release() {
        //nothing todo
    }
    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");
        if (interval.a < 0 || interval.b < interval.a - 1) {
            throw new IllegalArgumentException("Invalid interval!");
        }
        return input.readText(interval.a, interval.b).toString();
    }
    @Override
    public void consume() {
        read();
    }
    @Override
    public int LA(int ahead) {
        if (ahead == 0) {
            return 0;
        }
        int c = 0;
        for (int j = 0; j < ahead; j++) {
            c = read();
        }
        backup(ahead);
        return c;
    }
    @Override
    public int mark() {
        markers.push(index());
        return markers.size() - 1;
    }
    @Override
    public void release(int marker) {
        if(markers.size() < marker) {
            return;
        }
        //remove all markers from the given one
        for(int i = marker; i < markers.size(); i++) {
            markers.remove(i);
        }
    }
    @Override
    public int index() {
        return input.readLengthEOF();
    }
    @Override
    public void seek(int index) {
        int len = index();
        if (index < len) {
            //seek backward
            backup(len - index);
        } else {
            // seek forward
            while (len < index) {
                consume();
            }
        }
    }
    @Override
    public int size() {
        return -1; //unknown
    }
    @Override
    public String getSourceName() {
        return NAME;
    }
    private int read() {
        int result = input.read();
        if (result == LexerInput.EOF) {
            result = CharStream.EOF;
        }
        return result;
    }
    private void backup(int count) {
        input.backup(count);
    }
}
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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig11_HTML.jpg
Figure 11-11

Dialog for tests

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"
"http://www.netbeans.org/dtds/EditorFontsColors-1_1.dtd">
<fontscolors>
    <fontcolor name="KEYWORD" foreColor="blue" default="keyword"/>
    <fontcolor name="TYPE" foreColor="darkGray" default="type"/>
    <fontcolor name="FIELD" foreColor="pink" default="field"/>
    <fontcolor name="VALUE" foreColor="black" default="value"/>
    <fontcolor name="SEPARATOR" foreColor="magenta" default="separator"/>
    <fontcolor name="COMMENT" foreColor="gray" default="comment"/>
</fontscolors>
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">
<filesystem>
    <folder name="Editors">
        <folder name="text">
            <folder name="cto">
                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.hyperledger.Bundle"/>
                <folder name="FontsColors">
                    <folder name="NetBeans">
                        <folder name="Defaults">
                            <file name="coloring.xml" url="cto/fontColors.xml">
                                <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.hyperledger.Bundle"/>
                            </file>
                        </folder>
                    </folder>
                </folder>
            </folder>
        </folder>
    </folder>
    <folder name="OptionsDialog">
        <folder name="PreviewExamples">
            <folder name="text">
                <file name="cto" url="cto/ColorPreview.cto"/>
            </folder>
        </folder>
    </folder>
</filesystem>
Expand the node Important Files in the Project View and register the layer.xml file in the Module Manifest (manifest.mf). See Figure 11-12.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig12_HTML.jpg
Figure 11-12

Project View with extended node for Important Files

The content of the manifest.mf file:
Manifest-Version: 1.0
OpenIDE-Module: org.netbeans.modules.hyperledger
OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/hyperledger/Bundle.properties
OpenIDE-Module-Layer: org/netbeans/modules/hyperledger/layer.xml
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).
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig13_HTML.jpg
Figure 11-13

Editor with syntax highlighting

../images/479166_1_En_11_Chapter/479166_1_En_11_Fig14_HTML.jpg
Figure 11-14

Option dialog with color preview

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:
<dependency>
  <groupId>org.netbeans.api</groupId>
  <artifactId>org-netbeans-modules-parsing-api</artifactId>
  <version>${netbeans.version}</version>
</dependency>
<dependency>
  <groupId>org.netbeans.api</groupId>
  <artifactId>org-netbeans-spi-editor-hints</artifactId>
  <version>${netbeans.version}</version>
</dependency>

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.
parser grammar CtoParser;
options { tokenVocab=CtoLexer; }
modelUnit
        : namespaceDeclaration importDeclaration* typeDeclaration* EOF;

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) {
        this.parserProvider = parserProvider;
    }
    @Override
    public void parse(Snapshot snapshot, Task task, SourceModificationEvent sme) throws ParseException {
        String text = snapshot.getText().toString();
        CtoParser ctoParser = parserProvider.apply(text);
        ErrorParserListener errorListener = new ErrorParserListener();
        ctoParser.addErrorListener(errorListener);
        //do the parsing
        ctoParser.modelUnit();
        List<SyntaxError> errors = errorListener.getSyntaxErrors();
        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) {
        CtoProxyParser.CtoParserResult ctoResult = (CtoProxyParser.CtoParserResult) result;
        if (ctoResult.isValid()) {
            Document document = result.getSnapshot().getSource().getDocument(false);
            List<SyntaxError> errors = ctoResult.getErrors();
            List<ErrorDescription> descriptions = errors.stream().map(e
                -> ErrorDescriptionFactory.createErrorDescription(
                        Severity.ERROR,
                        e.getMessage(),
                        document,
                        e.getLine())).collect(toList());
            setErrors(document, descriptions);
        }
    }
    void setErrors(Document document, List<ErrorDescription> descriptions) {
        HintsController.setErrors(document, LAYER, descriptions);
    }
    @Override
    public int getPriority() {
        return 100; //the lower, the higher the priority
    }
    @Override
    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")
    public void run() {
        given(snapshot.getSource()).willReturn(source);
        given(source.getDocument(false)).willReturn(document);
        List<CtoResource> res = singletonList(new CtoResource("foo", 0, 0));
        List<SyntaxError> errs = singletonList(new SyntaxError("bar", 0));
        CtoParserResult ctoResult = new CtoParserResult(snapshot, res, errs);
        classUnderTest.run(ctoResult, null);
        verify(classUnderTest).setErrors(eq(document), any(List.class));
    }
}
Last, we have to write a class that creates the above result task.
@MimeRegistration(mimeType = FileType.MIME, service = TaskFactory.class)
public class NotificationResultTaskFactory extends TaskFactory{
    @Override
    public Collection<? extends SchedulerTask> create(Snapshot snpsht) {
        return singletonList(new NotificationResultTask());
    }
}
Run the plugin, open the cto file, and the editor will mark erroneous lines as shown in Figure 11-15.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig15_HTML.jpg
Figure 11-15

Editor with error hint

Code Completion

In this part we are going to add the code completion for the cto file. First, we need to add the following dependency to the Maven pom.xml file:
<dependency>
  <groupId>org.netbeans.api</groupId>
  <artifactId>org-netbeans-modules-editor-completion</artifactId>
  <version>${netbeans.version}</version>
</dependency>
The starting point is the interface CompletionProvider . We will provide our own implementation.
@MimeRegistration(mimeType = FileType.MIME, service = CompletionProvider.class)
public class CtoCompletionProvider implements CompletionProvider{
  @Override
  public CompletionTask createTask(int type, JTextComponent jtc) {
    if(type == CompletionProvider.COMPLETION_QUERY_TYPE) {
        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";
  private final CompletionFilter completionFilter;
  CtoCompletionQuery() {
    this(new CompletionFilter.FilterImpl());
  }
  CtoCompletionQuery(CompletionFilter completionFilter) {
    this.completionFilter = completionFilter;
  }
  @Override
  protected void query(CompletionResultSet crs, Document document, int offset) {
    CompletionFilter.FilterResult filterResult = completionFilter.filter(document, offset);
    crs.addAllItems(getKeywordItems(filterResult));
    crs.addAllItems(getPrimitiveTypeItems(filterResult));
    crs.finish();
  }
  private List<? extends AbstractCompletionItem>getKeywordItems(CompletionFilter.FilterResult              filterResult) {
  Function<CtoTokenId, KeywordCompletionItem> mapping = token -> {
    Optional<String> iconPath = iconPath(token.ordinal());
     return new KeywordCompletionItem(iconPath, token.name(), filterResult. location);
    };
    return map(filterResult.filter, Category.KEYWORD, mapping);
  }
  private List<? extends AbstractCompletionItem> getPrimitiveTypeItems(CompletionFilter.FilterResult filterResult) {
    Function<CtoTokenId, PrimitiveTypeCompletionItem> mapping = token -> {
      return new PrimitiveTypeCompletionItem(token.name(), filterResult.location);
    };
    return map(filterResult.filter, Category.TYPE, mapping);
  }
  private List<? extends AbstractCompletionItem> map(Optional<String> filter, Category category, Function<CtoTokenId, ? extends AbstractCompletionItem> mapping) {
    Stream<CtoTokenId> tokens = TokenTaxonomy.INSTANCE.tokens(category).stream();
    String name = filter.orElse("");
    if(!name.isEmpty()){
      tokens = tokens.filter(t -> t.name().startsWith(name));
    }
    return tokens.map(mapping::apply).collect(toList());
}
private Optional<String> iconPath(int type) {
  switch (type) {
    case CtoLexer.ASSET:
      return of(format(ICON_PATH, "asset.png"));
    case CtoLexer.PARTICIPANT:
       return of(format(ICON_PATH, "participant.png"));
    case CtoLexer.TRANSACTION:
       return of(format(ICON_PATH, "transaction.png"));
     default:
       return empty();
    }
  }
}
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) {
            String filter = null;
            int startOffset = offset - 1;
            try {
                StyledDocument styledDocument = (StyledDocument) document;
                int lineStartOffset = firstRowNotWhitespace(styledDocument, offset);
                char[] line = styledDocument.getText(lineStartOffset, offset - lineStartOffset).toCharArray();
                int whiteOffset = indexOfWhitespace(line);
                filter = new String(line, whiteOffset + 1, line.length - whiteOffset - 1);
                startOffset = (whiteOffset > 0 ) ? lineStartOffset + whiteOffset + 1 : lineStartOffset;
            } catch (BadLocationException ex) {
                Exceptions.printStackTrace(ex);
            }
            FilterResult result = new FilterResult();
            result.filter = Optional.ofNullable(filter);
            result.location = Pair.of(startOffset, offset);
            return result;
        }
        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.
@NbBundle.Messages({
 "docUrl=https://hyperledger.github.io/composer/latest/reference/cto_language.html"
})
public abstract class AbstractCompletionItem implements CompletionItem {
    private static final String TEMPLATE = "%s ";
    private static final Color SELECTED_COLOR = Color.decode("0x0000B2");
    private final String name;
    private final int startOffset;
    private final int endOffset;
    public AbstractCompletionItem(String name, Pair<Integer, Integer> location) {
        this.name = name;
        this.startOffset = location.first();
        this.endOffset = location.second();
    }
    @Override
    public void defaultAction(JTextComponent jtc) {
        try {
            Document doc = jtc.getDocument();
            doc.remove(startOffset, endOffset - startOffset);
            doc.insertString(startOffset, format(TEMPLATE, name), null);
            Completion.get().hideAll();
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    }
    @Override
    public void processKeyEvent(KeyEvent ke) {
    }
    @Override
    public int getPreferredWidth(Graphics graphics, Font font) {
        return CompletionUtilities.getPreferredWidth(name, null, graphics, font);
    }
    @Override
    public void render(Graphics grphcs, Font font, Color frontCol, Color backCol, int width, int height, boolean selected) {
        CompletionUtilities.renderHtml(getIcon(), name, null, grphcs, font,
                (selected ? Color.white : SELECTED_COLOR), width, height, selected);
    }
    @Override
    public CompletionTask createDocumentationTask() {
        Optional<String> opt = getMessage(name);
        return opt.map(msg -> new AsyncCompletionTask(new AsyncCompletionQuery() {
            @Override
            protected void query(CompletionResultSet completionResultSet, Document document, int i) {
                completionResultSet.setDocumentation(new Documentation(msg, getDocumentationURL()));
                completionResultSet.finish();
            }
        })).orElse(null);
    }
    @Override
    public CompletionTask createToolTipTask() {
        return null;
    }
    @Override
    public boolean instantSubstitution(JTextComponent jtc) {
        return false;
    }
    @Override
    public CharSequence getSortText() {
        return name;
    }
    @Override
    public CharSequence getInsertPrefix() {
        return name;
    }
    private URL getDocumentationURL() {
        String docUrl = getMessage("docUrl").orElse("");
        try {
            return new URL(docUrl);
        } catch (MalformedURLException ex) {
            return null;
        }
    }
    private Optional<String> getMessage(String key) {
        try {
            return of(NbBundle.getMessage(AbstractCompletionItem.class, key));
        } catch (MissingResourceException e) {
            return empty();
        }
    }
    protected abstract ImageIcon getIcon();
    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.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig16_HTML.jpg
Figure 11-16

Editor with auto suggestions

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.
<dependency>
      <groupId>org.netbeans.api</groupId>
      <artifactId>org-netbeans-spi-navigator</artifactId>
      <version>${netbeans.version}</version>
</dependency>
<dependency>
      <groupId>org.netbeans.api</groupId>
      <artifactId>org-openide-explorer</artifactId>
<version>${netbeans.version}</version>
</dependency>
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 -> {
    RP.post(() -> {
        rootNode.ifPresent(n -> {
          n.getFactory().cleanup();
          rootNode = empty();
        });
        if (selection != null) {
          display(selection.allInstances());
        }
      });
    };
  @Override
  public String getDisplayName() {
    return CTO_NAV_NAME();
  }
  @Override
  public String getDisplayHint() {
    return CTO_NAV_HINT();
  }
  @Override
  public JComponent getComponent() {
    return view;
  }
  @Override
  public void panelActivated(Lookup lkp) {
    selection = lkp.lookupResult(DataObject.class);
    selection.addLookupListener(selectionListener);
    selectionListener.resultChanged(null);
  }
  @Override
  public void panelDeactivated() {
    selectionListener.resultChanged(null);
    selection.removeLookupListener(selectionListener);
    selection = null;
  }
  @Override
  public Lookup getLookup() {
    return view.getLookup();
  }
  private void display(Collection<? extends DataObject> selectedFiles) {
    if (selectedFiles.size() == 1) {
      DataObject dataObject = selectedFiles.iterator().next();
      RootNode node = new RootNode(dataObject);
      node.getFactory().register();
      rootNode = of(node);
      view.getExplorerManager().setRootContext(node);
    } else {
      view.getExplorerManager().setRootContext(Node.EMPTY);
    }
  }
}
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());
    view = new BeanTreeView();
    view.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    setLayout(new BorderLayout());
    add(view, BorderLayout.CENTER);
  }
  @Override
  public ExplorerManager getExplorerManager() {
    return manager;
  }
  @Override
  public Lookup getLookup() {
    return lookup;
  }
  @Override
  public boolean requestFocusInWindow() {
    return view.requestFocusInWindow();
  }
}
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() {
        @Override
        public void actionPerformed(ActionEvent event) {
            RP.post(() -> {
                NbDocument.openDocument(dataObject, resource.getOffset(),
                        Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS);
            });
        }
    };
    public ChildNode(DataObject dataObject, CtoResource resource) {
        super(Children.LEAF);
        this.dataObject = dataObject;
        this.resource = resource;
        setIconBaseWithExtension(ICON);
        String type = NameMapping.map(resource.getType());
        setDisplayName(format(MEMBER, resource.getName(), type));
    }
    @Override
    public Action getPreferredAction() {
        return openAction;
    }
}
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() {
        @Override
        public void fileChanged(FileEvent fe) {
            refresh(false);
        }
    };
    MembersFactory(DataNode root) {
        this.root = root;
    }
    private FileObject getPrimaryFile() {
        return getDataObject().getPrimaryFile();
    }
    private DataObject getDataObject() {
        return root.getDataObject();
    }
    @Override
    protected Node createNodeForKey(CtoResource resource) {
        if (CtoLexer.NAMESPACE == resource.getType()) {
            updateRootName(resource.getName());
            return null;
        } else {
            return new ChildNode(getDataObject(), resource);
        }
    }
    @Override
    protected boolean createKeys(List<CtoResource> toPopulate) {
        ParserListener listener = new ParserListener();
        try {
            String text = getPrimaryFile().asText();
            CtoParser parser = ParserProvider.INSTANCE.apply(text);
            parser.addParseListener(listener);
            parser.modelUnit();
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        resources = listener.getResources();
        resources.forEach(toPopulate::add);
        return true;
    }
    private void updateRootName(String rootName) {
        String oldName = root.getDisplayName();
        if (!rootName.equals(oldName)) {
            root.setDisplayName(rootName);
        }
    }
    void register() {
        getPrimaryFile().addFileChangeListener(adapter);
    }
    void cleanup() {
        getPrimaryFile().removeFileChangeListener(adapter);
    }
}
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) {
        resources.add(new CtoResource(text, type, offset));
    }
    private int getStart(ParserRuleContext ctx) {
        return ctx.getStart().getStartIndex();
    }
    @Override
    public void exitNamespaceDeclaration(CtoParser.NamespaceDeclarationContext ctx) {
        CtoParser.QualifiedNameContext qualCtx = ctx.qualifiedName();
        if (qualCtx != null) {
            List<TerminalNode> identifiers = qualCtx.IDENTIFIER();
            String name = identifiers.stream().map(TerminalNode::getText).collect(Collectors.joining("."));
            addNode(name, CtoLexer.NAMESPACE, getStart(ctx));
        }
    }
    @Override
    public void exitAssetDeclaration(CtoParser.AssetDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(), CtoLexer.ASSET, getStart(ctx));
    }
    @Override
    public void exitParticipantDeclaration(CtoParser.ParticipantDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(), CtoLexer.PARTICIPANT, getStart(ctx));
    }
    @Override
    public void exitTransactionDeclaration(CtoParser.TransactionDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(), CtoLexer.TRANSACTION, getStart(ctx));
    }
    @Override
    public void exitEventDeclaration(CtoParser.EventDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(), CtoLexer.EVENT, getStart(ctx));
    }
    @Override
    public void exitEnumDeclaration(CtoParser.EnumDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(), CtoLexer.ENUM, getStart(ctx));
    }
    @Override
    public void exitConceptDeclaration(CtoParser.ConceptDeclarationContext ctx) {
        addNode(ctx.IDENTIFIER(0), CtoLexer.CONCEPT, getStart(ctx));
    }
}
When we open the model file now, the navigator window (Figure 11-17) will display an overview of the resources from the file.
../images/479166_1_En_11_Chapter/479166_1_En_11_Fig17_HTML.jpg
Figure 11-17

Navigator displays 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:
    @Override
    public void parse(Snapshot snapshot, Task task, SourceModificationEvent sme) throws ParseException {
        String text = snapshot.getText().toString();
        CtoParser ctoParser = parserProvider.apply(text);
        ParserListener listener = new ParserListener();
        ErrorParserListener errorListener = new ErrorParserListener();
        ctoParser.addParseListener(listener);
        ctoParser.addErrorListener(errorListener);
        //do the parsing
        ctoParser.modelUnit();
        List<CtoResource> resources = listener.getResources();
        List<SyntaxError> errors = errorListener.getSyntaxErrors();
        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) {
        CtoProxyParser.CtoParserResult ctoResult = (CtoProxyParser.CtoParserResult) result;
    if (ctoResult.isValid()) {
        LookupContext.INSTANCE.add(ctoResult.getResources());
...
Let’s consume the resources in MembersFactory.java . Note that only the required changes within the class are shown:
final class MembersFactory extends ChildFactory<CtoResource> implements LookupListener {
    private final LookupContext lookupContext = LookupContext.INSTANCE;
    private boolean fromFile = true;
    private Lookup.Result<List> selection;
    private final FileChangeAdapter adapter = new FileChangeAdapter() {
       @Override
       public void fileChanged(FileEvent fe) {
           fromFile = true;
           refresh(false);
       }
    };
    @Override
    protected boolean createKeys(List<CtoResource> toPopulate) {
        if(fromFile) {
            resources = parseFile();
            fromFile = false;
        }
        resources.forEach(toPopulate::add);
        return true;
    }
    private List<CtoResource> parseFile() {
        ParserListener listener = new ParserListener();
        try {
            String text = getPrimaryFile().asText();
            CtoParser parser = ParserProvider.INSTANCE.apply(text);
            parser.addParseListener(listener);
            parser.modelUnit();
        } catch (IOException ex) {
            Exceptions.printStackTrace(ex);
        }
        return listener.getResources();
    };
    void register() {
        getPrimaryFile().addFileChangeListener(adapter);
        selection = lookupContext.getLookup().lookupResult(List.class);
        selection.addLookupListener(this);
    }
    void cleanup() {
        getPrimaryFile().removeFileChangeListener(adapter);
        selection.removeLookupListener(this);
    }
    @Override
    public void resultChanged(LookupEvent ev) {
        if (selection != null) {
            //consume and remove
            Collection<? extends List> results = selection.allInstances();
            if (!results.isEmpty()) {
                members = results.iterator().next();
                lookupContext.remove(members);
                refresh(false);
            }
        }
    }
}

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.
cd ../path-to-project
keytool -genkey -storepass your-password -alias your-project-alias -keystore nbproject/private/keystore

Note

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.
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>nbm-maven-plugin</artifactId>
  <version>3.14.1</version>
  <extensions>true</extensions>
  <configuration>
    <useOSGiDependencies>false</useOSGiDependencies>
    <codeNameBase>org.netbeans.modules.hyperledger</codeNameBase>
    <Author>Your Name</author>
    <homePageUrl>https://github.com/mario-s/nb-hyperledger</homePageUrl>
    <keystore>nbproject/private/keystore</keystore>
    <keystorealias>your-project-alias</keystorealias>
    <licenseName>Creative Commons BY 3.0</licenseName>
    <licenseFile>https://creativecommons.org/licenses/by/3.0/</licenseFile>
  </configuration>
</plugin>

Create the NBM

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.

../images/479166_1_En_11_Chapter/479166_1_En_11_Fig18_HTML.jpg
Figure 11-18

Create NBM File for Plugin Distribution

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:
mvn clean package nbm:nbm -Dkeystorepass=your-password
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.
  1. 1.

    Upload the plugin to Maven Central using the notes provided by Sonatype (https://central.sonatype.org/pages/ossrh-guide.html).

     
  2. 2.

    Create Google account if you do not already have one.

     
  3. 3.

    Visit the Apache NetBeans Plugin Portal (http://netbeans-vm.apache.org/pluginportal/) and use the sign-in button in the upper corner to authenticate.

     
  4. 4.

    Click the “Add Plugin” button within the main menu.

     
  5. 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. 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. 7.

    Select the appropriate compatible Apache NetBeans version, provide optional release information, and choose “Save Plugin Version”.

     
  8. 8.

    If you believe the plugin meets quality standards for the Plugin Portal (see wiki: https://cwiki.apache.org/confluence/display/NETBEANS/Quality+criteria+for+Plugin+Portal+Update+Center), you can select to have your plugin verified and approved for publication by choosing the “Request Verification” button.

     

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.252.201