-
Notifications
You must be signed in to change notification settings - Fork 24
Home
The offline validator is a alternative way to validate a document against the specifications of SBML. As the name will suggest it doesn't need internet connection.
Below is a short description of what happen during validation in plain English:
- The user decide to validate a specific class, for example SBMLDocument. He can ask the validation to be recursive or not and select some check categories if he does not want to apply all of them.
- Then the system try to find all the ConstraintDeclaration that exist for this class. Meaning, it will use java reflection to determines all classes that
SBMLDocument
extends or implements, then for each of those classes, it will try to find, in the classpath, a corresponding constraint declaration. ForSBMLDocument
, it will findSBMLDocumentConstraints
,SBaseConstraints
andTreeNodeConstraints
. - For each of the
ConstraintDeclaration
classes, the system will ask them to give a list of error codes associated withValidationFunction
(with the help of the methodsaddErrorCodesForCheck
andgetValidationFunction
). - The next step is the actual validation of the
SBMLDocument
instance. All check methods from allValidationFunction
are called, giving theSBMLDocument
instance as second argument. If any method returns false, it means that the associated validation rule failed and we build a nice error message to report the problem to the user. - Last step, if the user asked to do the validation recursively, we will use the TreeNode interface methods to get the list of children of the
SBMLDocument
instance and start the process from the start with each of them (this is done in theTreeNodeConstraints
class). - When we have been through all the hierarchy of objects, the validation end and we report the list of errors to the user.
Below is a short description of the most important classes in the offline validator package:
-
ValidationContext
- Loads constraints
- Starts validation
- Unlimited
ValidationListener
s can be added
-
ValidationListener
- Interface
- Used to rack validation in real time
-
LoggingValidationContext
- Subclass of
ValidationContext
- Implements
ValidationListener
- Creates a
SBMLErrorLog
during validation
- Subclass of
-
AnyConstraint<T>
- Generic interface for all constraints
- Generic types indicates which class this constraint can validate
- Constraints uses composite pattern
-
ConstraintGroup<T>
- Contains list of
AnyConstraint<?>
- Children always
!= null
- Contains list of
-
ValidationConstraint<T>
- Basic constraint
- Uses a
ValidationFunction<T>
to perform a check
-
ValidationFunction<T>
- Functional interface
- Used to create a
ValidationConstraint<T>
-
ConstraintFactory
- Singleton
- Collects constraints
- Looks for
ConstraintDeclaration
-
ConstraintDeclaration
/AbstractConstraintDeclaration
- Used to define constraints for a class
- Contains the actual logic for constraints
- One per class
-
SBMLFactory
- Creates
SBMLError
objects from .json
- Creates
One of the most important classes of the offline validator is the ValidationContext
. In the best case, this should be the only class the user will ever need. To reduce overhead, this class is also designed to be reusable.
This his how you setup a validation context and perform a simple validation:
// 1. Obtain a new instance
ValidationContext ctx = new ValidationContext();
// 2. Loading constraints to the context
ctx.loadConstraints(MyClass.class);
// 3. Perform validation
MyClass myObject = new MyClass();
boolean isValid = ctx.validate(myObject);
Notice that the ValidationContext
is capable to validate EVERY class, as long as at least one motherclass or interface provides constraints.
Let's see what these lines of code really do:
- This is a simple constructor call, nothing special here. The result is a default
ValidationContext
with has recursive validation turned on and only loads the constraints for the General SBML Consistency Issues - Here's the real magic behind the validator. The first thing the context does is to obtain the shared instance of the
ConstraintFactory
. The factory now checks every superclass and interface of the given class and tries to find aConstraintDeclaration
. To avoid double checking, the factory remembers already visited classes. Be aware, that multiple calls ofloadConstraint(*)
orloadConstraintForAttribute(*)
will override each other. There can always be only one set of constraints in a context. - This function call triggers the validation. While loading constraints, the context remembers the root class for which the constraints were loaded. Before the validation starts, the context checks if the given object is assignable to the constraint type, if not the validation will return false and print a message to the console. If this test is passed, the
HashMap
of the context will be cleared and context calls thecheck(*)
method of the root constraint after checking fornull
. If the root constraint is null, no rules exist and therefore the object must be valid.
The steps above perform a really simple validation, which only gives a quick result. If the validate(*)
method returns false
you couldn't say how many or which constraints are broken.
If you want to have more informations about the validation process you could add a ValidationListener
to a context. A context can have unlimited amount of these listeners. Each of them has two methods, one of them will be triggered before a constraint will be validated and one afterwards. The second method also gets the result of this constraint (true
if everything is fine, false
otherwise). This informations in combination with the error codes of the constraints could be used to retrieve more information about a broken constraint.
Notice that a ConstraintGroup
will return false
if at least one of their child constraints is broken. You can recognize a ConstraintGroup
either by checking the class of the constraint or by comparing the error code:
class ValidationLogger implements ValidationListener {
public void willValidate(ValidationContext ctx, AnyConstraint<?> c, Object o) {
// using the instanceof operator to filter out groups
if (c instanceof ConstraintGroup){
system.out.println("enter group");
}
}
public void didValidate(ValidationContext ctx, AnyConstraint<?> c, Object o, boolean success) {
// all ConstraintGroups share the same error code
if (c.getErrorCode == CoreSpecialErrorCodes.ID_GROUP) {
system.out.println("leave group");
}
else
{
// log a broken constraint
if (!success)
{
system.out.println("constraint " + c.getErrorCode() + " was broken!");
}
}
}
}
JSBML already provides a context which also creates SBMLError
s for broken constraints and collects them in a SBMLErrorLog
. This context is called LoggingValidationContext
which is a subclass of ValidationContext
that also implements the ValidationListener
interface and listens to itself.
By default the context only enables the check category GENERAL_CONSISTENCY
which contains the most constraints and provide a solid base. To load additional constraints you can add more check categories to the context. After enabling/disabling new categories the constraints must be reloaded to take effect.
When the ConstraintFactory
is looking for ConstraintDeclaration
s it's actually using java reflection to find these classes. A ConstraintDeclaration
must be follow these rules to be recognized:
- Has package 'org.sbml.jsbml.validator.offline.constraints'
- Implements the
ConstraintDeclaration
interface - Follows naming convention: className + "Constraints" (e.g. constraints for
Species
must be declared inSpeciesConstraints
)
Because of these restriction there always can be only ONE ConstraintDeclaration
per class. The easiest way to obtain a ConstraintDeclaration
is to call AbstractConstraintDeclaration.getInstance(className)
where className is the simple name of a class (like "Species", "Compartment", etc.). The AbstractConstraintDeclaration
caches already found declarations and the names of the classes for which no declaration was found.
The AbstractConstraintDeclaration
also caches the constraints to reuse and share them. The key for which a constraint will be stored is a combination between the name of the class for which the constraint applies and his error code (The constraint for the Species
class with error code CORE_20601 will be stored in "Species20601", because the CORE error codes have a offset of 0). This makes it possible to have a constraint with the same error code for different classes, which is in some cases necessary. To avoid caching you could use negative error codes.
The AbstractConstraintDeclaration
is also a good point to start when you want to create your own constraints. It already provides a implementation of the most functions. It's only necessary to implement three functions on your own. The following example will demonstrate how you could create your constraints for MyClass
:
// Be sure to use this package, otherwise the ConstraintFactory won’t find your constraints.
package org.sbml.jsbml.validator.offline.constraints;
// This class will contain the constraints for a MyClass object
public class MyClassConstraints extends AbstractConstraintDeclaration {
// 1. Add your error codes to the set. Use the level, version and category parameter
// to select which error codes are needed.
@Override
public void addErrorCodesForCheck(Set<Integer> set, int level, int version, CHECK_CATEGORY category) {
switch (category) {
case GENERAL_CONSISTENCY:
if (level > 1)
{
// All official SBML error codes a hard coded in the SBMLErrorCodes interface
set.add(CORE_10200);
}
// a small helper function
addRange(set, CORE_10203, CORE_10208);
break;
case IDENTIFIER_CONSISTENCY:
break;
case MATHML_CONSISTENCY:
break;
case MODELING_PRACTICE:
break;
case OVERDETERMINED_MODEL:
break;
case SBO_CONSISTENCY:
break;
case UNITS_CONSISTENCY:
break;
}
}
// 2. Nearly the same as before, but this time you're looking just for a single attribute.
@Override
public void addErrorCodesForAttribute(Set<Integer> set, int level, int version, String attributeName) {
switch (attributeName){
case TreeNodeChangeListener.size:
set.add(CORE_10200);
case "name":
set.add(CORE_10204);
}
}
// 3. Here you provide the actual logic behind the constraints
@Override
public ValidationFunction<?> getValidationFunction(int errorCode) {
ValidationFunction<MyClass> func = null;
switch (errorCode) {
case CORE_10200:
func = new ValidationFunction<MyClass>() {
public boolean check(ValidationContext ctx, MyClass myObject) {
// Always use the level and version of the context and never the values from the object.
// This will make compatibility checks very easy
if (ctx.getLevel() > 1)
{
return myObject.isSetName();
}
return myObject.isSetName() && myObject.isNameUppercase();
}
};
break;
// other cases...
}
return func;
}
-
Use this method to insert all the required error codes into the set. You should use the level, version and category input to select the right codes. The official error codes are implemented as constants in the
SBMLErrorCodes
interface. Because this interface is already implemented in AbstractConstraintDeclaration you could use these constant without putting the class name in front. If the factory should load constraints for multiple check categories, it will use the same set to collect the error codes for the class. But because a set can't contain doubled values it's unnecessary to check if a value is already present. If the set has at least one member, the factory will create aConstraintGroup
which contains all the constraints. -
This method is for attribute validation. In this method you should only add error codes to a set which refers to an error (not a warning) in the given level/version. It's best to collect every constraint which depends in some way from the attribute.
-
Here is the real logic behind the constraints. You should provide one
ValidationFunction
for every error code you add in one of the methods from above. If this method returnsnull
the constraint will be ignored. Remember thatValidationConstraints
are cached and therefore the sameValidationFunction
could be called several times. That means if you use additional data structures like sets or lists in your function, remember to clear them. You should also prevent to use the level/version values from the validation target. Use the values of theValidationContext
instead. This will help to provide easy compatibility checks. If you need to share information between constraints you could use theHashMap
of the context. ThisHashMap
will be cleared by default if the user callsvalidate(Object o)
. If you want to keep the data in theHashMap
you could usevalidate(Object o, boolean clearHashMap)
instead, but beware that this could lead to undefined behaviors if the context had performed a validation before. In recursive validation theHashMap
is NOT cleared when the context heads over to the next child.
If you want to provide feedback to the user what went wrong during the validation a SBMLError
object could be very helpful. Since it's the goal of libSBML and JSBML to share their resources, the informations about an error are stored in a JSON file. This file is actually just a big Dictionary/HashMap, with the (numeric) error code as key for the information. It's located in core/resources/org/sbml/jsbml/resources/SBMLErrors.json
.
{
// ...
"80501": {
"Category": "Modeling practice",
"DefaultSeverity": "warning",
"Message": "As a principle of best modeling practice, the size of a <compartment> should be set to a value rather than be left undefined. Doing so improves the portability of models between different simulation and analysis systems, and helps make it easier to detect potential errors in models.\n",
"Package": "core",
"SeverityL1V1": "na",
"SeverityL1V2": "na",
"ShortMessage": "It's best to define a size for every compartment in a model"
},
// ...
}
As you can see, the object which is stored behind the error code is again a Dictionary/HashMap. Here is a explanation of the possible keys and how to interpret the value (note that every value is typed as String
):
- "Category" indicates to which check category the error belongs. For a full list of error codes and check categories visit sbml.org.
- "DefaultSeverity" This gives you the severity the most level and version uses. There are three different possible values:
- "na" means it's nothing and the error can be ignored.
- "warning" means it won't make your element invalid, but there's something to improve.
- "error" means the element doesn't follow the SBML specifications and could probably cause trouble.
- "Message" gives you a detailed description what is wrong with your element.
- "Packages" indicates the SBML package the error belongs to. Packages was introduced in level 3, so this errors shouldn't be appear in a element with a level prior 3.
- "SeverityL$xV$y" If the error has different severities in some level and versions of SBML, they get a own entry. The key to this (optional) entry is build by replacing $x with the level and $y with the version of SBML you want to use.
- "ShortMessage" is a shorter and simpler description for this error.
In the offline validator package there is also a SBMLFactory
class which loads and caches the .json-file and could try to create a SBMLError
object for a given error code. This should be the preferred way to create SBMLError
objects for the official error codes.
There's no additional setup needed to validate objects which are declared in a different SBML package (or in a custom project). the recursive validation will hit these objects as well, if one of the other JSBML objects returns them as child of there TreeNode
. You could create your own ConstraintDeclaration
in a separated project as long you follow the guide lines above.