Ch. 8 Creating a Custom Model Validator
Jellyfish provides an API that can be used to extend the System Descriptor language. One of these extension points is the ability to create custom validators that can validate models, data, or other modeling elements. Product teams can create validation rules they want to enforce in their models. These validation rules are applied
- when an SD project is edited with Eclipse
- when an SD project is built with Gradle using the command
./gradlew build
- when an SD project is validated with Jellyfish using the command
jellyfish validate
Validation rules are packaged in validation plugins which are just JARs or OSGi bundles.
Creating a New Validator
Validators are created in Java by implementing the ISystemDescriptorValidator
interface. A new project needs to be
created that contains the classes for the validator. Users can use whichever build tool they prefer; this example will
use Gradle.
First, we need to ensure our project contains a settings.gradle
file to name the project:
settings.gradle
rootProject.name = 'modeling.validation'
Next, we need to configure the project to include the relevant dependencies:
build.gradle
group = 'com.mystuff'
version = '1.0.0-SNAPSHOT'
dependencies {
api "com.ngc.seaside:systemdescriptor.model.api:$jellyfishVersion"
api "com.ngc.seaside:systemdescriptor.service.api:$jellyfishVersion"
implementation "com.google.inject:guice:4.1.0"
implementation "com.google.inject.extensions:guice-multibindings:4.1.0"
}
The first two dependencies contain the API for extending Jellyfish. The Guice dependencies are required because
Jellyfish uses Guice for dependency injection. Note the property $jellyfishVersion
should reference the version
of Jellyfish the product team is using. The version of Guice should match the version that Jellyfish uses. You can
find this version in the
versions.gradle file.
Additional Gradle setup
A Gradle project actually requires additional setup. IE, it is usually necessary to apply the Java and bnd plugins to a
Java project. The complete configuration of a Java project with Gradle is outside the scope of this guide, so we
omit the details of the full project.
Implementing the Validator
Validation plugins need to implement the ISystemDescriptorValidator
interface. Most of the time, the
AbstractSystemDescriptorValidator
can be extended instead of having to implement the interface directly. When
extending this class, simply override the appropriate method. The various methods are invoked to validate different
parts of the model. For example, override validateModel
to validate a model object or override validateScenario
to
validate a scenario. Actual validation is accomplished using the IValidationContext
. The declare method can be used
to declare errors, warnings, or suggestions. For example, the implementation below requires a model not be named “Foo”.
ExampleModelValidator.java
public class ExampleModelValidator extends AbstractSystemDescriptorValidator {
@Override
protected void validateModel(IValidationContext<IModel> context) {
IModel model = context.getObject();
if ("Foo".equals(model.getName())) {
context.declare(Severity.ERROR, "Foo is not a valid model name!", model).getName();
}
}
}
Note that the 3rd argument passed to declare
must be the original object obtained via context.getObject()
.
The declare
method returns an instance of this object, so you invoke a getter on the result that contains the invalid
property or value. In the example above, we invoked getName
to indicate the name field was invalid for the model. You
can invoke most any getter method on the object that is being validated after calling declare
to declare an error or
issue on that field. However, you cannot declare an error on the following:
getFullyQualifiedName
of either theIModel
orIData
interface. This is a derived property, declare an error on the name field of the model instead.- You cannot declare an error on a named child of any object (ie, something that implements
INamedChild
). Instead, you need to validate the child during the appropriate callback. For example, if you wanted to validate a scenario of a model, you would overridevalidateScenario
and perform that check in that method, not invalidateModel
. - Note that all the children of a model may not be available during validation since validation is performed live as parsing progresses. Always be sure to validate an object in the correct callback.
You can use the utilities in the SystemDescriptors
to determine if an IScenarioStep
is a Given, When, or Then step,
if a IDataReferenceField
is an input or output field, or if a IModelReferenceField
is a part or required model. For
example:
ExampleModelValidator.java
@Override
protected void validateDataReferenceField(IValidationContext<IDataReferenceField> context) {
IDataReferenceField field = context.getObject();
if (SystemDescriptors.isInput(field)) {
// Validate input here.
} else if (SystemDescriptors.isOutput(field)) {
// Validate output here.
}
}
A validator is a Guice-managed component. This means you can get dependencies injected via Guice with the @Inject
annotation. For example, you could log something in your validator like this:
ExampleModelValidator.java
public class ExampleModelValidator extends AbstractSystemDescriptorValidator {
private final ILogService logService;
@Inject
public ExampleModelValidator(ILogSerivce logSerivce) {
this.logService = logService;
}
@Override
protected void validateModel(IValidationContext<IModel> context) {
IModel model = context.getObject();
if ("Foo".equals(model.getName())) {
logService.error(ExampleModelValidator.class, "Found an invalid model name!");
context.declare(Severity.ERROR, "Foo is not a valid model name!", model).getName();
}
}
}
Packaging the Validator
Once the validator is complete, you need to write a Guice Module that will register the validator. Continuing the
example above, we will create a new module called ExampleModelValidatorModule
:
ExampleModelValidatorModule.java
public class ExampleModelValidatorModule extends AbstractModule {
@Override
protected void configure() {
// Always use a multibinder when binding validators since there is more than one implementation.
Multibinder<ISystemDescriptorValidator> multibinder = Multibinder.newSetBinder(
binder(),
ISystemDescriptorValidator.class);
multibinder.addBinding().to(ExampleModelValidator.class);
}
}
If your validator JAR contains multiple validators, bind them all here as shown above.
We need to tell Jellyfish how to find module implementations. Create a new file called com.google.inject.Module
in
the directory src/main/resources/META-INF/services/
. Inside the file, list the fully-qualified class name of the
module. Assume the module is contained in the package com.mystuff.modeling.validation.module
. The file would
look like this:
com.google.inject.Module
com.mystuff.modeling.validation.module.ExampleModelValidatorModule
If you have multiple modules, list them line by line.
Finally, we need to configure the package that contains the module to be exported. This is needed when deploying the
plugin inside Eclipse. By always doing this, we can use the same plugin both in Eclipse and with the JellyFish CLI that
runs outside of Eclipse. Assume the module is contained in the package
com.mystuff.modeling.validation.module
. Include the following in your build.gradle
:
build.gradle
jar {
manifest {
attributes('Export-Package': 'com.mystuff.modeling.validation.module')
}
}
Using the New Validator
When we build the validator project with the command ./gradlew clean build
, a JAR file is produced in the directory
build/libs/
. We need to deploy this JAR file to be able to actually use the validator. We can deploy the JAR a number of
ways depending on the use case.
Using the Plugin with Gradle
The easiest way to use the use the validator is to configure it with Gradle. We can do this by publishing the JAR that
contains the validator and by editing the build.gradle
of a System Descriptor project we want the validator to be
applied to.
First, we need to upload our JAR to a remote repository so it can be used when building System Descriptor projects. Run
the command ./gradlew clean build publish
from the directory that contains the validation project to upload the JAR.
Recall our SoftwareStore
example contains a build.gradle
file that looks like this:
build.gradle
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
classpath 'com.ngc.seaside:jellyfish.cli.gradle.plugins:2.13.0'
}
}
apply plugin: 'com.ngc.seaside.jellyfish.system-descriptor'
group = 'com.ngc.seaside'
version = '1.0.0-SNAPSHOT'
We need to update this file to list our new validator JAR as a buildscript dependency. We do this by editing the
dependencies
section of the buildscript
section:
build.gradle
buildscript {
repositories {
mavenLocal()
maven {
url nexusConsolidated
}
}
dependencies {
classpath 'com.ngc.seaside:jellyfish.cli.gradle.plugins:2.13.0'
// Declare the group, artifact, and version of the JAR that contains the validator here.
classpath 'com.mystuff:modeling.validation:1.0.0-SNAPSHOT'
}
}
apply plugin: 'com.ngc.seaside.jellyfish.system-descriptor'
group = 'com.ngc.seaside'
version = '1.0.0-SNAPSHOT'
We can now run ./gradlew clean build
on the SoftwareStore
project to build the project. The custom validator will
be used to ensure that no model declared in the SoftwareStore
project has a name called Foo
.
Using the Plugin with the Jellyfish Command Line Interface
The Jellyfish CLI looks for extra JARs in the folder
$JELLYFISH_USER_HOME/plugins
or %JELLYFISH_USER_HOME%\plugins
. If the JELLYFISH_USER_HOME
environment variable
is not set, it defaults to ~/.jellyfish
or %USERPROFILE%\.jellyfish
. When running a Jellyfish command directly
(such as validate
or create-java-service-project
) we can include the validator in Jellyfish by copying the JAR
to the plugins/
directory of JELLYFISH_USER_HOME
. The validator will be automatically used when running Jellyfish.
Deploying the Plugin to Eclipse
This deployment option is used to show validation errors and warnings directly inside Eclipse as a modeler is editing files. It is necessary to package the JAR into an Eclipse update site to be able to use the JAR inside Eclipse.
In this example, we are going to package our validator in its own update site that is separate from the update site for Jellyfish. This will require users to first install the Jellyfish update site and then the custom update site. Some teams may prefer to produce an update site that also installs Jellyfish itself so users don’t have to configure multiple update sites. See the project for the Jellyfish update site to see how to create such an update site.
Configuring the Update Site Project
We’ll need to configure a new Gradle project. This project will produce a ZIP file which is the update site. The
build.gradle
file should look something like this:
build.gradle
buildscript {
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
classpath "com.ngc.seaside:gradle.plugins:$seasidePluginsVersion"
}
}
apply plugin: 'com.ngc.seaside.repository'
apply plugin: 'com.ngc.seaside.eclipse.updatesite'
apply plugin: 'com.ngc.seaside.eclipse.p2'
eclipseDistribution {
linuxDownloadUrl = "https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/photon/R/eclipse-dsl-photon-R-linux-gtk-x86_64.tar.gz"
windowsDownloadUrl = "https://www.eclipse.org/downloads/download.php?file=/technology/epp/downloads/release/photon/R/eclipse-dsl-photon-R-win32-x86_64.zip"
}
eclipseUpdateSite {
def myStuffFeature = feature {
id = 'com.mystuff.modeling.feature'
label = 'My Stuff Jellyfish Extensions'
version = project.version
providerName = 'MyStuff, Inc'
description {
url = 'http://www.mystuff.com/description'
text = 'Extra extensions for Jellyfish.'
}
copyright {
url = 'http://www.mystuff.com/copyright'
text = project.resources.text.fromFile(project.file('src/main/resources/license.txt')).asString()
}
license {
url = 'http://www.mystuff.com/license'
text = copyright.text
}
plugin {
id = 'com.mystuff.modeling'
version = '0.0.0'
unpack = false
}
}
category {
name = 'my_stuff_category_id'
label = 'MyStuff'
description = 'Eclipse Plugin for MyStuff'
feature myStuffFeature
}
}
dependencies {
plugin "com.mystuff:modeling:$version"
}
The eclipseDistribution
section configures the version of Eclipse to use to actually build the update site.
eclipseUpdateSite
is used to configure the update site itself. This configuration declares the license, copyright,
etc of the software. Note the plugin
section is where we declare the JARs that make up the software. A feature
can reference any number of plugins. Plugins must be built as OSGi JARs. Attempting to reference a plain JAR in
an update site will not work correctly.
Next, the MyStuff
category is declared. This configures how Eclipse shows installable features to users.
Finally, we need to declare a plugin
dependency on each JAR that is referenced as a plugin in the update site.
We can now run the command ./gradlew clean build
to build the update site. The resulting ZIP file in the build/
directory is the update site. It can be installed into Eclipse using the same instructions as
installing Jellyfish
into Eclipse.