|
|
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 `XxxxxConstraintGroup and override the method
|
|
|
getGroup(). you should return the name of the Group that you want your
|
|
|
constraints to belong to.
|
|
|
Consider the following example implementation of the
|
|
|
RequirementsConstraintsGroup
|
|
|
|
|
|
public abstract class RequirementConstraintGroup extends @org.fortiss.af3.project.utils.ConstraintsProjectUtils.AF3ProjectConstraintCheckerBase@. {
|
|
|
/**
|
|
|
* @return this function will return the group Name
|
|
|
*/
|
|
|
@Override
|
|
|
public String getGroup() {
|
|
|
return "Requirement Constraints Group";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Note: the name of the group is picked up from the getGroup() method and
|
|
|
will be displayed in the constraints configuration tree.
|
|
|
|
|
|
**4.** add a class in `plugin` extending the class
|
|
|
`XxxxxConstraintGroup`.
|
|
|
Give it a name of the form `XxxxConstraint`
|
|
|
|
|
|
public class XxxxConstraint extends XxxxxConstraintGroup {
|
|
|
/** {@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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
**5.** 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;
|
|
|
|
|
|
**6.** 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
|
|
|
}
|
|
|
|
|
|
**7.** 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);
|
|
|
(...)
|
|
|
}
|
|
|
|
|
|
**8.** 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;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
**9.** 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";
|
|
|
}
|
|
|
|
|
|
**10.** 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;
|
|
|
}
|
|
|
|
|
|
**11.** 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.
|
|
|
|