-
Notifications
You must be signed in to change notification settings - Fork 8
Plugin Development
JDK 10 installed on you computer.
Gradle 4.8.1
Clone CUBA CLI sources and install it to you local m2 repository using ./gradlew install
command.
You can also use CUBA CLI binaries from the repository https://repo.cuba-platform.com/content/groups/work.
Add to your build.gradle
:
repositories {
maven {
url "https://repo.cuba-platform.com/content/groups/work"
}
}
The main extension point for CUBA CLI is com.haulmont.cuba.cli.CliPlugin
interface. To make CLI able to load your plugin you should implement this interface. CLI will load it with the ServiceLoader
and register in Guava event bus. Also module-info.java should declare that it provides CliPlugin with your implementation.
CLI may be started in two modes.
-
SHELL
is an interactive mode, when CLI takes user command, evaluate it and waiting for next command. -
SINGLE_COMMAND
is mode, that runs only one command, that user specified in command line parameters. Plugin can get current mode fromInitPluginEvent
.
Life cycle is served by Guava EventBus. After plugin loading it will be registered in event bus. To subscribe an event simply add void method with single parameter of the event type and mark the method with an annotation com.google.common.eventbus.Subscribe
.
Available events are:
com.haulmont.cuba.cli.event.InitPluginEvent
com.haulmont.cuba.cli.event.BeforeCommandExecutionEvent
com.haulmont.cuba.cli.event.AfterCommandExecutionEvent
com.haulmont.cuba.cli.event.ModelRegisteredEvent
com.haulmont.cuba.cli.event.DestroyPluginEvent
-
com.haulmont.cuba.cli.event.ErrorEvent
After CLI is launched it firesInitPluginEvent
, and all subscribed plugins may register their commands. Before CLI is closed it firesDestroyEvent
.
To create your own command you should create a class for it that implements com.haulmont.cuba.cli.commands.CliCommand
interface. By the way, more convenient would be extending com.haulmont.cuba.cli.commands.AbstractCommand
class or com.haulmont.cuba.cli.commands.GeneratorCommand
, if your command will generate some content.
Package of the command should be marked as open in module-info.java.
You can add parameters to command that will be input after command name. To add them simply create corresponding field and mark them with [com.beust.jcommander.Parameter] annotation. You can get more information about command parameters in JCommander documentation. To add command description, mark it with com.beust.jcommander.Parameters
annotation and specify com.beust.jcommander.Parameters.commandDescription
value.
To register your command, subscribe your plugin to InitPluginEvent
. The event contains special CommandsRegistry
with that you can register your commands and their sub-commands as following:
@Subscribe
fun onInit(event: InitPluginEvent) {
event.commandsRegistry {
command("command-name", SomeCommand()) {
command("sub-command-name", SomeSubCommand())
}
}
}
CUBA CLI has special mechanisms to generate code or another content with special templates. To use these mechanisms, it is preferable your command to implement com.haulmont.cuba.cli.commands.GeneratorCommand
.
GeneratorCommand has special lifecycle.
- Prompting
GeneratorCommand.prompting
is the first phase, at which user is asked with questions about ahead generated artifact. - After that, the command creates artifact model based on the prompting phase user answers and register it in the
cliContext
by name retrieved fromgetModelName
. - At generation phase, the command gets all available models as
Map<String, Any>
and generates artifact.
There is a sort of DSL to prompt user about artifact parameters. It is quite simple, has some question types, default values and validation. You can view an example in any CUBA CLI command GeneratorCommand.
The generation is based on templates and models. Every model is often a simple POJO class that describes future generated artifact parameters. Models stored in cliContext, and every of them has own name. If command is launched in existing CUBA project, cliContext will be store ProjectModel
by name project
. GeneratorCommand
registers newly created model in cliContext after prompting phase. You also can register additional models in your plugin on BeforeCommandExecutionEvent
. Actually, as model is a class without any restrictions, you can register anything as model, for example, some class with helper functions.
You can read about templates structure here, but there is one difference, that templates you use in the commands doesn't need to have template.xml descriptor. Variables are allowed in template directories names. Records like ${varpath}
will be substituted with corresponding variable from Velocity Context. Records like $[packageNameVariable]
is used to automatically convert package names to directories names. Packages of all your models should be opened in order to Apache Velocity may access them through reflexion.
To generate code or another content from template, use com.haulmont.cuba.cli.generation.TemplateProcessor
. If you implement GeneratorCommand class, you will have to implement generate
method, that takes bindings
parameter. This bindings
parameter is a map when key is a model name and value is a corresponding model object. In most cases generation looks like
TemplateProcessor(templatePath, bindings) {
transformWhole()
}
Besides transformWhole
method, that processes every file in template directory as it is a Apache Velocity template, you can use methods copy(from, to)
, transform(from, to)
and copyWhole
.
CUBA CLI uses Kodein-DI as a dependency injection container. All default dependencies are available throw the com.haulmont.cuba.cli.kodein
instance. But if you need to provide your own dependencies in your plugin, you can extend default kodein, and use it inside your plugin.
import com.haulmont.cuba.cli.kodein
val localKodein = Kodein {
extend(kodein)
bind<Worker>() with singleton { Worker() }
}
...
private val worker: Worker by localKodein.instance()
Lets create simple CUBA CLI plugin that will be open current project in IntelliJ IDEA. We will add command idea
that will firstly create .ipr file by invoking gradlew idea
and than communicate through TCP with CUBA idea plugin to open project. You can get all code here.
First of all, lets create new project.
- Open IntelliJ IDEA, choose
Create New Project
. - Choose Java 10 SDK, at
Additional Libraries and Frameworks
section checkJava
andKotlin (Java)
options. PressNext
, input Group id and Artifact name. - Open
build.gradle
and paste following code:
buildscript {
ext.kotlin_version = '1.2.41'
repositories {
mavenCentral()
maven {
credentials {
username System.getenv('HAULMONT_REPOSITORY_USER') ?: 'cuba'
password System.getenv('HAULMONT_REPOSITORY_PASSWORD') ?: 'cuba123'
}
url System.getenv('HAULMONT_REPOSITORY_URL') ?: 'https://repo.cuba-platform.com/content/groups/work'
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
group 'com.haulmont' // Don't forget to specify your group id
version '1.0-SNAPSHOT'
def moduleName = "cli.plugin.tutorial" // You can use your module name
apply plugin: 'kotlin'
apply plugin: 'maven'
apply plugin: 'application'
sourceCompatibility = 10
targetCompatibility = 10
repositories {
mavenCentral()
mavenLocal()
maven {
credentials {
username System.getenv('HAULMONT_REPOSITORY_USER') ?: 'cuba'
password System.getenv('HAULMONT_REPOSITORY_PASSWORD') ?: 'cuba123'
}
url System.getenv('HAULMONT_REPOSITORY_URL') ?: 'https://repo.cuba-platform.com/content/groups/work'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.haulmont.cuba.cli:cuba-cli:1.0-SNAPSHOT"
testCompile group: 'junit', name: 'junit', version: '4.12'
}
[compileKotlin, compileTestKotlin].each {
it.kotlinOptions.jvmTarget = '1.8'
}
compileJava {
inputs.property("moduleName", moduleName)
doFirst {
options.compilerArgs = [
'--module-path', classpath.asPath,
'--patch-module', "$moduleName=${compileKotlin.destinationDir}"
]
classpath = files()
}
}
/**
* The task allows us to quickly put plugin to special plugins directory.
*/
task installPlugin(dependsOn: jar, type: Copy) {
inputs.files jar.outputs.files
jar.outputs.files.each {
from it
}
into System.getProperty("user.home") + "/.haulmont/cli/plugins/"
}
Don't forget to specify your group id.
Create src/main/java/module-info.java file with following content:
import com.haulmont.cuba.cli.CliPlugin;
module cli.plugin.tutorial {
requires java.base;
requires kotlin.stdlib;
requires kotlin.reflect;
requires jcommander;
requires com.haulmont.cuba.cli;
requires com.google.common;
requires kodein.di.core.jvm;
requires kodein.di.generic.jvm;
requires practicalxml;
opens com.haulmont.cli.tutorial;
exports com.haulmont.cli.tutorial;
// This line very important, as it indicates that module provides its own implementation of CliPlugin and ServiceLoader could load it.
provides CliPlugin with com.haulmont.cli.tutorial.DemoPlugin;
}
In src/main/kotlin/ create package com.haulmont.cli.tutorial
. In it create new kotlin class DemoPlugin
and implement com.haulmont.cuba.cli.CliPlugin
. This class will be our plugin entry point.
Lets add method that will register our plugin command.
@Subscribe
fun onInit(event: InitPluginEvent) {
event.commandsRegistry {
command("idea", IdeaOpenCommand())
}
}
Our plugin doesn't have class IdeaOpenCommand
so lets create it in the same package. As it will not generate any content from templates it is preferable to extend it from AbstractCommand
. We can also add command description, so we could see it by invoking help
command.
@Parameters(commandDescription = "Opens project in IntelliJ IDEA")
class IdeaOpenCommand : AbstractCommand() {
...
}
First of all, there is no sense, if command invoking not in CUBA project directory. So add project existence precheck.
override fun preExecute() = checkProjectExistence()
Insert the rest code, that generates .ipr file and opens project in IDEA.
override fun run() {
// From context we can get project model, and than get it name
val model = context.getModel<ProjectModel>(ProjectModel.MODEL_NAME)
val iprFileName = model.name + ".ipr"
// This class helps to find project files, such as build.gradle, modules source, etc.
val projectStructure = ProjectStructure()
val hasIpr = projectStructure.path.resolve(iprFileName).let { Files.exists(it) }
if (!hasIpr) {
val currentDir = projectStructure.path.toAbsolutePath()
val createIprGradle = "${currentDir.resolve("gradlew")} idea"
// Exec gradle task
val process = Runtime.getRuntime().exec(createIprGradle, emptyArray(), currentDir.toFile())
process.waitFor()
}
// CUBA IDEA plugin listens to this port
val url = URL("""http://localhost:48561/?project=${projectStructure.path.resolve(iprFileName).toAbsolutePath()}""")
sendRequest(url)
}
private fun sendRequest(url: URL) {
try {
val connection = url.openConnection()
connection.connect()
InputStreamReader(connection.getInputStream(), "UTF-8").use { reader ->
val response = CharStreams.toString(reader)
val firstLine = response.trim { it <= ' ' }.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]
if (!firstLine.startsWith("OK")) {
// fail method stops command execution and prints error
fail("Unable to connect to the IDE. Check if the IDE is running and CUBA Plugin is installed.")
}
}
} catch (e: IOException) {
fail("Unable to connect to the IDE. Check if the IDE is running and CUBA Plugin is installed.")
}
}
In terminal execute ./gradlew installPlugin
. Now, cli will load your plugin on startup. Lets test it.
- Ensure, that CUBA CLI is installed in your system.
- In terminal execute
cuba-cli
. - Run
create-app
. Fill the parameters. - Run
idea
. If you don't have IDEA launched on your PC, you will see error message. Launch it and repeat.
Debug is very important part of development. There is short instruction how to debug your plugin.
- Open CUBA CLI installation directory.
- Make copy of
cuba-cli/cuba-cli.bat
and name itcuba-cli-debug/cuba-cli-debug.bat
- Open created file and replace
JLINK_VM_OPTIONS=
withJLINK_VM_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005
. - In the IntelliJ IDEA create new
Remote
run configuration on port 5005. - Run
installPlugin
gradle task. - In the terminal launch
cuba-cli-debug
. It will hang and wait for debugger. - Start created
Remote
debug configuration in the IntelliJ IDEA. - Now you can debug your plugin.