Developing a Plugin
In this chapter we will get to work and actually write some code. We will start by setting up a project and then get to making the actual plugin.
You can do this with any IDE of your choice. As long as it has maven integration.
Setting up the Project
Let's get our hands dirty and prepare a project for our code. The main supported way of building your plugin is using Maven. Please note that the examples below might not use the latest versions of the Xill API.
If you get a little confused or you want to see an example you can head over to this guide's GitHub repository. There you will find a Maven project that supports this guide.
Step 1: Setting up the build
- Make sure you have Maven 3+ installed or run an IDE with an embedded maven installation.
Create a
pom.xml
file containing the configuration below.<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>nl.xillio.xill</groupId> <artifactId>xill-plugins-parent</artifactId> <!-- Replace this with the latest API version --> <version>3.04.00</version> </parent> <artifactId>plugin-guide</artifactId> <repositories> <repository> <id>jcenter</id> <url>https://jcenter.bintray.com</url> </repository> <repository> <id>xillio-release</id> <url>https://maven.xillio.com/artifactory/libs-release</url> </repository> </repositories> <dependencies> <!-- Here you declare your maven dependencies --> </dependencies> <build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> </plugin> </plugins> </build> </project>
Import your project into your IDE.
Step 2: Package Implementation
Now to create your plugin you should create a subclass of the XillPlugin
class. The Maven build will automatically pick it up and present it to the plugin framework.
package nl.xillio.xill.plugins.guide;
import nl.xillio.plugins.XillPlugin;
/**
* This class represents the Guide Xill package.
*/
public class GuideXillPlugin extends XillPlugin {
}
This class is empty for now because we do not have any configuration that we have to do here.
The Project Structure
A plugin exists within its own package. In the example above we created the nl.xillio.xill.plugins.guide
package which is the root for the GuideXillPlugin
. Later we will be adding constructs to this package. If we place them in the nl.xillio.xill.plugins.guide.**constructs**
package they will be automatically loaded by the abstract XillPackage
implementation.
This concludes the initial project setup. We are now ready to start adding functionality to the plugin.
MetaExpression
Before we create a construct let's take a look at the most important class in the Xill API. The MetaExpression
represents all expressions in Xill. It is the main container of data.
Expression Types in Xill
We do not want to bother the Xill programmer with typing. This means that the typing system used by Xill might not be entirely intuitive for you if you are more used Java's static type system.
The way we solve this problem is by implementing a conversion for every type in the Xill language to an other type. As a result you can interpret most expressions as the type you want them to be.
For example. Say I have the expression "300.5"
. Clearly this is a String
. But, yet if you pass this value to the Math.round
construct you will notice that it will still work. This is because the MetaExpression allows you to call the MetaExpression#getNumberValue()
method which formats that string to a Java Number
.
Of course conversion is not always possible. If I have the expression "Hello World"
then when I call getNumberValue()
, the return value will be Double.NaN
.
Let's take a look at the different internal types you'll run into when working with the MetaExpression.
String
You can build a String
MetaExpression
from either Java or Xill.
Create expression from | Code |
---|---|
Xill | "Hello World" |
Java | ExpressionBuilderHelper.fromValue("Hello World") |
It will convert to almost all other types.
Expression | "2356264" |
"" |
"Hello World" |
---|---|---|---|
Description | A Number String | Empty String | Any Other String |
getBooleanValue() |
true |
false |
true |
getNumberValue() |
"2356264" |
Double.NaN |
Double.NaN |
getBinaryValue() |
No Data | No Data | No Data |
Boolean
You can build a Boolean
MetaExpression
from either Java or Xill.
Create expression from | Code |
---|---|
Xill | true |
Java | ExpressionBuilderHelper.fromValue(true) |
It will convert to almost all other types.
Expression | true |
false |
---|---|---|
Description | True | False |
getStringValue() |
"true" |
"false" |
getNumberValue() |
1 |
0 |
getBinaryValue() |
No Data | No Data |
Number
You can build a Number
MetaExpression
from either Java or Xill.
Create expression from | Code |
---|---|
Xill | true |
Java | ExpressionBuilderHelper.fromValue(5246.3) |
It will convert to almost all other types.
Expression | 153 |
0 |
23.3 |
---|---|---|---|
Description | An Integer | Zero | A Double |
getStringValue() |
"153" |
"0" |
"23.3" |
getBooleanValue() |
true |
false |
true |
getBinaryValue() |
No Data | No Data | No Data |
Binary
The binary data type is special. It represents a source or target of streamed data. An example would be the result of the File.openRead
construct.
Create expression from | Code |
---|---|
Xill | File.openWrite |
Java | ExpressionBuilderHelper.fromValue(new SimpleIOStream(...)) |
It will convert to almost all other types.
Expression | Any Stream |
---|---|
Description | Any Stream |
getStringValue() |
Source or Target Description |
getBooleanValue() |
true |
getNumberValue() |
Double.NaN |
Data Structures in Xill
We can arrange these expressions in different ways. In Xill we support three basic structures: ATOMIC
, LIST
and OBJECT
.
ATOMIC
The ATOMIC
structure is the most basic. It represents a single expression. Some examples could be "Hello World"
, true
and null
.
LIST
The LIST
structure represents a collection of expressions in a fixed order. These expressions can by of any type or structure.
Note: The Xill
LIST
works likejava.util.List
but has a nice syntax wrapped around it.
To create a list in Xill you use the bracket notation.
// Create the list
var myList = [
0,
2,
"Hello World",
[1,2,3]
];
// Add an item to the end of the list
myList[] = 5;
// Set an item on a specific index
myList[1] = 5;
To create a LIST
in Java you call fromValue
with a List<MetaExpression>
as the value.
// Create list elements
MetaExpression item1 = fromValue(0);
MetaExpression item2 = fromValue("Element");
// Create the list
List<MetaExpression> list = new ArrayList<>();
list.add(item1);
list.add(item2);
// Build the expression
MetaExpression listExpression = ExpressionBuilderHelper.fromValue(list);
OBJECT
The OBJECT
structure is like LIST
but has String
indexes.
Note: The Xill
OBJECT
works likejava.util.Map
but attempts to preserve the insertion order. For this reason you have to pass the concreteLinkedHashMap
instead of aMap
when building the expression.
To create an OBJECT
in Xill you use the brace notation.
// Create an object
var myObject = {
"message": "Hello World"
};
// Add or override elements
myObject.more = "More...";
To create an OBJECT
in Java you call fromValue
with a LinkedHashMap<String, MetaExpression>
as the value.
// Create the elements
MetaExpression item1 = fromValue(0);
MetaExpression item2 = fromValue("Element");
// Create the map
LinkedHashMap<String, MetaExpression> map = new LinkedHashMap<>();
map.put("message", item2);
map.put("count" , item1);
// Build the expression
MetaExpression objectExpression = ExpressionBuilderHelper.fromValue(map);
Creating Constructs
That should be enough theory for now. It's time to get our hands dirty. The first thing we will do is create a construct that performs a simple task. Then we will get a little bit more in-depth by doing a little bit more useful operation.
Hello World
Let's create our GreetConstruct
. This construct should print Hello World you call it.
Note: You should create your constructs in the
constructs
package under the plugin package root. This is where theXillPlugin
implementation will look for constructs and load them.
package nl.xillio.xill.plugins.guide.constructs;
import ...;
public class GreetConstruct extends Construct {
public ConstructProcessor prepareProcess(ConstructContext constructContext) {
return new ConstructProcessor(
name -> process(constructContext, name),
new Argument("name", fromValue("World"), ATOMIC)
);
}
private MetaExpression process(ConstructContext constructContext,
MetaExpression name) {
Logger logger = constructContext.getRootLogger();
logger.info("Hello " + name.getStringValue() + "!");
/*
Note that here we return NULL and not null.
NULL represents a null value in Xill, null
represents a null value in Java.
*/
return NULL;
}
}
Let's take a step back and see what's going on here. We created a subclass of Construct
and placed it in the constructs
subpackage. Next we have to add the prepareProcess(ConstructContext)
method.
This ConstructProcessor
contains all the information required for executing the construct. We will pass it a process method and the input parameters.
In the process(ConstructContext, MetaExpression)
method you can see how we grab the logger and use it to print a message. Something you might notice is how we always return NULL
. This is because every Xill construct requires an output value.
Note: For the more experienced software developers, you might notice that the
GreetConstruct
class is aConstructProcessor
factory.
One interesting piece of the code about that you should take a closer look at, is the constructor of the Argument
class.
new Argument("name", fromValue("World"), ATOMIC)
This is where we define how an argument behaves. First we give it a name by passing "name"
as the first parameter, then we give it a default value by passing a MetaExpression
as the second parameter. This is optional. And finally we pass any number of ExpressionDataTypes
that determine what type of data structure you will accept. In our case we accept ATOMIC
values.
Performing an Operation
Now the example above only demonstrates how to create your construct. It does not do anything that would be considered useful, so let's create a construct that is a little bit more advanced. Let's download a resource from the internet and return it to the user.
The resource we will be downloading is a web api that returns yes, no or maybe with a random distribution. We can find it on https://apis.rtainc.co/twitchbot/8ball. Go ahead, try it in your browser. You will find that every time you refresh your browser you will see a random response. This is exactly what we will make our next construct do.
First off we create the EightBallConstruct
class like we did before with the GreetConstruct
.
public class EightBallConstruct extends Construct {
@Override
public ConstructProcessor prepareProcess() {
return new ConstructProcessor(
this:process
);
}
private MetaExpression process() {
...
}
}
Now we have to perform the download itself. To do this we will add another private method which returns a String
and then we will parse that result to a MetaExpression
in the process
method.
private String get8Ball() {
try (InputStream stream = new URL(¨https://apis.rtainc.co/twitchbot/8ball¨).openStream()) {
return IOUtils.toString(stream);
} catch(IOException e) {
throw new OperationFailedException(
"download the ´future´",
e.getMessage(),
e
);
}
}
private MetaExpression process() {
return fromValue(get8Ball());
}
Now let's take a look at how our constructs work in Xill IDE. To create the required jar file you can run the package phase in maven by entering mvn package
on the command line or running the phase from your IDE. You should now have a jar file in your target
folder. Copy that to the plugins
folder in your installation of Xill IDE.
You can run the following script to verify your construct.
use System, Guide;
System.print("Does my construct work correctly?");
System.print(Guide.eightBall());
If everything went well, you will see two messages in your console: "Does my construct work correctly?" and a response from the 8-ball.
Documenting Constructs
In Xill IDE you have access to a documentation section. This is where you can find documentation for all loaded constructs. To make your construct visible in there you can create an xml file that matches the name of the construct. For example, if you have a construct at io.xill.guide.constructs.GreetConstruct
you can create an xml file at /io/xill/guide/constructs/GreetConstruct.xml
and the documentation generator will load the file.
Below you can find a fully expressed example of the documentation model.
<?xml version="1.0" encoding="utf-8"?>
<function>
<description>
Returns true if the value is contained in the given list or object. Otherwise false.
</description>
<examples>
<example title="Usage">
<code>
use Collection;
var list = ["a", "b", "c"];
Collection.contains(list, "b"); // TRUE
Collection.contains(list, "e"); // FALSE
var object = {1:"a", 2:"b", 3:"c"};
Collection.contains(object, "b"); // TRUE
Collection.contains(object, "e"); // FALSE
</code>
</example>
</examples>
<searchTags>
contains, value, filter, list, object
</searchTags>
<references>
<reference>remove</reference>
</references>
</function>
You can see several components in this xml file so let's walk through them.
Description
In the description section you can write the main body of the documentation. Here you should describe how your construct works and what valid input and output is. You can get into great detail here and use markdown headings and tables to make your documentation more readable.
Examples
It is extremely useful to a Xill user to have proper code examples. Not unlike this guide, you can read all the documentation you want but until you see some context it will be a lot more clear.
In this section you have two tools at your disposal: header and code. The code element should contain some Xill code that describes how to use that specific construct. The header tag can by used to give a title to a piece of code.
Search Tags
Above the documentation panel there is a search box. To allow for better results you can add some search tags to your documentation. If any of those tags are then present in the search box, your construct will pop up.
References
And finally, if you have constructs that work as a group or that are similar to each other you can reference them here. This will add a link to the documentation page to the referenced construct. You reference a construct by it's name (like System.print). You can leave out the package name if you are referencing a construct in the same package as the documented construct.
Using Services
In the EightBallConstruct
example above we put all the functionality for the construct in a single spot. If we would like to create another construct that fetches a resource using http, we would have to duplicate our code.
A good way to combat code duplication like that is by using services. A service is a class that is responsible for performing a collection of specific operations. Let's take a look a service that performs an http request.
public class HttpService {
private String get(String url) {
try (InputStream stream = new URL(url).openStream()) {
return IOUtils.toString(stream);
} catch(IOException e) {
throw new OperationFailedException(
"get " + url,
e.getMessage(),
e
);
}
}
}
This small service fetches a resource from the internet using an http get request. We can now use this in our constructs. To do this we make use of Guice.
public class EightBallConstruct extends Construct {
private final HttpService httpService;
// Note the @Inject annotation to instruct guice to use this constructor
@Inject
public EightBallConstruct(HttpService httpService) {
this.httpService = httpService;
}
@Override
public ConstructProcessor prepareProcess() {
return new ConstructProcessor(
this:process
);
}
private MetaExpression process() {
String result = httpService.get("https://apis.rtainc.co/twitchbot/8ball");
return fromValue(result);
}
}
Using services is a simple as that. Of course there are more advanced scenarios. For these I would like to refer you to the Guice Wiki.