Skip to content
Nicolas Rodriguez edited this page Feb 16, 2018 · 23 revisions

JSBML Developer Wiki

Offline Validation

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.

Quick Tour

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. For SBMLDocument, it will find SBMLDocumentConstraints, SBaseConstraints and TreeNodeConstraints.
  • For each of the ConstraintDeclaration classes, the system will ask them to give a list of error codes associated with ValidationFunction (with the help of the methods addErrorCodesForCheck and getValidationFunction).
  • The next step is the actual validation of the SBMLDocument instance. All check methods from all ValidationFunction are called, giving the SBMLDocument 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 the TreeNodeConstraints 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:

Actors

  • ValidationContext
    • Loads constraints
    • Starts validation
    • Unlimited ValidationListeners can be added
  • ValidationListener
    • Interface
    • Used to rack validation in real time
  • LoggingValidationContext
    • Subclass of ValidationContext
    • Implements ValidationListener
    • Creates a SBMLErrorLog during validation

Constraints

  • 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
  • ValidationConstraint<T>
    • Basic constraint
    • Uses a ValidationFunction<T> to perform a check
  • ValidationFunction<T>
    • Functional interface
    • Used to create a ValidationConstraint<T>

Factories

  • 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

How it's work

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:

  1. 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
  2. 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 a ConstraintDeclaration. To avoid double checking, the factory remembers already visited classes. Be aware, that multiple calls of loadConstraint(*) or loadConstraintForAttribute(*) will override each other. There can always be only one set of constraints in a context.
  3. 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 the check(*) method of the root constraint after checking for null. If the root constraint is null, no rules exist and therefore the object must be valid.

Take control of the validation

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

How constraints are loaded

When the ConstraintFactory is looking for ConstraintDeclarations 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 in SpeciesConstraints)

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.

Add Constraints

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;
}
  1. 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 a ConstraintGroup which contains all the constraints.

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

  3. 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 returns null the constraint will be ignored. Remember that ValidationConstraints are cached and therefore the same ValidationFunction 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 the ValidationContext instead. This will help to provide easy compatibility checks. If you need to share information between constraints you could use the HashMap of the context. This HashMap will be cleared by default if the user calls validate(Object o). If you want to keep the data in the HashMap you could use validate(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 the HashMap is NOT cleared when the context heads over to the next child.

Creating SBMLError objects

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.

Validation For SBML Packages

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.