Object Constraint Language (OCL) is a formal language that is widely used in the modelling community to complement the (UML) models with textual constraints that precisely define additional business rules for the system. 

Despite the benefits of using OCL to create precise software specifications, writing the OCL rules themselves is a challenging task that may lead to a number of verification (are the OCL expressions correct?) and validation (are they the correct OCL expressions?) challenges. Formal methods techniques, such as smart bounded verification, are typically used for the verification part. But for the validation part, we need to be able to “execute” or “evaluate” the OCL expressions over different valid and invalid scenarios to make sure the expressions forbid the invalid ones and allow the valid ones. 

Unfortunately, our search for an OCL interpreter able to run OCL expressions that was suitable to be integrated in the low-code low-modeling BESSER platform came up empty. Not really a surprise, as OCL is probably the only language that has less and less tool support every year. And the few OCL tools that exist (shoutout to USE and Eclipse OCL) are Java-based and by now you should already know that BESSER is Python-based. 

So, at the risk of reinventing the wheel, we decided to go ahead and add full OCL support to our modeling BESSER components, including a brand new OCL interpreter to evaluate OCL constraints on scenarios conforming to BESSER models. 

OCL is widely used in modelling domain and a rich set of tools support OCL interpretation such as eclipse modeling framework. Unfortunately, the available tool sets are only available in java language and to the best of our knowledge, there is no OCL interpreter available in python 🙁

Introducing the BESSER Object Constraint Language (B-OCL) Interpreter

The first-ever open source OCL interpreter in python language. The beta version is available on GitHub and it is ready to use. We have also provided two example BESSER models and sample constraints in the models’ folder.

Remember that in the core BESSER platform we already provided the OCL metamodel and parser so you were already able to annotate your B-UML models with OCL constraints. Or better said, with B-OCL constraints as we don’t claim to have fully compliant support with the OCL standard but a more pragmatic approach for now. 

The B-OCL interpreter adds to that the capability to actually execute those constraints and evaluate them on top of an object model, where each object is an instance of a B-UML class. The figure below shows the workflow for our B-OCL interpreter

How to define and evaluate OCL constraints

Let’s now see each step in more detail.

First Step: Defining a domain Model

In the first step, we need a domain model. Let’s take an example of a simple team-player domain model containing a team and player class as shown in the figure below

The corresponding B-UML code for defining the classes and association in the diagram is shown below (just keep in mind you don’t need to write this code by hand, BESSER has importers to automatically generate the model description from different sources!):

# Primitive DataTypes
t_int: PrimitiveDataType = PrimitiveDataType("int")
t_str: PrimitiveDataType = PrimitiveDataType("str")
t_date: PrimitiveDataType = PrimitiveDataType("date")

# Team attributes definition
team_name: Property = Property(name="name", type=t_str)
team_city: Property = Property(name="city", type=t_str)
team_division: Property = Property(name="division", type=t_str)
# Team class definition
team: Class = Class (name="team", attributes={team_name,team_city,team_division})

# Player attributes definition
name: Property = Property(name="name", type=t_str)
age: Property = Property(name="age", type=t_int)
position: Property = Property(name="position", type=t_str)
jerseyNumber: Property = Property(name="jerseyNumber", type=t_int)

# Player class definition
player: Class = Class (name="player", attributes={name, age, position,jerseyNumber})

# team-player association definition
hasteam: Property = Property(name="many",type=team, multiplicity=Multiplicity(1, 1))
many: Property = Property(name="has", type=player, multiplicity=Multiplicity(0, "*"))
team_player_association: BinaryAssociation = BinaryAssociation(name="team_player_asso", ends={hasteam, many})

Now, let’s define some OCL constraints, starting with a simple one that the age should be greater than 10.

context player inv inv1: self.age > 10

And the corresponding B-UML code is:

constraintPlayerAge: Constraint = Constraint(name = "playerAge", context=player, expression="context player inv inv1: self.age > 10", language="OCL")

Similarly, let’s have a constraint where less than three players can play center:

context team inv inv2: self.many -> collect(p:player| p.position = 'center')->size()<3

And the corresponding B-UML code is:

constraintTeamCenter: Constraint = Constraint(name = "teamCenter", context=team, expression="context team inv inv2: self.many -> collect(p:player| p.position = 'center')->size()<3", language="OCL")

Let’s combine all this into a domain model with the code:

team_player_model : DomainModel = DomainModel(name="Team-Player model", types={team, player}, associations={team_player_association}, constraints={constraintPlayerAge, constraintTeamCenter})

Second Step: Defining an object Model

In the second step, we need to define an object model for these constraints to be validated on. Now, let’s create the objects for our team-player domain model as shown in the figure below:

And its corresponding B-UML code:

# Team object attributes
teamObjectName: AttributeLink = AttributeLink(attribute=team_name, value=DataValue(classifier=t_str, value="test-3"))
teamcity: AttributeLink = AttributeLink(attribute=team_city, value=DataValue(classifier=t_str, value="not important"))
teamDivision: AttributeLink = AttributeLink(attribute=team_division, value=DataValue(classifier=t_str, value="junior"))
# Team object
teamObject: Object = Object(name="team object", classifier=team, slots=[teamObjectName,teamcity,teamDivision])

# player object attributes
player1_obj_name: AttributeLink = AttributeLink(attribute=name, value=DataValue(classifier=t_str, value="test"))
player1_obj_age: AttributeLink = AttributeLink(attribute=age, value=DataValue(classifier=t_int, value=12))
player1_obj_position: AttributeLink = AttributeLink(attribute=position, value=DataValue(classifier=t_str, value="center"))
player1_obj_JN: AttributeLink = AttributeLink(attribute=jerseyNumber, value=DataValue(classifier=t_int, value=10))

# Player object
player_1_obj: Object = Object(name="playerTest1", classifier=player, slots=[player1_obj_name, player1_obj_age,player1_obj_position,player1_obj_JN])

# player 2 object attributes
player2_obj_name: AttributeLink = AttributeLink(attribute=name, value=DataValue(classifier=t_str, value="test-2"))
player2_obj_age: AttributeLink = AttributeLink(attribute=age, value=DataValue(classifier=t_int, value=15))
player2_obj_position: AttributeLink = AttributeLink(attribute=position, value=DataValue(classifier=t_str, value="center"))
player2_obj_JN: AttributeLink = AttributeLink(attribute=jerseyNumber, value=DataValue(classifier=t_int, value=11))

# Player object
player_2_obj: Object = Object(name="playerTest2", classifier=player, slots=[player2_obj_name, player2_obj_age,player2_obj_position,player2_obj_JN])

# player1 team object link
player_1_link_end: LinkEnd = LinkEnd(name="many", association_end=hasteam, object=player_1_obj)
team_1_link_end: LinkEnd = LinkEnd(name="has", association_end=many, object=teamObject)
team_player_link_1: Link = Link(name="team_player_link_1", association=team_player_association, connections=[player_1_link_end,team_1_link_end])

# player2 team object link
player_2_link_end: LinkEnd = LinkEnd(name="many", association_end=hasteam, object=player_2_obj)
team_2_link_end: LinkEnd = LinkEnd(name="has", association_end=many, object=teamObject)
team_player_link_2: Link = Link(name="team_player_link_2", association=team_player_association, connections=[player_2_link_end,team_2_link_end])

# Object model definition
object_model: ObjectModel = ObjectModel(name="Object model", instances={teamObject, player_1_obj,player_2_obj}, links={team_player_link_1,team_player_link_2})

Third Step: Evaluating the constraint

For the third step, we will define as an example a test case that will iteratively parse the constraints and interpret them based on the values of the object model. Here is the python code:


from models.team_player_object import team_player_model as domain_model, object_model
from bocl.OCLWrapper import OCLWrapper

if __name__ == "__main__":
    wrapper = OCLWrapper(domain_model, object_model)
    for constraint in domain_model.constraints:
        print("Query: " + str(constraint.expression), end=": ")
        res = None
        try:
            res = wrapper.evaluate(constraint)
            print('\x1b[0;30;35m' + str(res) + '\x1b[0m')

        except Exception as error:
            print('\x1b[0;30;41m' + 'Exception Occured! Info:' + str(error) + '\x1b[0m')
            res = None
    

And the output for this code will be:


Query: context team inv inv2: self.many -> collect(p:player| p.position = 'center')->size()<3: True
Query: context player inv inv1: self.age > 10: True
    

showing how the OCL interpreter was able to successfully evaluate the modeled constraints on our set of objects. 

Ready to give it a try? The complete code is available on our GitHub repository. Looking forward to your feedback and contributions!

Want to build better software faster?

Want to build better software faster?

Get the latest news in software modeling, model-based and low-code development

Thanks for your interest. Check your inbox and confirm your subscription!

Pin It on Pinterest

Share This