Project

General

Profile

Add a new constraint on a metamodel

Overall concepts

  • Constraint = a function taking a model element and returning true or false (or null or an error)
  • Constraint instance = the application of a constraint to a given model element

Contrarily to usual constraint systems, constraint instances are a first-class citizen for this service
because it allows us to store the status of the constraint on the given element as well as the checksum of the constrained element.

Example:
  • Constraint: "requirement has a non-empty author"
    -> essentially a function taking as input a requirement and returning true if the author field is non-empty
  • Constraint instance 1: "Requirement REQ_01 has a non-empty author"
    -> a persisted model artifact storing the value of the previous function applied on REQ_01, as well as the checksum of REQ_01 when the check was done.
  • Constraint instance 2: "Requirement REQ_02 has a non-empty author"
    -> same thing for REQ_02

Basic recipe

Big picture:

you need to:
  • implement a "verifier" for your constraint by extending the base class AF3ProjectConstraintCheckerBase
  • implement a "verifier UI" by extending the base class ConstraintVerifierUIBaseAutocheck

In details:

1. identify the plugin which is relevant for you constraint -> plugin

2. ensure that the model elements which you want to constrain inherit from the IConstrained interface
(might already be the case - if not just make the corresponding modification in the ecore model)

3. add a class in plugin extending the class org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintCheckerBase.
Give it a name of the form XxxxConstraint

public class XxxxConstraint extends ConstraintsProjectUtils.AF3ProjectConstraintCheckerBase {
    /** {@inheritDoc} */
    @Override
    public IConstraintInstanceStatus verify(IConstrained constrained) {
        // TODO Auto-generated method stub
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isApplicable(IConstrained constrained) {
        // TODO Auto-generated method stub
        return false;
    }
}

4. implement in verify the verification of your constraint. Different values shall be returned depending on the cases:

  • if the verification is successful
    return org.fortiss.tooling.kernel.utils.ConstraintsUtils.createSuccessStatus();
    
  • if the verification is not successful:
    return org.fortiss.tooling.kernel.utils.ConstraintsUtils.createFailStatus();
    
  • if the verification yielded an exception:
    return org.fortiss.tooling.kernel.utils.ConstraintsUtils.createErrorStatus();
    
  • if it is actually not relevant to verify the constraint:
    return null;
    

5. add a method isApplicable which should return true if the constraint is applicable to the given constrained element :

/** {@inheritDoc} */
@Override
public boolean isApplicable(IConstrained constrained) {
  return constrained instanceof Component; // For instance
}

6. in the method PluginActivator.start ("PluginActivator" denotes the activator of your plugin), register your constraint:

/** {@inheritDoc} */
@Override
public void start(BundleContext context) throws Exception {
  super.start(context);
  plugin = this;

  (...)
  IConstraintService.getInstance().registerConstraint(XxxxConstraint.class);
  (...)
}

7. add a class in plugin.ui extending the class org.fortiss.tooling.kernel.ui.extension.base.ConstraintUIBases.ConstraintUIBaseAutocheck.
Call it XxxxxConstraintUI.

public class XxxxxConstraintUI extends ConstraintUIBaseAutocheck {

  /** {@inheritDoc} */
  @Override
  public String getDescription() {
    // TODO Auto-generated method stub
    return null;
  }

  /** {@inheritDoc} */
    @Override
    public boolean shouldBeManuallyActivated() {
        return true;
    }
}

8. implement the getDescription method. This is a short text describing what the constraint ensures:

public String getDescription() {
  return "Every component is so and so xxxx";
}

9. implement the shouldBeManuallyActivated method. This is a boolean deciding whether the user shall be offered the posibility to control if the constraint shall be activated:

public boolean shouldBeManuallyActivated() {
   return true;
}

10. in the method PluginUIActivator.start ("PluginActivator" denotes the activator of your plugin), register your constraint UI :

/** {@inheritDoc} */
@Override
public void start(BundleContext context) throws Exception {
  super.start(context);
  plugin = this;

  (...)
  IConstraintUIService.getInstance().registerConstraintUI(XxxxConstraintUI.class, XxxxConstraint.class);
  (...)
}

Note that registerConstraintUI requires both the UI part of your constraint and its not UI part: this is the point where the connection between both implementations is made.

-> You should now have a standard behavior for your constraint. See below for further customization.

Customization

Customize the icon of your constraint

When your constraint is not satisfied by an element, it shows in the context menu of the element which is not satisfying the constraint.
By default, there is no icon. However you can specify one by overriding the following method in your UI constraint:

/** {@inheritDoc} */
@Override
public ImageDescriptor getIconImageDescriptor() {
  return AF3Activator.getImageDescriptor("icons/myicon.png");
}

Display a warning instead of an error

By default, all unsatisfied constraints display with an error marker.
If you want instead to have a warning marker, override the following method in your UI constraint so that it returns true:

/** {@inheritDoc} */
@Override
public boolean displayAsWarning() {
  return true;
}

Customize when a constraint is automatically outdated

By default, your constraint will be-checked as soon as the constrained element changes (even without the user saving!).
This might happen very often, even though in many cases, some changes (e.g., layout changes) are of no importance for the constraint.

Concrete example: changing the layout of a state automaton does not change anything to the fact that the initial state shall be unique;
therefore you do not want the constraint "initial state should be unique" to be re-run each time the layout changes.

To fix this you can override the following method in your (non-UI) constraint:

/** {@inheritDoc} */
@Override
public void preprocessBeforeChecksum(EObject obj) {
  LayoutDataUtils.filterAllLayoutData(obj);
}

Inside the method just remove the information from obj which is irrelevant for the constraint.
In the snippet above, we make use of the utility method filterAllLayoutData to remove all layout information.

Customize the verification status

Often, you might want to store in the status some information collected during the verification.
This might be useful to provide quick fixes or better error messages.
However, the verification only provides you with a status and no additional information, so you cannot make use of it.
An easy way to circumvent this is to extend the verification status with your own class containing the relevant information.

For instance, if a constraint fails because some elements do not contain a specific property, do not return ErrorVerificationStatus.
Instead, define an Ecore class "MyErrorStatus -> ErrorVerificationStatus" containing a reference to the elements which do not have the property.
Your fixes will then have directly access to these elements and won't need to recompute them.

Cancel a constraint while it is being checked

Some constraints can be really long to check (e.g., formal verification constraints).
In particular it might happen that the user modifies the model while the previous constraint check is still not finished.
In such a case, the service will attempt to cancel the previous check before starting the new one.
You need however to implement the cancellation of your check manually, which you can do by overriding the following method in your (non-UI) constraint:

/** {@inheritDoc} */
@Override
public void cancel(ConstraintInstance ci) {
  // By default, we do nothing. Not all constraints are so heavy that they deserve to have
  // a cancellation procedure.
}

Provide quick fixes

If relevant, you can provide "quick fixes" to your constraint: these will appear in the context menu when your constraint shows as unsatisfied.
To do so you need to override the method fixes in your UI constraint:

/** {@inheritDoc} */
@Override
public List<IConstraintVerificationUIService.IFix> fixes(ConstraintInstance ci, IConstraintInstanceStatus status) {
  // TODO
}

"What if I would like to implement a fix, but to do it I have to implement something partly similar to checking the constraint itself?":

To implement the fix, you might need some information which you get only during the check of the constraint.
However, the check only provides you with a status and no additional information, so you cannot make use of it.
See the section above on "Customize the verification status" to circumvent this.

Time-consuming constraints: how to improve the user experience?

Some constraints are intrinsically time consuming (e.g., those involving formal verification).
If you do not want these constraints to be automatically checked there is no real problem.
Otherwise, we can try to improve the user experience, i.e. (at the moment of writing these lines):
  • introduce a delay before the automatic check of the constraint
  • warn the user that activating such a constraint might take some resources

To do so just overload the method isTimeConsuming of your constraint UI to make it return true:

/** {@inheritDoc} */
@Override
public boolean isTimeConsuming() {
    return true;
}

Customize the error message

When your constraint is not satisfied, a default message box with a standard message will be displayed.
If you want to customize it, for instance to provide more details about the error, you can override the method getMessage of your constraint UI:

/** {@inheritDoc} */
@Override
public String getMessage(Constraint c, IConstraintVerificationStatus status) {
  if(status instanceof ErrorVerificationStatus) {
    return "My error message here possibly giving more details about the problem";
  }
  return super.getMessage(c, status);
}

Like explained in the section about quick fixes above, you might define your own statuses to store relevant information for the user during the verification.
See the section above on "Customize the verification status".

If you would like to change more than just the error message but even change the pop up or trigger some other behaviour, see the section below on "Customize what happens when a status is opened".

Customize what happens when a status is opened

When your constraint is not satisfied, a default message box with a message will be displayed.
If you want to customize this behaviour, for instance open automatically the elements which are guilty or highlighting some elements in the editor, you can override the following method in your constraint UI:

/** {@inheritDoc} */
@Override
public boolean openStatus(Constraint cstr, IConstraintVerificationStatus status) {
  if(status instanceof SuccessVerificationStatus) {
    // My action here
  }
  return super.openStatus(c, status);
}

Prevent the constraint from being automatically activated

You might want to offer the possibility for the user to manually activate or deactivate a constraint, instead of just letting the constraint being checked permanently.

To do so, just overload the method isTimeConsuming of your constraint UI to make it return true:

/** {@inheritDoc} */
@Override
public boolean shouldBeManuallyActivated() {
  return true;
}

The user will then be able to activate/deactivate the constraint in the "Development process" editor, by double-clicking on the root of the AF3 project.

Manual check instead of automatic

Sometimes, you do not want your constraint to be checked automatically but rather let the user trigger themselves the check.
For instance if the constraint is too heavy to check, or if the check can only be done by a human (e.g., review).

To do so, just make your constraint UI extend org.fortiss.tooling.kernel.ui.extension.base.ConstraintUIBases.ConstraintUIBase
instead of org.fortiss.tooling.kernel.ui.extension.base.ConstraintUIBases.ConstraintUIBaseAutocheck.

Customize the information contained in a constraint

Every object of type Constraint always contains a verification status.
However, you might want it to contain some information, e.g., some timeout
indicating for every element the maximal time allowed for the verification of the constraint.
The best way to customize this is to define an Ecore class extending the type Constraint.

In such a case your will need however to make your constraint (non-UI) extend org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintBase
instead of org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintCheckerBase.

Constrain more than one element at a time

Some constraints focus on more than one element only. For instance, a constraint checking whether a test is up to date relates both a component and a test case.
To tackle this situation, make your (non-UI) constraint extend org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintBase
instead of org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintCheckerBase.

The main differences are as follows:
  • the verify method then takes the constraint as input, and not the constrained element
  • in this method, use utility functions like org.fortiss.tooling.kernel.utils.ConstraintsUtils.getFirstConstrained(Constraint) to access the constrained elements
  • you need to implement the method createConstraintIfNeeded which creates the constraint (or returns null if not relevant)
  • if you constrain two elements only, it should be enough to call org.fortiss.af3.project.utils.ConstraintsProjectUtils.createConstraint(String, IConstrained, IConstrained)

Add constraint support for your own non-AF3 but kernel-based project

The method described above is AF3-dependent, as the dependency on af3.project shows.
However the service has been designed so that you can easily adapt your project to support constraints.
Step-by-step:
  1. in your model, have some class implement IConstraintInstanceContainer (either pick an existing one or create a new one, important thing is that it is contained in the main project - see DevelopmentProcessConfiguration in AF3 for an example). There should be only one constraint instance container per project.
  2. define a compositor for this class, make it inherit org.fortiss.tooling.base.compose.ConstraintInstanceContainerCompositor<DevelopmentProcessConfiguration>. You should not have anything to implement in this class.
  3. in your model, have some class extend ConstraintBasedProcess (either pick an existing one or create a new one, important thing is that it is contained in the main project - see ConstraintBasedDevelopmentProcess in AF3 for an example).
    There should be only one constraint based process element per project.
  4. in your .ui plugin, define a compositor for this class, make it inherit org.fortiss.tooling.base.ui.compose.ConstraintBasedProcessCompositor<ConstraintBasedDevelopmentProcess>. You should not have anything to implement in this class.
  5. define some utility method which allows to retrieve the unique IConstraintInstanceContainer corresponding to any EObject. Say getConstraintInstanceContainer.
  6. define two abstract classes:
    /** Base class to simplify even more migrating from the old to the new constraint system. */
    public static abstract class MyProjectConstraintBase extends ConstraintBases.ConstraintBase {
    
      /** {@inheritDoc} */
      Override
      public IConstraintInstanceContainer getConstraintInstanceContainer(EObject obj) {
        // Here "getConstraintInstanceContainer" is project-specific!
        return getConstraintInstanceContainer(obj);
      }
    }
    
    /** Base class to simplify even more migrating from the old to the new constraint system. */
    public static abstract class MyProjectConstraintCheckerBase extends ConstraintBases.ConstraintCheckerBase {
    
      /** {@inheritDoc} */
      Override
      public IConstraintInstanceContainer getConstraintInstanceContainer(EObject obj) {
        // Here "getConstraintInstanceContainer" is project-specific!
        return getConstraintInstanceContainer(obj);
      }
    }
    
  7. In your UI activator, define a function of signature ConstraintBasedProcess getConstraintBasedProcess(ITopLevelElement top) which, given a top level element is able to retrieve its unique constraint based process.
  8. In the start function of your UI activator, add the following lines:
    ConstraintBasedProcessAwareActivator cstrBasedActivator = new ConstraintBasedProcessAwareActivator(t -> getConstraintBasedProcess(t));
      cstrBasedActivator.start();
    

    (see AF3ProjectUI for an example)

You should now be good to go.
To add a constraint on your metamodel, follow the step-by-step above only replacing AF3ProjectConstraintBase by MyProjectConstraintBase and AF3ProjectConstraintCheckerBase by MyProjectConstraintCheckerBase.

Optional: if you want the user to be able to define their own development process based on constraints, define some editor extending org.fortiss.tooling.base.ui.editor.ConstraintBasedProcessEditor<ConstraintBasedDevelopmentProcess>. You should not have anything to implement in this class.