USING ASPECT-ORIENTED PROGRAMMING TO INSTRUMENT OCL

2 downloads 0 Views 609KB Size Report
used to generate the Java code from parsed OCL constraints and how ... classes and methods, and localizes it into one place [11]. ...... home/doc/quick.pdf.
Carleton University TR SCE-04-03

Version 1, February 2004

USING ASPECT-ORIENTED PROGRAMMING TO INSTRUMENT OCL CONTRACTS IN JAVA L. C. Briand1,2, W. Dzidek2, Y. Labiche1 1

Software Quality Engineering Laboratory Department of Systems and Computer Engineering – Carleton University 1125 Colonel By Drive, Ottawa, ON, K1S 5B6, Canada {briand, labiche}@sce.carleton.ca 2

Simula Research Laboratory Lysaker, Norway [email protected]

ABSTRACT Analysis and design by contract allows the definitions of a formal agreement between a class and its clients, expressing each party’s rights and obligations. Contracts written in the Object-Constraint Language (OCL) are known to be a useful technique to specify the precondition and postcondition of operations and class invariants in a UML context, making the definition of object-oriented analysis or design elements more precise. In this report, we introduce the ocl2j approach to automatically instrument OCL constraints in Java programs using aspect-oriented programming (AOP). One of the many possible applications of checking contract assertions at run time is to help testing and debugging. The approach strives for automatic, efficient generation of contract code and a nonintrusive instrumentation technique. It is assessed on a case study and initial results show that the approach is viable. We conclude by discussing strategies to optimize instrumentation so as to further decrease overhead.

1 INTRODUCTION Design by contract (DbC) is a technique developed by Bertrand Meyer [26], where the relationship between a class and its clients are viewed as a formal agreement, expressing each party’s rights and obligations. These contracts are expressed with three types of predicates: class invariants, operation preconditions, and operation postconditions. An 1

Carleton University TR SCE-04-03

Version 1, February 2004

invariant is specified in the context of a class and is defined as being true (1) after an instance creation and (2) before and after any remote call to a method that is a call from a client object to a server object. A precondition is specified in the context of an operation of a class and is defined as being true before the execution of the constrained operation. And finally, a postcondition is also specified in the context of an operation of a class and is defined as being true after the execution of the constrained operation. The DbC approach, originally intended to be used during low level design (same abstraction level as the implementation language), was later expanded by several works (like [27]) to be used not only during low level design but also during analysis. When at the analysis and design levels constraints can be expressed in natural language, and then manually hard coded with the rest of the application code at the implementation level. In doing this, we face two major disadvantages: •

The manual translation of the constraints is error-prone, and moreover it makes maintenance of constraints difficult as two versions must be maintained: the constraints in the code and the constraints in the model.



As practice has often shown, information in two different places deviates in meaning over time, and in this case this is aggravated by the fact that the information is in a different format. The source of this deviation can have several roots, for example updating code but not the analysis & design model.

In the context of object-oriented (OO) development, the Unified Modeling Language (UML) is the de facto standard notation to be used during analysis and design and the definition of contracts is facilitated by the Object Constraint Language (OCL) [28]. OCL, which is part of the UML, is a textual, formal specification language that allows for the compact notation of precise, side-effect free constraints on instances of UML models. Part of the compactness of OCL is owed to the fact that classes of UML class diagrams automatically belong to the type system that OCL constraints use. OCL is, for a specification language (based on set theory and first order logic), relatively easy to learn, and is intuitive enough that many OCL constraints can be understood without complete knowledge of the language, at least by those who are familiar with programming 2

Carleton University TR SCE-04-03

Version 1, February 2004

languages and/or basic set theory and first order logic. Amongst other things, this language can be used to define constraints on models in the spirit of the DbC methodology. As OCL can specify constraints on models, using OCL the DbC methodology can be naturally extended to “Analysis by Contract”. In addition to being able to write constraints at the analysis stage, using OCL has many other advantages including: Like UML, OCL is a standard language; OCL is at a higher level of abstraction than OO programming languages leading to more concise and powerful contracts than if they were written in an implementation language. Current and emerging software paradigms have recognized the usefulness of contracts. First, component and distributed object-oriented software (also known as middleware) rely heavily on contacts in order to achieve quality. In [31], a book on the subject of component software, an entire chapter is devoted to the subject of contracts. In that chapter Szyperski observes that using a formal language to specify contracts would be ideal except for the determent of the complexity associated with the usage of a formal language – recent experiments have shown that OCL provides a number of advantages in the context of UML modeling [9], thus suggesting its complexity to be manageable by software engineers. Likewise in [12], a book discussing distributed object-oriented technologies, Emmerich argues that the notion of contracts is paramount in distributed systems as client and server are often developed autonomously. Last, model driven architecture (MDA), also known as model driven development (MDD), is perceived by many as a promising approach to software development [23]. In [23] the authors note that the combination of UML with OCL is at the moment probably the best way to develop high-quality and high-level models, as this results in precise, unambiguous, and consistent models that contain much information about the system to be implemented. The MDA approach is based on the idea that you can convert a high-level model directly to code. Since this approach relies so strongly on OCL it is not unreasonable to assume that for the MDA framework to evolve effective code generating OCL tools are required. Having discussed the advantages of OCL, it comes as a surprise that the language is hardly used at all. One reason for this might be the well-established prejudices against

3

Carleton University TR SCE-04-03

Version 1, February 2004

any formal elements among software development experts and many influential methodologists. Another reason for the unsatisfactory utilization of OCL is the lack of tools. The most basic support for engineers using OCL would be to check constraints for syntactic and semantic correctness. Even this is not offered by state-of-the-art CASE tools like IBM Rational Rose and Borland Together ControlCenter. It is worth noting, though, that the OCL Compiler developed by Cybernetic Intelligence GmbH [17] adds support for syntactic and limited semantic correctness (conflicting constraints are not detected) to IBM Rational Rose. Yet even this is not enough. To gain maximum benefits from using OCL, tools must be able to generate code from OCL constraints and instrument this code into the target program’s code so that the constraints can be enforced at runtime. The benefits of the kind of constraint enforcement discussed above is shown in [8], where a rigorous empirical study showed that contract assertions detected a large percentage of failures and thus can be considered acceptable substitutes to hard-coded oracles in test drivers. This study also showed that they can be used to significantly lower the effort of locating faults after the detection of a failure and that the contracts need not be perfect to be highly effective. Based on such results, the next step was therefore to address the automation of using OCL contracts to instrument systems under test (SUT).This work focuses on the automated verification of OCL contracts, at run time, by translating them into Java and instrumenting them in the SUT they describe using Aspect-oriented programming (AOP) [22]. The pioneering work that was first in providing automation for OCL contract assertion checking was the Dresden OCL toolkit [13, 34]. Unfortunately, a thorough analysis of the tool showed that the proposed solution is only adequate for very small and simple programs with simple constraints. As discussed later in this document, the whole OCL 1.3 notation is not supported and some constraints are incompletely enforced. In addition, due to technical choices made, the strategy is intrusive and the code generated is sometimes less than optimal.

4

Carleton University TR SCE-04-03

Version 1, February 2004

This report is organized in the following manner. Chapter 2 provides the required material to read it: an introduction to the Object Constraint Language, an introduction to aspect-oriented programming, and finally the related work is discussed. Chapter 3 gives an overview of the solution presented in this report. Chapter 4 details the transformations used to generate the Java code from parsed OCL constraints and how aspect-oriented technology and AspectJ apply and are used in this work. Chapter 5 outlines the architecture of the tool developed in this work, ocl2j. Chapter 6 reports on an initial case study. Chapter 7 shows, in detail, how this work can be continued in order to overcome limitations identified in Chapter 6. And finally, Chapter 8 presents our conclusions and points out directions for future work.

2 BACKGROUND & RELATED WORKS This chapter presents a background of material that the reader must be familiar with to fully capture the material introduced in the following Chapters. We first provide a brief introduction to OCL (Section 2.1), and then an introduction to aspect-oriented programming (Section 2.2). Finally, related works are discussed in Section 2.3.

2.1 The Object Constraint Language (OCL) The Object Constraint Language (OCL) is a language that enables one to describe constraints on UML model elements. OCL is a standard query language, based on set theory and first order logic, which is part of the UML set by the Object Management Group (OMG) as a standard for object-oriented analysis and design. OCL is a very important addition to the UML as modellers use it to express those additional nuances of meaning which the diagrams alone cannot represent. In other words, OCL is used to give added precision to the definition of UML. OCL, developed by IBM, is part of the UML from version 1.1 on. This extension has been designed to augment a class diagram with additional information which cannot be otherwise expressed by UML diagrams; previous versions of UML have only allowed the definition of constraints as annotations in an informal textual way. OCL allows the

5

Carleton University TR SCE-04-03

Version 1, February 2004

definition of constraints associated with UML models elements and has also been used for the formalization of the UML metamodel. The introduction of a constraint language is an important step towards the formalization of analysis and design models and it provides precise meaning to model elements such as classes and operations, thus facilitating the interpretation and usage of models [27].

2.2 Aspect-Oriented Programming (AOP) Technology Aspect-oriented programming is a new methodology that facilitates the modularization of concerns in software development. In particular, it extracts scattered concerns from classes and turns them into first-class elements: aspects. For example, code that implements logging services would have to be distributed across all classes and methods in the system that need to have this information logged. However, with aspect-oriented technology, this logging functionality could be pulled out from all the classes to an aspect. Thus, the aspect decouples the code that affects the implementation of multiple classes and methods, and localizes it into one place [11]. By decoupling these concerns and placing them in aspects, the original classes are relived of the burden of managing functionalities orthogonally related to their purpose. Later, the aspect code is injected into appropriate places by a process known as weaving. Aspects contain join points that specify well-defined execution “points” in the execution of the instrumented program where aspect code interacts, e.g., calls to a specific method. Pointcuts describe sets of join points by specifying the objects and methods to be considered, for example. An advice is additional code that should execute before or after join points. It can even have control on whether the join point can run at all. A direct consequence of aspect use is that less code is written, code that would otherwise be spread throughout the system can now be localized in one place. By keeping aspects separate from methods that are indirectly related, code may become more maintainable and easier to understand [11]. In this work, AOP is used to keep the contract assertion code separate from the system under study (SUS). More specifically AspectJ is used – a successful and well supported 6

Carleton University TR SCE-04-03

Version 1, February 2004

implementation of AOP for Java. For a quick reference to the AspectJ language please see [4]. In this work AspectJ is used to intercept the invocation of desired methods so that the constraints associated with the method can be checked. A preliminary look is also taken to see how this technology can help in the optimization of constraint checking. As already stated, pointcuts specify where a concern crosscuts the code (using join points). Next, we must also specify when the advice associated with these points executes: before, after, or around. A before advice executes just before the pointcut (specified by the join points) executes. An after advice executes right after the pointcut executes. An around advice is more complex as the advice is executed instead of the code specified by the pointcut allowing the user to specify whether the pontcut executes or not. In this work preconditions are ensured using before advices, and invariants are ensured using before and after advices. Postconditions require the around advice as using this advices gives us access to the state of the system before the constrained method executes. This is necessary as, for example, the OCL @pre construct provides access to values as they were before a constrained method executed at postcondition checking time (after the method finishes executing). For example, consider the class SimpleInteger and the precondition “context SimpleInteger::div(i2:Integer):Integer pre: i2 0”. class SimpleInteger { int i; public int div(int i2) { ... } }

The following aspect (AspectJ syntax) checks the precondition before the div method gets a chance to execute (note that the body of the advice is made up completely of Java code):

7

Carleton University TR SCE-04-03

Version 1, February 2004

public aspect Aspect { before(int i2) : // Type of advice execution(public int SimpleInteger.div(int)) // Pointcut && args(i2) // Provides access to the argument { // Body of the advice if (i2 == 0) { // Check if assertion holds // Display a warning if assertion fails System.err.println("Precondition failed!"); } } }

Furthermore, since when using AspectJ contract code is instrumented at the bytecode level, constraints can be set on classes for which source code is not available. This is very advantageous in cases where one works with third-party components. Even though we do not have the source code we can specify partial1 pre- and postconditions on this class’ methods. In this case the precondition will ensure that our application uses the component’s methods correctly and the postcondition ensures that the component’ methods return valid results. A final advantage of using AspectJ for the purposes of instrumentation is that it allows the ocl2j framework to support the optimizations presented in Chapter 7. For example, consider that there is an invariant constraint on a very large collection, it takes a long time to check the constraint: inv: arrayListCollection->includes(someObject), where someObject is at the end of a collection with millions of elements. Using AspectJ it is possible to determine when and how the collection has been modified by intercepting operations on the collection that can potentially modify it. This allows us to answer the following two questions: •

Has the collection changed?



If yes, how has the collection changed?

With this information it can be determined whether the invariant has been violated without having to traverse the entire collection. This subject is explored in detail in Chapter 7.

1

The constraints are partial as we are not familiar with the functional details of a third-party component.

8

Carleton University TR SCE-04-03

Version 1, February 2004

2.3 Related Works Three academic efforts exist in the area of OCL to Java code generation and instrumentation. The first and main research contribution, the Dresden OCL Toolkit, was the pioneering work for the problem and is open-source software (Section 2.3.1). The second research contribution (Section 2.3.2) is based on the first and has therefore similar problems. Finally, while our work was under completion another solution become available (Section 2.3.3). 2.3.1

The Dresden OCL toolkit (DOT)

The Dresden OCL toolkit (referred to as DOT from this point on) converts an OCL 1.3 expression into Java code [13, 18, 34]. The DOT generates Java code from OCL expressions and then instruments the system under study (SUS) in four steps. First, OCL expressions, supplied either from an XMI file or from comments inserted in the source code, are parsed using a LALR(1) parser generated with the SableCC parser generator, resulting in an abstract syntax tree (AST) of the expression. A limited semantic analysis is then performed on the AST to uncover errors not detected by the parser. Subsequently, the AST is transformed to involve only a subset of the OCL syntax, the objective being to simplify the code-generation stage: This is called normalization by the authors. This is possible, without any loss of expressiveness as it is often possible to write an OCL expression in different ways (e.g., collection operator select can be replaced with iterate). This however, results in a longer AST. The code generator traverses the normalized AST and builds expressions in Java for the SUS. Finally, the generated code is inserted into the SUS source code so that the contracts can be tested at runtime. The following technical choices have been made in the DOT approach. First, the instrumentation occurs at the source code level, requiring that the original SUS source code be heavily modified. Original methods are renamed and wrapped, and three types of code fragments are inserted (Figure 1). Preconditions are checked right before the first statement in the method, in the PRE FRAGMENT, and postconditions are checked right 9

Carleton University TR SCE-04-03

Version 1, February 2004

after the last statement in the method, in the POST FRAGMENT. If a postcondition has @pre values, those are saved before the method executes, in the TRANSFER FRAGMENT.

Invariants are dealt with in a more complex as optimization is attempted. Instead of checking invariants before and after every public operation—simple but inefficient as only a percentage of methods in the class may affect the invariant, invariants are only checked after methods that modify attributes used in invariants—not trivial to implement but, if successful, is very efficient. In order to achieve this, the DOT introduces a backup attribute for every attribute in every class, virtually cloning each object. It is important to note that this is done regardless if the attribute is used in an invariant or not. Additional mechanisms are inserted into the SUS source code to detect attribute value changes, identify which invariants have to be checked because of those changes, and finally check the invariant. In particular, for every attribute in the SUS source code, the DOT adds a collection of all the observers of that attribute (i.e., invariants) that need to notified if this attribute’s value changes (an implementation of the observer design pattern [15]). public class SomeClass { // The original method is renamed. public returnType someMethod_original() { // Original method code. } // Wrapper around the original method. public returnType someMethod() { // TRANSFER FRAGMENT { // PRE FRAGMENT } // Invocation of the original method. result = someMethod_original(); { // POST FRAGMENT } return result; } }

Figure 1 DOT instrumentation strategy Another technical choice concerns the differences between OCL and Java types. For instance, OCL type Integer corresponds to Java types Integer, int, long, … Additionally, Java collection types do not support some of the functionalities provided by OCL collection types. This led the authors of the DOT to implement the OCL types in 10

Carleton University TR SCE-04-03

Version 1, February 2004

Java and wrap Java variables (attribute, method parameter or return value) used in assertions with equivalent OCL types. This results in additional objects created at runtime and more operation calls. Last, the generated code is constructed in such a way that it uses Java reflection mechanisms at runtime (i.e. when the instrumented program executes) to determine implementation details: e.g., how an OCL collection used in an OCL expression is implemented in the SUS source code. A close analysis of the DOT approach thus shows that due to these technical choices, the tool pays a large memory and performance penalty: every object created when running the SUS is virtually cloned, additional objects are created to map Java types to OCL types, resulting in more method calls. Additionally, the following issues are worth mentioning: First, constraints on elements in collections are not properly enforced as they assume that a constraint on a collection can only change if the state of the collection changes (objects are added or removed from the collection) and they observe this state change in an unreliable manner. The state change is detected in one of three ways. First, the size of the collection changes, thus if an element is added to a collection and a different element is removed from the collection the size of the collection does not change resulting in a potential violation of the constraint being undetected. Also, a potential violation of the constraint goes undetected if the state of an element in the collection changes in such a way that violates the constraint but not the size. The second solution depends on the collection iterator’s failfast methods (see the Java API documentation for the HashSet class in [29] for more information on fail-fast functionality). These methods throw an exception when they detect that there’s a modification to the collection (as it’s being iterated over). This solution has two problems, the first problem is, as stated in [29]: “it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs”. The second problem is that changes to elements states that may violate constraints are not detected. In the last solution suggested in the DOT, a hash code value is derived from the hash value of every element in the collection. This solution can provide inaccurate results as the strategy they chose to

11

Carleton University TR SCE-04-03

Version 1, February 2004

compute hash code values does not take into account object equality properly, and changes to elements in the collection may not be detected. The hash code is extracted using System.identityHashCode(Object x), that returns the same hash code for the object passed as a parameter as would be returned by the default method hashCode() (that is typically based on the memory address of the object). Consider the code listing in Figure 2. Note that even though the object changes, it’s hash code, as returned by System.identityHashCode(Object x) does not change and therefore the changes to

this element on the collection goes on being unobserved. Thus, the constraint “collection->forAll(p:Point | p.x > 0)” will not be re-checked on account of the change shown in the figure. // The Code Point p = new Point(0, 0); System.out.println("hashCode 1: " + System.identityHashCode(p)); p.x = -1; System.out.println("hashCode 2: " + System.identityHashCode(p)); Sample Output hashCode 1: 18508170 hashCode 2: 18508170

Figure 2 System.identityHashCode(Object x) Example Second, due to the way which the OCL @pre is handled, postconditions may be checked erroneously leading to false-positives and false-negatives (See more details on this issue in Section 4.5.) Indeed, when the @pre construct is used in a postcondition, the DOT approach consists in saving the attribute/parameter’s value at the start of the executuion of the method in a local variable using the ‘=’ (assignment) operator. For this reason some postconditions will result in false-positives and false-negatives. For example, consider an operation SomeClass::SomeOperation() whose postcondition states that, elements of a collection (say, aCollection) with attribute attr’s value equal to true are not the same. This is illustrated in OCL below: context SomeClass::SomeMethod() post: not( aCollection@pre->select(obj.attr = true) = aCollection->select(e.attr = true) )

12

Carleton University TR SCE-04-03

Version 1, February 2004

In the case, the DOT approach is to create a local variable to store the value of aCollection before the execution of the method. As a result, the local variable points to

the same collection as aCollection, thus the postcondition would never be satisfied. Third, since the DOT inserts the contract code directly into the source code the user is faced with a dilemma: keep only one version of the source code (either the clean version or the instrumented version) or keep both versions of the source code. Both options have disadvantages, when only one version of the code is kept then the user must deal with the long wait times for the cleaning and instrumentation whenever a change to the source needs to be made. If the version of the code being kept is the instrumented version then the code must be cleaned whenever the user wants to read the code. Considering the fact that on a program with 17K lines of code (LOC) it takes almost two minutes to clean and instrument the program this is a significant burden. This is especially true when considering the style of code development today, where the programmer makes a small change and then runs the program to see if the change had the desired effect. Thus it is easy to see that this lag time is unacceptable during development which means that, in a non academic environment, the DOT cannot be used all the time during program development reducing the value of the tool. Keeping both versions of the source code solves some of the above outlined problems but introduces new ones. If a version management system is used, two versions of the source code must be kept in the system. The first issue with this is that size of the code databases instantly more than doubles. Secondly, whenever two versions of the same thing exist an inevitable thing happens: inconsistencies are introduced amongst the two versions of the “same” code. Due to the fact that it takes a lot of time to clean and instrument the code, a programmer nearing a deadline will be tempted to just change the instrumented version of the code (this is possible but is not elegant) thinking that he or she will update the un-instrumented version of the code at a later time. If something more important comes up the programmer may forget to update the other version of the code introducing an inconsistency (between the instrumented version of the code and the un-instrumented version).

13

Carleton University TR SCE-04-03

Version 1, February 2004

Last (more information on the following issues can be found in [10]), the implementation of OCL contracts is incomplete as, for instance, query operations and operation OclType::allInstances are not supported. When the DOT was used on a real world

project [34] with only a few constraints, the size of the source code was multiplied by 3 (in Mb) leading to longer compilation time and slower execution. In addition, it took good parts of a minute to instrument or clean the source code, which is a lot considering that only a few constraints were istrumented. (On asystem that takes several hours to build, the delay due to instrumentation and cleaning would be even worse.) Generating the code in such a way that it uses Java reflection mechanisms at runtime provides for easier Java-code-generation logic in exchange for execution time overhead, i.e. a slower execution time for the instrumented system. The Java code generated by the DOT may throw unexpected runtime exceptions when executed (see Appendix G). To conclude, a study of this tool performed by [16] determined that more work needs to be done to provide usable code generation and instrumentation, with emphasis on efficiency. The DOT is a start, but is both incomplete and inefficient making it unusable for even slightly non-trivial programs. This dictates the need to develop a system which would solve the current limitations so that research in OCL as a design by contract language may progress. 2.3.2

An AspectJ-based approach

Another contract checking system is proposed in [32]. It is based on the DOT’s Java code generator described above [13], but uses AspectJ instead of the strategy proposed in [34] for the instrumentation. The authors’ objective is to identify locations in the source code (referred to as insertion points) where class invariants are to be checked, the set of those insertion points being defined as the scope of the invariant. This stems from the fact that the authors adhere to the definition presented in [33] stating that invariants are true “at any moment in time”. This definition is different than the one outlined by Meyer which states that invariants must only be true before/after entry/exit to public methods in the context class. At the heart of the difference between the two definitions lies the fact that invariants may be temporarily invalidated during the executing of such public methods. 14

Carleton University TR SCE-04-03

Version 1, February 2004

We agree with the latter definition and for this reason we will discuss this work in a very terse manner and will not compare this approach against the one taken by us. In their approach the scope of an invariant, say I, depends on the navigation path(s) of its corresponding OCL expression. As the navigation path becomes longer, more classes are involved, more operations (in those classes) are likely to modify model elements constrained in I, and, as a result, more insertion points have to be specified. The complexity of this issue led the authors to create a classification of invariants based on their navigation path types, or the classes they involve. Invariants are classified into four types: Invariants on properties of one class; Invariants on properties of an associated class; Invariants on properties of associated collections; Invariants on properties of elements in collections. For each type of invariant insertion points are specified and a brief example is given of what the aspect (contract assertion) code may look like. The paper shows how Aspect Oriented Programming (AOP) can be used to implement the checking of the different types of invariants, and the authors used AspectJ for their work. When generating the aspect contract assertion code, involved classes, methods and attributes are determined using the navigation path specified in the constraint. The approach is dependent on coding conventions: e.g., getters and setters must be used to access/modify class attributes. 2.3.3

The Object Constraint Language Environment (OCLE)

Recently, another piece of work came to completion [24]: The OCLE 2.0 tool. OCLE version 2.0 is a UML CASE Tool offering full OCL support both at the UML metamodel and model level. It can read UML models saved in the XMI format. Apart from OCL support offered at the metamodel level (enforcement of UML’s well-formedness rules, e.g., no two classes have the same name in the same name space), OCLE claims support for runtime enforcement of OCL constraints in Java. In this Section only this aspect of the tool will be discussed. The “OCL constraints” to “Java assertions” functionality was added to OCLE in version 2.0. Like in the DOT, the assertion code is injected into the source code. Like in the ocl2j approach, they use built-in Java types wherever possible. The tool works by first 15

Carleton University TR SCE-04-03

Version 1, February 2004

converting a UML class diagram into Java class skeletons, the user then needs to add the body to the generated method. This generated code contains the assertions to check the OCL constraints on the class diagram although the user must manually specify each point where they wish for the class invariant to be checked (pre- and postconditions are handled automatically). The OCLE approach has several major limitations; (1) OCL constraints cannot be added to exiting code and round-trip engineering is not supported, (2) Some core OCL functionalities are not supported, e.g., @pre and OclAny::oclIsNew(), (3) Injecting the assertion code into the source code comes with problems that have already been discussed, e.g., source code pollution (see Section 2.3.1). For these reasons, along with the fact that no publicly available documentation discusses the transformation rules employed by the tool, this work will not be examined in greater detail.

3 THE OCL2J APPROACH – AN OVERVIEW This chapter provides an overview of the ocl2j approach towards the automatic generation and instrumentation of OCL constraints in Java. More technical details are provided in the following Chapters and in [10]. The ocl2j approach is similar to the DOT approach in the sense that Java code is created from OCL 1.3 expressions and the SUS is then instrumented: (1) The necessary information is retrieved from the SUS UML model and source code; (2) Every OCL expression is parsed, an abstract syntax tree (AST) is generated [2], and the AST is used to create Java code; (3) The SUS is then instrumented with the assertion code. However, the ocl2j approach is based on significantly different technical decisions than the DOT, addressing its main shortcomings. First, the instrumentation is non-intrusive, as aspect oriented programming technology is used to externalize the contract assertions: Separation of the assertions from the SUS source code. Having all contracts code in separate files has many advantages, as discussed in Section 3.1. Second, instead of converting all variables in the Java SUS source code that participate in contracts to their OCL equivalent variables (as done in the DOT), a set of rules transform OCL types and 16

Carleton University TR SCE-04-03

Version 1, February 2004

operations to their equivalent Java types and operations (Section 3.2). Also, in the ocl2j approach those transformations are performed when the SUS is instrumented, whereas in the DOT approach they are performed at run time (i.e., when the instrumented SUS executes). Additionally, objects are not cloned in the ocl2j approach. These were the major drawback of the DOT approach. Last, ocl2j is more complete than the DOT in terms of its support of OCL as illustrated in Section 3.6. The rest of the Chapter is structured as follows. Section 3.1 describes how the assertion code is kept separate from the SUS source code. Section 3.2 discusses the OCL to Java transformations. Section 3.3 summarizes the information that needs to be reverse engineered from the SUS. Section 3.4 discusses the topic of contract checking, i.e., when the contracts should be checked during the execution of the SUS. Section 3.5 discusses the impact of inheritance on contracts and assertions, and finally Section 3.6 discusses the hypotheses made by the ocl2j approach and its limitations.

3.1 Externalization of Contracts Aspect code resides in its own files (outside of the SUS’s source code), leading to the following advantages: •

The user can work on the code without having to regenerate the contract assertions before each compile. This is true as long as the class diagram (including operation signatures but also class relationships) and contracts do not change as then the contract code will not need to change either. This saves the user large amounts of time as the generation and instrumentation of contracts may take significant amounts of time.



Having the program code and the contract code in separate files allows for parallel development of the SUS source code and its aspects, once again as long as the class diagram and contracts are stable. As one person is working on the program source code the other person can be creating the contract code. The two are later woven together.



When dealing with version management software and using ocl2j, there is only one 17

Carleton University TR SCE-04-03

Version 1, February 2004

version of the SUS source code in the version management software’s database, thus avoiding duplication. This is a big advantage as duplication often leads to inconsistencies (between the two versions) and cleaning of the code takes a significant amount of time. •

If the user wants to run the program without the contracts it’s as easy as recompiling the code without including/weaving the aspects.



The weaving of the contract code and the program code is done at the byte code level. For this reason the source code is not necessary for the insertion of the contract code. This is very important if the SUS uses classes for which the source code is not available (e.g., it is possible to observe classes provided by third parties).

3.2 OCL to Code Transformations Instead of wrapping Java types and operations with OCL-like types and operations, as in the DOT, the ocl2j approach consists is expressing OCL expressions directly in Java, using the types and names retrieved from the SUS through reflection at the assertion code-generation stage. This OCL to code transformation is thus more complicated and takes more time in ocl2j than in the DOT. However, the generated code is more efficient (no wrapping) and the transformation is performed only once, before any instrumentation of the SUS. Whenever possible, that is whenever a mapping exists between OCL and Java types/operations, the translation is straightforward. For instance, if postcondition result=i1+i2 for operation add(i1:Integer, i2:Integer) is found in the UML

model, and the SUS source code shows signature add(i1:int, i2:int), then the assertion for the postcondition is result==(i1+i2). Similarly, OCL collection operation size()maps directly to the size() operation of the java.util.Collection interface

(which every collection in Java implements). This mapping is not always simple as intermediate collections may have to be generated. This is the case with OCL collection operation coll->including(obj) that returns a collection with all the elements found in collection coll plus object obj. For instance, if an OCL expression shows coll-

18

Carleton University TR SCE-04-03

Version 1, February 2004

>including(obj)->select(…), an intermediate collection containing all the elements of coll and obj has to be created before performing operation select.

When OCL types/operations cannot be directly converted to types/operations from standard Java libraries, the instrumentation code (aspect code, Section 4.4) provides the functionality that is “missing” in the libraries. This ensures that no wrapping is necessary, and no additions to the SUS are required. The aspect contains inner classes with operations that provide additional functionality to complete the mapping to Java such as the Collection->count(obj):Integer operation, that counts the number of times object obj occurs in a Collection and does not have any counterpart in Java collection classes/interfaces. The aspect code thus contains inner class OclCollection with a count operation that takes two arguments: the collection on which count must be

performed and the object that needs to be counted. The details of the OCL to java transformations are discussed in Section 4.

3.3 Reverse engineering of the SUS When generating the assertion code, the SUS must be reverse-engineered to extract types used in its implementation and to extract details about inheritance hierarchies. First, reflection is used to extract method-parameter types, method-return types, and attribute types. This information needs to be extracted from the source code to generate the assertion code properly. For example, consider a constraint that calls a query operation with an argument value of 18 (to retrieve a list of people in the system that are over that specified age), like: getListOfPeopleOverTheAgeOf(18). From an assertioncode generation standpoint we need two pieces of information to generate the assertion code properly: What is type of the age parameter? Is it of primitive type int or of reference type Integer? If it happens to be of reference type Integer then we must convert “18” to “new Integer(18)” in the assertion code. Next, the return type for the query method: of what type is that? Is it of type List or of type Set. Depending on the type we may need to treat it differently.

19

Carleton University TR SCE-04-03

Version 1, February 2004

Second, information about inheritance hierarchies is necessary to enforce constraints properly. Essentially, a subclass that does not override a parent class’ method not only inherits that method but also the precondition and postcondition to this method. As will be seen in Section 4.7, support for this inheritance of preconditions and postconditions requires knowledge as to which methods are not overridden. It could be argued that method-parameter types, method-return types, and attribute types could be extracted directly from the UML class diagram. This is true assuming that the class diagram is very specific in nature up to the point that it exactly specifies types used in the implementation. We have not chosen to take this approach as we felt that in most cases the class diagram is not specified to such a fine-grain. At the same time the logic to the extraction of all these implementation details is hidden behind a bridge/façade [15], thus adding support to extract this information from a different source (say an XMI file) would not be difficult. Also, reflection was chose over static analysis of the source code mainly for simplicity. The only disadvantage in using reflection over static code analysis is that (compiled) class files must exist as that is where the Java reflection facility extracts information from. This is not a serious disadvantage as during development these files are perpetually kept updated by modern integrated development environments (like Eclipse [20]) in order to instantly show the programmer problems with the code.

3.4 Checking Contracts Every contract, a precondition, a postcondition or an invariant, is transformed into an assertion, which is placed in the SUS code. An insertion point is a location in the SUS code where the assertion is to be placed. The insertion point for an assertion checking a precondition is right before the execution of the corresponding method. Similarly, the insertion point for an assertion checking a postcondition is right after the execution of the corresponding method. As for class invariants, Meyer states that a class invariant must be true (i) after an instance creation and (ii) before and after any remote call to an operation. In UML terms, this means that every public operation’s contract must be instrumented as 20

Carleton University TR SCE-04-03

Version 1, February 2004

public operations are the only ones that can participate to a remote call, according to Meyer’s definition. This is explicitly confirmed in [1] which specifies that “the invariant must be true upon completion of the constructor and every public method but not necessarily during the execution of methods”. However, a public operation in a UML model may end up being implemented as a public or protected operation in Java, as protected operations are accessible to other classes in the same package2. So, in Java terms, we may instrument both public and protected methods. Nevertheless, in our approach, we decided to adhere to the UML definition so as to be independent of the specific programming language being used in the SUS: The criteria driving the selection of methods to be instrumented is driven by the information in the UML model (i.e., only public operations as defined in the UML model are instrumented), whether those operations are implemented as public methods or not in the SUS source code.

3.5 Inheritance of Constraints Since a child class inherits features from its parent class it’s natural to assume that the same should happen with constraints. The Liskov Substitution Principle (LSP) [25] provides a theoretical framework for this, distinguishing subtyping from subclassing. The LSP states that “if for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T”. Otherwise S is not a subclass of T. Bertrand Meyer probably captured LSP best with his contract-oriented paraphrase that "A subtype must require no more and promise no less than its supertype." If this principle is followed then we can derive the following rules to instrument contracts in inheritance hierarchies: (i) A child class invariant implies the parent class invariant; (ii) An overridden operation precondition implies the overriding operation precondition (the precondition is weakened); (iii) An overriding operation postcondition implies the overridden

operation

postcondition

(the

postcondition

is

strengthened).

In

instrumentation terms, this means that ancestor classes’ invariants must be checked for 2

This differs from the UML definition of a protected operation which states that “any descendant of the classifier can use the feature” [6].

21

Carleton University TR SCE-04-03

Version 1, February 2004

descendent classes. Though, based on the definition of the LSP, this may seem unnecessary, the point is to check whether the implementation—and not only the model—actually complies with the LSP. For similar reasons, when a method overrides another, both postconditions must be checked at the end of the execution of the overriding method. However, since the precondition of the overriding method does not imply that of the overridden method (that is exactly the contrary), only the overriding method’s precondition is checked at the beginning of its execution. Consider the example presented in Figure 3, since Ceo is a child class of Employee the following are true: (i) The invariant in Ceo implies the invariant in Employee; (ii) The precondition

on

Employee::increaseSalary(…)

Ceo::increaseSalary(…);

implies

the

precondition

on

(iii) The postcondition on Ceo::increaseSalary(…)

implies the postcondition on Employee::increaseSalary(…).

Employee #salary : int #onPayroll : boolean +increaseSalary( increase : int ) : void

Ceo -companyCar : boolean +increaseSalary( increase : int ) : void

context Employee::increaseSalary(increase:int) pre: increase > 0 and increase < 1000 post: salary = salary@pre + increase and salary > 30000 context Employee inv: onPayroll = true context Ceo::increaseSalary(increase:int) pre: increase > 0 and increase < 5000 post: salary = salary@pre + increase and salary > 100000 context Ceo inv: companyCar = true and onPayroll = true

Figure 3 LSP Example In [28] the authors promote the use of the LSP as it results in a safe use of inheritance. Having said this, it is a reality that the LSP is not always followed. Thus, ideally the user should be allowed to specify inheritance hierarchies where the LSP is or is not followed so that constraint inheritance is only enforced where it makes sense to do so. Currently, the ocl2j prototype allows enforcement of the LSP for all hierarchies or none.

22

Carleton University TR SCE-04-03

Version 1, February 2004

3.6 Assumptions & Limitations This section lists the hypotheses made by the ocl2j approach in term of the use of OCL and coding standards. The reader will see that those assumptions are reasonable in a context where a UML model is implemented in Java and a certain level of traceability is achieved between the UML model and the source code. Next, we list the underlying assumptions and current limitations of the ocl2j approach, along with possible solutions to address them. 3.6.1

Assumptions

OCL-Java Mapping In terms of traceability, our approach assumes that class, attribute, operations, parameters have the same names in the UML model and the source code. Additionally, association role names in the UML model are used as attribute names in the source code. Also, we assume that OCL Set and Sequence (and Bag) collections are implemented in Java with classes that implement interfaces java.util.Set and java.util.List, respectively, as justified in Section 4.4. Four additional assumptions concern the way the UML model is implemented. First, OCL collection operation sum() is only applicable to collections where the elements in the collection is of a type supporting addition [23]. In other words, the implementation counterpart of those types must implement the java.lang.Number interface. (In the standard Java 1.4 library the following types fulfill this requirement: BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short). Second, When using OCL

collection operation sortedBy(expr:OclExpression), parameter expr is a property of the type of the elements in the collection and this type must have a < operation. In other words, the implementation counterpart of the property must be a primitive type, or an object implementing the java.util.Comparable interface. Third, if user defined collections are to be used, they must implement the java.util.Collection directly or indirectly (inheriting from a class that implements the interface). Additionally, sequences

23

Carleton University TR SCE-04-03

Version 1, February 2004

and bags must implement the java.util.List interface while sets must implement the java.util.Set interface.

As further discussed in Section 4.3, if “=” is used in an OCL expression (see example in Section 4.5) it is important that the class implementation contains a proper implementation of equals() in its public interface and furthermore, if the OCL equality expression refers to @pre values, the corresponding class implementation must be cloneable (it implements the java.lang.Cloneable interface and provides a proper clone() method). Note that support for cloneability is not default to objects in Java (see java.lang.Cloneable in [29]). But both providing proper equals() and clone()

methods is considered good programming practice. All the above assumptions are consistent with what is usually considered good practice by experienced Java programmers and therefore do not constitute a severe limitation. OCL Usage To facilitate the processing of OCL expressions, a subset of the OCL syntax is used. The OCL syntax not taken into account is human shorthand syntax (e.g., implicit use of self when referring to the context). An OCL expression using the full OCL syntax can be easily transformed to only use the syntax allowed by ocl2j with a normalizer [7, 13]. Examples of OCL syntax reduction are (the complete list can be found in 0): All variable types in collection operations must be specified; The let expression that can be used to simplify large OCL expressions is not supported (the let expression defines a variable that can be used instead of a complete sub-expression), i.e., any occurrence of the variable defined by let is to be replaced by the corresponding sub-expression. From the complete list, provided in 0, the only restriction that can potentially make the generated code less efficient is the lack of support for the let expression. However, this will only happen in the case where let is used to define an expression that takes a long time to compute and the definition is then used multiple times in the constraints. One more detail is worth mentioning. As stated in [28], “The forAll operation has an

24

Carleton University TR SCE-04-03

Version 1, February 2004

extended variant in which more than one iterator is used. Both iterators will iterate over the complete collection.” In this paper this is interpreted as saying that the OCL collection operation forAll uses a maximum of two iterators. In other words, the transformations only take into account a forAll with one or two iterators. It is important that our ocl2j tool (in fact, the subsystem in charge of loading model information) warn the user if the above assumptions do not hold. 3.6.2

Limitations

The two main limitations of our tool are not theoretical in nature and are expected to be overcome in the near future. First, enumerations are not supported as there is no Java construct for enumerations in release 1.4 making them implementation specific. Though some coding standard could be assumed, it was decided to address this issue in future work, especially since Java release 1.5 has primitive support for enumerations [30]. It is worth noting that if ocl2j was to be integrated with a CASE tool like IBM Rational Rose [21], then enumerations could be easily supported as the CASE tool would be the one generating the code for the enumeration, thus knowing the idiom used to implement it. Second, OCL specifies the operation oclInState(s:OclState):Boolean as one predefined on all objects: It returns true if the object is in state s, one of the specified states in the statechart associated with the class of the object. Similarly to the case of enumerations there is no one way to implement this in Java. For this reason one way to provide support for this functionality is with the integration of ocl2j with a CASE tool that knows how the state properties of objects are implemented and used (for example using the “state pattern” as specified in [15]). Another way is to have coding standards that enforce the use of good design practices to implement statecharts, such as the state design pattern. In that case, checking the state of an object results in simply checking that its associated state object reference is an instantiation of the right state concrete subclass. As was pointed out in [19] it is possible to specify a precondition such as self.b.c@pre. Although grammatically correct, the problem lies in the fact that before the invocation of the constrained method, it is not yet known what the future value of the b property will 25

Carleton University TR SCE-04-03

Version 1, February 2004

be, and therefore it is not possible to store the value of self.b.c@pre for later use in the postcondition. Three possible solutions to this problem are presented in [19]: (i) build complex tools to handle such a case, (ii) exclude the problematic case from the grammar, (iii) exclude support for this case and present a warning to the user when trying @pre in this way. Implementing solution (i) would result in very complex code and very poor runtime efficiency, as runtime efficiency is already an issue without this problem it is not a feasible solution. Solution (ii) is not very convincing, since it restricts the language in a rather awkward way. As the problematic case is easily detectable we decided to implement solution (iii). Not supporting this case is not a serious limitation as such a use of @pre is extremely rare, throughout our extensive experience with OCL we have never used @pre in such a way.

4 THE OCL TO JAVA TRANSFORMATIONS This chapter begins by giving an overview of the transformation rules in Section 4.1, and the remaining sections discuss important issues with respect to the transformation rules. More specifically, Section 4.2 discusses the effects that Java’s dual type system (primitive types and their class counterparts) has on the transformations. Section 4.3 discusses the issue of object equality from the OCL and Java points of view. Section 4.4 discusses discrepancies between OCL and Java collections. Section 4.5 discusses some subtleties that stem from the usage of the OCL @pre keyword with respect to Java programs. Section 4.6 describes how AspectJ was used to implement the OCL operations OclType::allInstances() and oclAny::oclIsNew(), and finally Section 4.7 uses the

concepts introduced in Section 2.2 on AOP to explain how assertion code is instrumented into the SUS bytecode and enforced at runtime.

4.1 Transformation Rules and Usage The OCL to Java Transformation rules are carried out from the abstract syntax tree (AST) representing the OCL expression to be transformed. Since an OCL grammar is used by the parser to generate the AST (non-terminals and terminals represent the OCL constructs and keywords, respectively), an obvious practical way to rigorously specify the 26

Carleton University TR SCE-04-03

Version 1, February 2004

transformation rules is to add semantic actions [3] to the EBNF production rules. A grammar in which semantic actions are embedded in the right sides of production rules is called a translation scheme [2]. In our EBNF grammar extended with semantic actions, non-terminals and terminals are associated with a set of properties. Among them, since the objective is to transform an OCL expression into Java source code, is property code that holds, in the form of a string, the Java code corresponding to the transformation of the (non-)terminal. For instance, expression being one of the non-terminals in the grammar, expression.code represents

the Java code obtained when transforming expression. Note that, in the rules and text, we use different fonts to distinguish (non-)terminals from semantic actions: courier for (non-)terminals and arial for semantic actions. Since transforming an OCL expression is performed by a depth-first traversal of the AST (i.e., production rules are used from left to right), semantic actions appear on the right of the (non-)terminal they are associated with, between braces. For instance in "if" expression {ex1=expression.code}, semantic action {ex1=expression.code} appears on the right of expression: we need to transform the AST subtree of expression before using the corresponding Java source code and assigning it to variable ex1. Another property for (non-)terminals is type that holds the type of the Java code generated for the (non-)terminal. As an example, the following part of a production rule retrieves both the code generated for a relational_expression, and its type: relational_expression {lCode = relational_expression.code; lType = relational_expression.type}. This type property has different characteristics that can be used

in semantic actions: It has a name—e.g., expression.type.name is a string representation of the Java type of the implementation of expression); It can represent a primitive type (e.g., int) or a reference type (e.g., Integer)—e.g., expression.type.isPrimitiveType() is true if

the Java type of the implementation of expression is a primitive type; It can be a collection—e.g., expression.type.isCollectionType() is true if the Java type of the implementation of expression is a collection.

27

Carleton University TR SCE-04-03

Version 1, February 2004

Three kinds of semantic actions are added to the OCL grammar. The first one systematically appears at the very end of a production rule and shows, using keyword return, followed by a string, how the Java source code is produced for the rule. In other

words, it shows what the corresponding string (Java code) is composed of. When that string is the concatenation of different strings (e.g., from different non-terminals on the right side of the rule), we use the + operator to unify the strings. Semantic actions of the second kind appear on the right side of production rules to retrieve (e.g., using property code previously described) the information required to generate the Java source code for the production rule, i.e., the information returned (semantic action return). In those semantic actions, property code for non-terminal nonTermA (i.e., semantic action nonTermA.code) refers to the string following keyword return in the production rule for nonTermA, i.e., the production rule where nonTermA

appears on the left side. As an example, Table 1 shows the production rule for an ifthen-else expression in OCL. The EBNF rule is if_expression

::=

"if"

expression "then" expression "else" expression "endif", and four semantic

actions have been added. The first three ones, of the second type of semantic actions, retrieve the Java source code generated for the three expression non-terminals, whereas the last one (with keyword return) describes how the Java source code for the complete if-then-else expression is generated from those three pieces of code (the Java ternary

conditional operator ?: is used). if_expression ::= "if" expression {ex1=expression.code} "then" expression {ex2=expression.code} "else" expression {ex3=expression.code} "endif" ; {return "(" + ex1 + " ? " + ex2 + " : " + ex3 + ")"}

Table 1 Example 1 of production rule with semantic actions. The last kind of semantic action is used when some preparation code is required before the assertion proper (code generated by return semantic actions) can execute. The way this preparation code is generated is indicated by keyword PreCode. A straightforward example is when OCL keyword @pre is used on a primitive type in a postcondition,

28

Carleton University TR SCE-04-03

Version 1, February 2004

indicating a value at the start of the execution of the operation. In this case, PreCode shows how the value at the start of the execution is saved. One possible situation is illustrated is Table 2. The preparation code differs according to the type of the value to be saved (see Section 4.5). For example, assuming path_name is an attribute named att and its type is int, the source code is: int pre = att;. Though it only contains a direct reference to an attribute here, the property path_name.code could contain a Java path expression. primary_expression ::= path_name {pType=path_name.type ; pCode=path_name.code} time_expression ; {if pType.isPrimtive() then PreCode=pType + " pre = " + pCode + ";" elseIf pType.isCollectionType() then …} {return pre}

Table 2 Example 2 of production rule with semantic action (simplified). We specified 32 transformation rules (production rules with semantic actions), see Appendix B. The number of rules will increase with the support for OCL expressions that are assumed to be normalized (e.g., no need for rules related to the OCL construct

let),

and the support for some OCL constructs that could not yet be supported (e.g., enumerations: there’s no default way to represent these in Java yet), as discussed in Section 3.6. The above rules are exemplified in the following four sub-sections, by means of the class ClassWithBooleans which has three attributes of type boolean; b1, b2, and b3. The

examples become progressively more complex with respect to the generation of the Java code. They were chosen in such a way as to show how recursion is handled when generating the assertion code. 4.1.1

First example

In this first example, consider the following invariant for class ClassWithBooleans: context ClassWithBooleans inv: self.b1

29

Carleton University TR SCE-04-03

Version 1, February 2004

We use a production rule for non-terminal primary_expression (Table 3), which is different from the one in Table 2. This rule is used when primary_expression is only a path_name. In this case, path_name.code equals to string “(self.b1)”, and this piece of

code is tested in an if statement where the invariant has to be checked. Note that like in OCL, self refers to the object on which the constraint is being validated upon in the aspect code. How this is achieved will be seen in Section 4.7. primary_expression = path_name ; {return path_name.code}

Table 3 Simplest production rule for non-terminal primary_expression 4.1.2

Second example

In this second example, the invariant for class ClassWithBooleans is: context ClassWithBooleans inv: self.b1 or self.b2 implies self.b3

The OCL expression begins with logical expression “self.b1 or self.b2”, followed by “implies” and another logical expression. The production rule in Table 4 is used and the first logical expression is analyzed and Java code (self.b1 || self.b2) is generated: semantic action {ex1=logical_expression.code}. The right hand side of the “implies” is then analyzed in the same way: Java code (self.b3) is generated. Note that production rules for non-terminal logical_expression are not shown in this section (they can be found in Appendix B): In our example (and the following ones) the nested production rules for logical_expression eventually involve production rule for primary_expression in Table 3. The semantic action generating the code for the

“implies” is then used (line 5 in Table 4): Modus Ponens is applied and Java code !(self.b1 || self.b2) || (self.b3) is produced. The remaining part of the rule is

not used as there is no additional “implies”, and the generated Java code returned is !(self.b1 || self.b2) || (self.b3).

30

Carleton University TR SCE-04-03

Version 1, February 2004

expression ::= logical_expression {ex1=logical_expression.code} "implies" logical_expression {ex2=logical_expression.code} {ex="!(" + ex1 + ") || " + ex2} ( "implies" logical_expression {ex3=logical_expression.code} {ex="!(" + ex +") || " + ex3} )* ; {return ex}

Table 4 Production rules for non-terminals expression 4.1.3

Third example

context ClassWithBooleans inv: self.b1 implies self.b2 implies self.b3 is

the class invariant for ClassWithBooleans in this third example. This constraint is quite similar to the last one; except that there are multiple occurrences of keyword implies. Using production rule in Table 4, the first and second logical expressions are analyzed and Java codes self.b1 and self.b2 are generated, through semantic actions {ex1=logical_expression.code} and {ex2=logical_expression.code}. The code for this first

“implies” is then produced using Modus Ponens (semantic action { ex="!(" + ex1 + ") || " + ex2}): !(self.b1)||self.b2. The next “implies” is then analyzed. Semantic action {ex3=logical_expression.code} produces Java code (self.b3), and semantic action { ex="!(" +

ex

+")

||

"

+

ex3}

generates

the

following

Java

code:

!(!(self.b1)||self.b2)||(self.b3). Last, this piece of code is returned.

4.1.4

Fourth example

In this fourth example, the class invariant is very similar to the previous one except that we force a different order of evaluation of the “implies”, by means of parentheses: context ClassWithBooleans inv: self.b1 implies (self.b2 implies self.b3)

In this case, semantic action {ex1=logical_expression.code} in Table 4 produces Java code: self.b1. Semantic action {ex2=logical_expression.code} produces Java code for the

expression between parentheses in the invariant. Another use of the production rule in Table 4 is necessary. The first two semantic actions then generate self.b2 and 31

Carleton University TR SCE-04-03

self.b3,

respectively,

Version 1, February 2004

and

the

application

of

Modus

Ponens

returns

!(self.b2)||(self.b3). This result is then used along with self.b1 in another

application

of

Modus

Ponens.

The

generated

code

is:

!(self.b1)||(!(self.b2)||(self.b3)).

4.2 Resolutions of Java Types In addition to primitive types—like int and float—Java also supports primitive value wrapper classes, or simply wrapper classes—like Integer and Long (Table 5). OCL on the other hand has no notion of primitive types as everything is considered an object. OCL provides four, so-called, basic types: Boolean, Integer, Real and String (Table 5). There is one exception to these differences in OCL and Java type systems: strings are objects in both OCL and Java. This has a major impact on the process of transformation of OCL constraints into Java code. For example, consider the following OCL constraint: someCollection->includes(5). In OCL “5” is an object and OCL Integer operations

can be invoked on this object (e.g., 5.div(3) is a legal OCL expression). When transforming the OCL expression into Java source code, 5 has to be transformed into either primitive value 5 or an instance of wrapper class Integer (new Integer(5)). As Java collections only take objects as elements, the latter is the right choice. OCL basic types Integer Real Boolean String

Corresponding Java types Wrapper class: Integer, Long, Short, Byte Primitives: int, long, short, byte Wrapper class: Double, Float Primitives: double, float Wrapper class: Boolean Primitives: Boolean Class: String

Table 5: OCL basic types and corresponding Java types A general, trivial solution to this problem would be to convert every literal value into an object, but as discussed in Section 3.2, this is inefficient. A more efficient solution consists in analyzing the types used in the OCL expression, the types required in the corresponding Java source code, as well as the characteristics of the expression, and

32

Carleton University TR SCE-04-03

Version 1, February 2004

converting objects to their primitive types or vice versa only when necessary. During the transformation of OCL constraints into Java code two strategies are followed: •

S1: Values (left-value or right-value) used in logical, addition, multiplication, and unary operations are evaluated in their primitive form. If a value is an object then the primitive equivalent is extracted from it through an operation call. For example, in the case of an object value being an instance of the java.lang.Integer class, the primitive value of the integer is retrieved using the intValue() operation.



S2: Values used as arguments in operation calls are converted, if necessary, into an instance of the required Java type according to the operation signature. For example, the java.util.Collection::contains(o:Object):Boolean operation expects the parameter to be an object. Thus, in the case of the OCL constraint integerCollection->contains(10), where integerCollection is implemented

in

the

Java

source

code

as

a

Java

collection

(the

class

implements

java.util.Collection), “10” is converted to an object using the integer object

wrapper: new Integer(10). Consider the constraint in Table 6 where someCollection and self.collectionSize are implemented with Java classes java.util.HashSet and java.lang.Integer, respectively. The following steps are taken to generate the assertion code for this constraint. First, the left-hand-side and the right-hand-side of the OCL ‘=’ are of OCL Integer type. As discussed above (S1), Java primitive types are used in the assertion. As a result, Java operator “==” is used. In the assertion, a new collection of the same type as someCollection has to be created, for example named someNewCollection, to hold all

the elements in self.someCollection in addition to element 10. Also, since java.util.HashSet operations—and in particular operation add (to add element 10)—

only manipulates objects, literal 10 is wrapped using class java.lang.Integer (S2). Table 6 shows how this new collection is created and filled with the elements in someCollection and element new Integer(10). The Java collection operation size(),

which is used to implement OCL collection operation size(), returns a primitive type value. The results thus do not need to be wrapped (S1). Last, since 33

Carleton University TR SCE-04-03

Version 1, February 2004

self.collectionSize is implemented with type java.lang.Integer, its value has to

be transformed into a primitive type value, using the intValue() operation. OCL constraint

self.someCollection->including(10)->size() = self.collectionSize

Preparation code

java.util.HashSet someNewCollection = new java.util.HashSet(); someNewCollection.addAll(someCollection); someNewCollection.add(new Integer(10));

Assertion code proper

(someNewCollection.size() == self.collectionSize.intValue())

Table 6 OCL constraint example and corresponding preparation and assertion code When query operations are involved in an OCL constraint, this type resolution step between OCL basic types and Java types is driven by the mapping between UML operations and Java methods. Recall that query operations are operations in user-defined classes in the UML model that return a value but do not change the system state [6]. This mapping may not be straightforward because of method overloading in the Java source code. For example, consider OCL expression add(1, 2) = 3 where add(i1:Integer, i2:Integer):Integer is a query method of some class. When creating the assertion for

this OCL expression we first have to find a method that implements operation add. We have to search for valid candidate methods in the SUS assuming the method we look for preserves the name and number of parameters of the model operation. In our example, there exists (at least) one method named add that has two parameters and a return type. We first try to find a method with the name “add” and with parameter types “int, int” based on the way add is used in the constraint. If this straightforward search fails a more advanced search is performed for a method with the same name, the same number of parameters, but other variations of the (Java) Integer and Real types. This justifies the need for some simple static code analysis, as discussed in Section 3.3.

4.3 Testing For Equality Assertion code that tests for equality can take any one of three forms. First, if the values to be compared are of primitive type then the Java “==” construct is used in the equality test. Next, if the values being compared (or just one of them) are of reference type 34

Carleton University TR SCE-04-03

Version 1, February 2004

wrapping a primitive (see Section 4.2) then the primitive value is extracted from the object using the appropriate method (e.g., the intValue() is used to extract the primitive int value out of an object of type Integer) and again the values are tested for equality

using the Java “==” construct. Third, for objects that are instances of classes other than primitive type wrappers, i.e., for instance user defined classes, testing for equality requires more attention. In UML/OCL, objects are uniquely identified by their identity, and two instances are equal if they have the same identity. This identity cannot be the object reference as there is no such notion in UML/OCL. In other words, object equality in UML/OCL is a logical equality. From a logic standpoint, objects are thus uniquely identified by what we call their key attributes values. For instance, the social security number (SSN) can be considered the key attribute for class Person, and, obj1 and obj2 being two objects of class Person (e.g., obtained from two different navigation paths), OCL expression obj1=obj2 is true if and only if these two objects have the same SSN. At the implementation level, this logical equality does not translate into reference equality as it is sometimes necessary to have object clones (e.g., in a distributed system, to keep safe copies of data). Therefore, objects are tested for equality using their equals(o:Object):boolean method. We assume that the equals method is properly implemented (see Item 7 of [5]) so that objects are deemed equal when their key attributes are equal. Sometimes each instance of a class is unique in which case the default equals functionality will suffice as this functionality (inherited from java.lang.Object) only compares reference values for equality, but when this is not the case the equals method must be overridden.

4.4 From OCL Collections to Java Collections OCL has three collection types, namely Set, Bag, and Sequence, whereas, Java only has two main collection interfaces, namely java.util.Set and java.util.List. There is a direct mapping between OCL Set and java.util.Set (since java.util.Set ensures elements appear only once in the collection), and between OCL Sequence and java.util.List (since java.util.List provides methods to access elements in the

35

Carleton University TR SCE-04-03

Version 1, February 2004

collection given their index, this index representing the sequence order). However, OCL Bag does not have a direct Java counterpart. A bag is a collection in which duplicates are

allowed [28]. java.util.Set cannot be used to implement an OCL Bag as it does not allow duplicates. The only possible alternative, which is assumed in the ocl2j approach, is to implement OCL Bag with java.util.List. There are also differences when one tries to map OCL collection operations to methods on java.util.Set and java.util.List (see 0 for a complete mapping). The three following situations are encountered: i. There is a direct mapping between an OCL collection operation and a java.util.Set or java.util.List operation, that is, there exists a Java method

that provides the exact OCL collection operation functionality, sometimes with the exact same name (e.g., OCL operation size() and Java method size()) and sometimes with a different name (e.g., OCL operation includes() and Java operation contains()). ii. The OCL collection operation does not have a direct counterpart but its functionality can easily be derived from existing java.util.Set or java.util.List operations. For instance, OCL operation notEmpty() can be implemented with the negation of the result of operation isEmpty(). An implementation of OCL operation symmetricDifference() on Set can be built from operations removeAll() and retainAll(). OCL operations sum() and count() can easily be built using a for

loop. These transformations are performed by a specialized class within the aspect code, called OclCollection (Figure 4). iii. OCL collection operations that iterate over collections and evaluate an expression (passed as a parameter to the operation) on each element in the collection are more complicated. They do not have a direct Java counterpart and cannot be simply implemented using the operations provided by java.util.Set or java.util.List. These OCL operations are exists, forAll, isUnique, sortedBy, select, reject, collect, and iterate.

36

Carleton University TR SCE-04-03

Version 1, February 2004

Figure 4 The OclCollection class, part of the aspect The latter situation (iii) is also handled by class OclCollection in the aspect, but requires more attention, since the parameter is an OCL expression, whose code must be generated using transformation rules. Indeed, the way any of these OCL collection operations is implemented depends on the OCL expression passed as a parameter. Unless two instances of collection operations share the same OCL expression parameter, every operation instance is implemented with a single method in OclCollection. This is because for each distinct OCL expression parameter, different code needs to be executed. However, a template implementation can be defined for each of these OCL collection operations. (Templates for all collection operations can be found in 0.) As an example, Table 7 provides the template Java implementation for OCL collection operation forAll(expression): This operation returns true if the OCL expression passed as a

parameter evaluates to true for all the elements in the collection, and false otherwise. This template shows that class OclCollection has a method whose name is forAll appended with a number. This number uniquely identifies any single use of OCL operation forAll, and is determined when the aspect is created: In other words, if, when analyzing all the contracts to be transformed into assertions, there are 10 uses of OCL operation forAll, there are 10 methods in class OclCollection named forAll0 to forAll9. Each forAll method in class OclCollection has a parameter which is the collection on which the 37

Carleton University TR SCE-04-03

Version 1, February 2004

forAll is evaluated. The template implementation then consists in a for loop that

evaluates the implementation of the OCL expression passed as a parameter on each element of the collection. The Java code for the evaluation of the OCL expression passed as a parameter to the OCL forAll is produced using the mappings and transformations that are described in this chapter (Chapter 4). (1)private boolean forAll#(Collection ocl2jCollection) { (2) for (Iterator ocl2jIterator = ocl2jCollection.iterator(); ocl2jIterator.hasNext(); ) { (3) ElementType elementRef = (ElementType) ocl2jIterator.next(); (4) if (!(OclExpr predicate in Java Code format)) { (5) return false; } } (6) return true; }

Table 7 Template for the Java implementation (Aspect code) of OCL collection operation forAll

The code commences by defining a forAll method that is private (to the aspect) and that returns a result of Boolean type (1). This is not surprising as the OCL collection operation also returns a result of Boolean type. Notice that the # is used to distinguish this forAll method from other ones in the aspect. This is necessary as such a forAll method is generated for every forAll operation used to specify the system constraints. The method has one parameter, ocl2jCollection of type java.util.Collection that is used to reference the constrained collection. Next, in (2), the for loop is defined to traverse the entire collection. In (3), the body of the loop starts by retrieving an (unprocessed) element from the collection, downcasting it to its type (specified by ElementType), and referring to it by same name local in scope to the loop’s body. Remember that ElementType and elementRef are specified in the body of the forAll constraint. Now that we can refer to

an element in the collection directly we can also check the constraint on this element and this is done in (4). If the element fails the check then there is no point continuing the loop and checking other element as forAll, as the name implies, specifies that the constraint must be true for all the elements in the collection. Thus, the loop is aborted and the

38

Carleton University TR SCE-04-03

Version 1, February 2004

method returns a false value (5). If the entire collection is traversed successfully, then the method returns a true value (6). For example, consider the following constraint on a collection that specifies that in the collection collectionOfPeople that are no persons with empty names: collectionOfPeople->forAll(p:Person | p.name '')

Table 8 shows the method that is added to class OclCollection and is called when this constraint is evaluated. private boolean forAll0(Collection ocl2jCollection) { for (Iterator ocl2jIterator = ocl2jCollection.iterator(); ocl2jIterator.hasNext(); ) { Person p = (Person) ocl2jIterator.next(); if (!((p.name) != (""))) { return false; } } return true; }

Table 8 Example of use of the template implementation of OCL operation forAll

4.5 Using Previous Property Values in OCL Postconditions This section discusses the practical implementation of the OCL language construct @pre, used in postconditions to access the value of an object property at the start of the execution of the operation. Depending on the property that the @pre is associated with different values and amount of data must be stored temporarily until the constrained method finishes executing so that the postcondition can be checked. @pre can be used with respect to one of the following: i.

Java types corresponding to OCL Basic types or query methods that return a value of such a type. The mapping between these types is discussed in Section 4.2. In the case of a primitive type, the primitive value is stored in a temporary variable. In the case of an object, the reference to the object is stored in a temporary variable. Only

39

Carleton University TR SCE-04-03

Version 1, February 2004

the reference is stored as these types are immutable and thus they cannot change (during the execution of the constrained method). ii.

Query methods that return an object. In this case the objects are handled in the same way as described above, only the reference to that object is stored in a temporary variable (duplicated), the object itself is not cloned. The object is not cloned as we assume that the SUS is written with proper encapsulation techniques, meaning that query methods that return an object to which the context class (the class containing the query method) is related via composite aggregation return a clone of the object, not the object itself. This is standard practice as discussed in Item 24 of [5].

iii.

Objects (references to objects). The object types in this discussion exclude the ones discussed in the points above. In this case a clone of the object is taken and stored in a temporary variable. We assume that the programmer properly implements cloneability support (as will be discussed).

iv.

Collections. A collection’s identity is defined by the elements in that collection, thus a clone of a collection contains a clone of every element in the original collection. Using @pre on a collection will result in such a duplication of the collection in most cases. When the OCL collection operation being invoked on someCollection@pre

is

size():Integer,

isEmpty():Boolean,

notEmpty():Boolean, or sum():T then only the result of the operation is stored in

the temporary variable. We note that in a lot of cases it may not be necessary to duplicate the collection in such a manner to enforce the postcondition correctly, but this is a subject for future work. In this work our primary goal is to enforce constrains correctly, efficiency is the next goal. For a guide to providing support for cloneability see Item 10 in [5]. Essentially, two types of cloning methods exist. First, a shallow copy is where the fields declared in a class and its parents (if any) will have values identical to those of the object being cloned. In the case of a class exhibiting one or more composite relationships the shallow copy is not

40

Carleton University TR SCE-04-03

Version 1, February 2004

sufficient and a deep copy must be used—during a deep copy all the objects in the composition hierarchy must also themselves be cloned. To understand why, recall our objective here: We need access to the objects, as they were, before the constrained method executed. Objects are uniquely identified by their key attributes (key attributes are discussed in Section 4.3). If these objects have composite links to other objects (i.e., their class has composite relationships), thus forming a hierarchy of objects, the key attributes may be located anywhere in the hierarchy. A deep copy is therefore necessary. The DOT only handles the most trivial cases associated with the @pre keyword: cases i and ii. Postconditions are thus not properly checked and checking them can result in false-positives and false-negatives.

4.6 OclType::allInstances() and oclAny::oclIsNew() Any OCL type in a UML model, including user-defined classes, is an instance of OclType: OclType allowes access to meta-level information regarding the UML model.

In particular, OclType::allInstances() returns the set of all instances of a type and all its subtypes at the time that the expression is evaluated. In addition, every type in OCL is a child class of OclAny, i.e., all model types inherit the properties of OclAny. Among those properties is operation oclAny::oclIsNew() that can only be used in a postcondition: It evaluates to true if the object on which it is called has been created (using the new construct) during the execution of the constrained method, i.e., it didn’t exist before the execution of the operation. Java does not provide any functionality to which these operations could be mapped to. The implementation of these two operations requires special attention as it would be very messy and in certain cases even impossible to provide an implementation for these two operations

using

pure

Java.

For

example,

consider

proving

support

for

OclType::allInstances(). First, all the classes in the system would have to be

inspected in order to identify subclasses of the type on which allInstances() is being invoked. Then, each one of the identified classes would need to have every one of their constructors instrumented so that when an instance of the class is created it is added to a 41

Carleton University TR SCE-04-03

Version 1, February 2004

collection of all the instances of the class and its subclasses. In the case that allInstances() is invoked on a class for which the programmer does not have the

source-code the instrumentation could not take place (as that class cannot be instrumented at the source-code level), thus, making this OCL functionality impossible to use. The ocl2j solution to the problem of implementing operation allInstances() is the following. If this operation is used on a type in any OCL expression, a collection is added to the aspect. This collection will store references to all the instances of the type at runtime: This is easily achieved with AspectJ as it only requires that the aspect comprises an advice to add, at the end of the execution of any constructor of the type of interest or its subtypes (Section 4.7 exemplifies how aspect that pick out constructs look like), the reference of the newly created instance. This raises the question of the choice of the Java data structure to store those references and the impact of aspect code on object garbage collection in Java: Objects in the instrumented program should be garbage collected if they are not used in the application code, even though they may be referenced by the aspect code. A solution to this problem is to use class java.util.WeakHashMap to store these references in the aspect. This was specifically designed so as to store references that would not be accounted by the garbage collector. It is based on a hash map where the keys are (weak) references to the objects we are monitoring. The garbage collector can get rid of an object, even when this object is still referenced, provided that these references are only used in instances of class WeakHashMap. When this is the case, the object is garbage collected and any reference to it removed from instances of the WeakHashMap.

The instrumentation of operation oclAny::oclIsNew() is very similar to that of OclType::allInstances() with the difference between the scope in which new

instances are being observed. For OclType::allInstances() the scope is total application running time while for oclAny::oclIsNew(), the scope is the execution time of the constrained method. Thus, after the constrained method finishes executing and the postcondition is checked the collection of instances (created during the execution

42

Carleton University TR SCE-04-03

Version 1, February 2004

of that method) is discarded.

4.7 Usage of AOP in This Work Once the OCL contracts are converted into Java code, this generated code must be integrated with the SUS code (this process is called instrumentation). As discussed before, it has been decided to instrument the bytecode instead of the source code (Section 3.1), thus the use of the AOP technology and AspectJ. The aspect code which contains the assertion code (for all constraints) is stored in the Ocl2jAspect.java file. The AspectJ code template for enforcing preconditions is shown

in Figure 5. before(SomeClass self[, parameters of the method – if any]): execution( [visibility] [method return type] SomeClass.someMethod([parameter types of the method - if any])) && target(self) && args(parameter_names) && if (self.getClass() == SomeClass.class) { // Check the precondition. if (!(predicate code)) { constraintFailed("the constraint in OCL form"); } }

Figure 5 AspectJ code template for enforcing preconditions First, a before advice declaration is used to indicate that the advice will be executed before the specified pointcut executes. Next, the execution context is exposed: SomeClass self specifies that the pointcut acts on an object of type SomeClass and that

the object is accessible to the advice code via the variable self. Likewise, the arguments (if any) used to invoke the method specified by the pointcut are exposed to the advice code. Then, the pointcut is specified: execution( … )

target(self)

This specifies the method executions to be intercepted by providing the method’s visibility, its return type, and the method’s signature, prepended by its class name. This refers the target objects considered for the method executions to be intercepted. self can then be accessed in the advice code.

43

Carleton University TR SCE-04-03

args( … )

if ( … )

Version 1, February 2004

This refers to the arguments of the specified method executions. They can then be referred to in the advice code. This is omitted if the method has an empty parameter list. Method getClass() returns the object of type Class that represents the runtime class of the object. The first if statement checks that the instance is really an instance of the class SomeClass and not one of its descendents. (Another, similar before advice addresses executions on descendents to address constraint inheritance.) In that case, the precondition is checked (i.e., its corresponding predicate code, generated using the above transformation rules). Otherwise, method constraintFailed() either throws an unchecked exception (defined in aspect code) or prints an error message on the standard output.

The AspectJ template code for enforcing invariants is shown in Figure 6.

1

2

3

4

private void inv[number](SomeClass self) { if (!(predicate code)) { constraintFailed("the constraint in OCL form"); } } // Before the execution of every public method in SomeClass before(SomeClass self) : execution(public * SomeClass.*(..)) && target(self) { inv[number](self); // call to the operation above } // After the execution of every public method in SomeClass after(SomeClass self) : execution(public * SomeClass.*(..)) && target(self) { inv[number](self); // call to the operation above } // After the execution of any constructor in SomeClass after(SomeClass self) : execution(public SomeClass.new(..)) && target(self) { inv[number](self); // call to the operation above }

Figure 6 AspectJ code template for enforcing invariants In (1) the method to check an invariant is specified. This is done to avoid repeating the code in the multiple advice bodies. As systems usually have more than one invariant, each invariant method name is appended with a unique integer to distinguish it (from other invariant-checking methods in the aspect). The method has one parameter which takes the reference to the object upon which the invariant needs to be checked. Next, (2) 44

Carleton University TR SCE-04-03

Version 1, February 2004

ensures that the invariant is satisfied before any public method within the context class executes. This is done by specifying in the pointcut by using the “*” wildcard instead of specific method name and “..” is used in the parameter list to signify that the pointcut applies to methods with any parameter list. Likewise, (3) ensures that the invariant is checked after the same methods as specified in (2) execute. Finally, (4) ensures that the invariant is checked after the execution of all public methods that create an instance of the object, e.g., constructors and clone(). In the case that public methods in the model map onto protected methods in the code, (23) is re-specified for those methods. Also, if constraint inheritance is not desired the pointcuts would contain if statements to contain the checking of the invariant to a specific type. The AspectJ code template for enforcing postconditions is shown in Figure 7. // Around the execution of the someMethod in SomeClass someMethod return type around(SomeClass self [, parameters of the method – if any]) : execution([visibility] [method-return-type] SomeClass.someMethod( [parameter types of the method – if any])) && target(self) && args(parameter names) && if (self.getClass() == SomeClass.class) { // Create any @pre variables needed // Let the execution of the method proceed. [method-return-type result;] [result =] proceed(self [, parameters of the method – if any]); // Check the postcondition. if (!(predicate code)) { constraintFailed("the constraint in OCL form"); } }

Figure 7 AspectJ code template for enforcing postconditions. Ensuring postconditions can be a little different than ensuring preconditions and invariants as postconditions allow for the use of additional constructs, like the @pre keyword that saves the state of an attribute before the method executes for use in the postcondition after the method finishes executing. Also, postcondition constraints have access to the return value of the method (via the result keyword), thus this must be

45

Carleton University TR SCE-04-03

Version 1, February 2004

provisioned for. The around advice type is ideally suited for this situation as it trivially solves both of the aforementioned problems. It does so thanks to the fact that the around advice intercepts the postcondition-constrained method before it executes allowing for the creation of @pre-related values, also, it has constructs to allow for access to the method’s return value after the method is allowed to execute by the code in the advice (via the locally declared result variable). All aspect code is placed inside the Ocl2jAspect aspect which is privileged aspect, meaning that code in that aspect has access to private members of other types. In the following example, class Person has attributes age, salary, and maxSalary, all of type Integer in the UML model (java.lang.Integer in code). Additionally, class Person has a method called implementRaise(raise:int) that raises the person’s

salary. The class invariant for Person and the precondition and postcondition for implementRaise are the following:

• •

context Person inv: self.age >= 18



context Person::implementRaise(raise:int) post: self.salary = self.salary@pre + raise

context Person::implementRaise(raise:int) pre: self.salary + raise = (18))) { constraintFailed("self.age >= 18"); } } // Before the execution of every public method in Person before(Person self): execution(public * Person.*(..)) && target(self){ inv0(self); } // Around the execution of the implementRaise method in Person before(Person self, int raise) : execution(public void Person.implementRaise(int)) && target(self) && args(raise) && if (self.getClass() == Person.class) { if (!((self.salary.intValue() + raise) size() = 2 -- Enforces the association end cardinality 1..* and self.partners->size() > 0 -- When a LoyaltyProgram does not earn or burn points then its -- members do not have LoyaltyAccounts and self.partners->collect(pp:ProgramPartner|pp.deliveredServices) ->forAll(s:Service|s.pointsEarned=0 and s.pointsBurned=0) implies self.membership->collect(m:Membership|m.loyaltyAccount) ->isEmpty() -- Each customer must have a distinct name and self.customer->forAll(c1:Customer, c2:Customer | c1c2 implies c1.namec2.name) and self.customer->isUnique(c:Customer|c.name) -- Each customer must be 18 years of age or older and self.customer->forAll(c:Customer|c.age >= 18) context LoyaltyProgram::enroll(c:Customer) pre: -- The customer is not yet enrolled in the system not self.customer->includes(c) post: -- The customer is enrolled in the system self.customer@pre->including(c) = self.customer and -- The new customer’s loyalty account has no points and no -- transactions self.membership->select(m:Membership|m.customer = c) ->forAll(m:Membership| m.loyaltyAccount->notEmpty() and m.loyaltyAccount.points = 0 and m.loyaltyAccount.transactions->isEmpty())

Table 15 Class LoyaltyProgram‘s invariant and pre- and postcondition for LoyaltyProgram::enroll(c:Customer)

Method LoyaltyProgram::enroll(c:Customer) creates a CustomerCard object and a LoyaltyAccount object, and invokes ProgramPartner::newCustomer(). This leads to

the checking of: the invariants on CustomerCard and LoyaltyAccount after the

55

Carleton University TR SCE-04-03

Version 1, February 2004

execution of their constructor, and the ProgramPartner invariants (checked twice before and after the execution of the method) along with the pre- and postconditions of ProgramPartner::newCustomer() (Table 16). context CustomerCard inv: -- The start-date on the card is younger the end-date self.validFrom.before(self.validThru) -- The card’s colour must be either silver or gold and self.colour = Colour.SILVER or self.colour = Colour.GOLD -- The printed-name is a derivation of the title and name and self.printedName = self.owner.title.concat(self.owner.name) context LoyaltyAccount inv: -- An account cannot have a negative points balance self.points >= 0 context ProgramPartner inv: -- A program partner’s name must always be non-blank and it must -- not have a negative number of customers self.name '' and self.numberOfCustomers >= 0 context ProgramPartner::newCustomer() post: -- Ensure that the number of customers is correct self.numberOfCustomers = self.loyaltyProgram->collect(lp:LoyaltyProgram|lp.customer)->size()

Table 16 Invariants for classes CustomerCard, LoyaltyAccount, ProgramPartner, and postcondition for operation ProgramPartner::newCustomer After profiling the assertion code it appears that the degradations in performance are mostly due to the constraints on collections. Consider for instance this relatively simple part

of

class

LoyaltyProgram‘s

>isUnique(c:Customer|

invariant

c.name).

(Table

15):

self.customer-

Collection

operation

isUnique(expr:OclExpression):Boolean results in true only if expr evaluates to a

different value for each element in the collection. In the R&L system, customers must have a unique name. To evaluate this constraint the constrained collection must be traversed, the result from applying expr on each element must be stored in a new collection (say isUniqueCollection). If the result of expr on an element of someCollection already exists in isUniqueCollection then we know the constraint is

falsified. Checking this constraint is CPU intensive, all the more so since the invariant 56

Carleton University TR SCE-04-03

Version 1, February 2004

must be checked before and after the execution of every public method in LoyaltyProgram.

Thus, programs that have large collections with many complicated constraints associated with these collections can expect degradation in execution time of 2 to 3 times (Table 14). Otherwise, the degradation in performance is relatively small; the execution speed is slowed down by roughly 50%. This problem of degradation in performance is further investigated in Chapter 7 where possible optimizations of the aspect are suggested. 6.2.3

Memory Footprint

The memory footprint of a program shows the total amount of memory the program uses and consists of the memory usage by objects, classes, threads, native data structures, and native code. The memory footprint of the two versions of the SUS was measured under the same five scenarios as described in the previous section (Table 13). Table 17 summarizes the memory footprint. Instead of a memory profiling during the whole execution of the five scenarios, the footprint was measured using the Windows Task Manager at the end of the execution of the scenario (before the program terminated). Memory Footprint Scenario

Original Instrumented

1 (1, 1) 2 (10, 10) 3 (100, 100) 4 (1, 100) 5 (100, 1)

4,924K 4,936K 5,164K 4,928K 5,304K

5,352K 5,388K 5,884K 5,404K 5,860K

Percentage Increase in Memory Footprint 8.7% 9.2% 14% 9.7% 10.5%

Table 17: Memory footprint comparison The results in Table 17 indicate that the memory footprint increases are not very large as they range from 8.7% to 14%. The increase in the memory footprint is caused by the assertion code that must be added and the additional objects that must created. Given that the SUS is a small program with a large number of often complex constraints it is safe to assume that in most cases the percentage increase in the memory footprint size will probably not exceed 14%.

57

Carleton University TR SCE-04-03

Version 1, February 2004

6.3 Conclusion This section shows the use of the ocl2j tool on the implementation of an expanded version of Royal and Loyal system found in [28]. This is a small program (381 LOC) with a relatively large number of classes (14), operations & constructor (46), and most importantly OCL constraints (47), often complex. Effects of the instrumentation were measured in three ways: bytecode size increase, execution time increase, and memory footprint increase. The execution time and the memory footprint increases were measured in five scenarios: adding one customer to the system and making one purchase, adding ten customers to the system and making ten purchases, adding 100 customers to the system and making 100 purchases, adding one customer to the system and making 100 purchases, and adding 100 customers to the system and making one purchase. Though additional case studies should be considered to draw more general conclusions, the R&L system allows us to reasonably come to the following conclusions. After the instrumentation the bytecode grew up to three times in size. This increase in size was attributed to the fact that this small program has a large number of operations with very small method bodies and a large number of (often complex) constraints. In a realistic application the increase of the bytecode is expected to be much smaller as this system used an exuberant number of constraints. The execution time of the instrumented SUS was shown to increase between 50% and 100% with respect to the execution time of the uninstrumented SUS for scenarios where large collections were not used. In scenarios where large collections are used the execution time can be substantially greater. This was attributed to the large number of complex constraints on these collections that had to be repeatedly checked. Though this is a limitation, Chapter 7 proposes an extension to the current methodology that has the potential to overcome this limitation.

58

Carleton University TR SCE-04-03

Version 1, February 2004

In all five scenarios the increases to the memory footprint were reasonable, between 8.7% and 14%. Given the nature of this particular SUS, it is expect that most real applications will undergo an increase in the memory footprint closer to 10% and rarely over 14%. Finally, effects of falsified assertions were shown on all three types of constraints: invariants, preconditions, and postconditions. In all cases an error is thrown showing the user the constraint that failed along with the location of the failure. Considering the fact that this was a small program with a large number of complex constraints it would be reasonable to assume that the results displayed here can viewed as “worst-case” scenarios. Whether this tool proves to be a viable solution for most systems remains to be demonstrated by further experiments. Systems with large collections and complex constraints upon those collections may suffer unreasonable increases in execution time.

7 POSSIBLE OPTIMIZATIONS This Chapter focuses on investigating strategies to optimize the instrumentation of constraints on collection operations implemented using OCL collection operations. Because such operations are largely responsible for execution time overhead, they need to be the focus of any optimization effort. In general one can consider the following three strategies to check constraints on a collection (Figure 12): 1. Every time the constraint is checked the entire collection is traversed. This is the solution currently used in the non-optimized version of ocl2j. This strategy is expensive in terms of execution time if the SUS has complex constraints on large collections (see Section 6). Figure 12 (a) illustrates this solution. 2. The constraint is only checked when a change to the collection is detected (addition or deletion of element, change of element’s state), and checking the constraint requires the traversal of the whole collection (Figure 12 (b)). This is more efficient than the previous solution, as the constraint is only checked when necessary, but is 59

Carleton University TR SCE-04-03

Version 1, February 2004

still very inefficient as the whole collection is systematically checked. This is the strategy adopted in the DOT (partially, though, as it does not detect all changes to collections – see Section 2.3.1). Note that this requires that any change to the collection be detected. 3. The constraint is only checked when a change to the collection is detected, and checking the collection is traversing only the part of the collection that is relevant to the constraint. For instance, assuming a constraint is that the age of persons in a collection is to be above 18 and the age of a particular person in the collection is changed, we only have to check the constraint on that person object: The changed person object is the only part of the collection relevant to the constraint. This solution is illustrated by Figure 12 (c): The aspect intercepts the change and sets a flag indicating whether the constraint is falsified; The flag is then checked after a call to a public method. This last solution is the most efficient one but also more complex: How to identify changes to collections and the part of the collection that has to be checked after a change. Note that the entire collection can be relevant to the constraint: For instance, assuming persons in a collection must have distinct names, and a person’s name is changed, we have to check the constraint on each pair on person object. This is the solution pursued in this section. To each collection type and each collection operation corresponds to a specific strategy to optimize the mode in which a constraint is verified on the collection.

Modify an element in the collection (add, delete, state change)

Modify an element in the collection (add, delete, state change)

After a call to a public method. Check the constraint: traverse the whole collection.

After a call to a public method. Check the constraint only if change detected: traverse the whole constraint.

(a)

(b)

Aspect: Intercept the change.

Modify an element in the collection (add, delete, state change)

Aspect: Intercept the change. Check the constraint on what has changed. Set a flag.

After a call to a public method. Check the flag.

(c)

Figure 12 Checking constraints – Three strategies

60

Carleton University TR SCE-04-03

Version 1, February 2004

Section 7.1 discusses how changes to collections can be observed using AspectJ. Section 7.2 discusses how the optimization technique modifies the assertion code. Section 7.3 discusses the difference between simple and derived collections, that is, intermediate collections that have to be built when checking a constraint. Section 7.4 describes optimization techniques for OCL collection operations that do not list OclExpression in the parameter list (along with an explanation as to why such a distinction is necessary), whereas section 7.5 discusses the more complicated case of optimization techniques for OCL collection operations that do list OclExpression in the parameter list. Section 7.6 presents the results of a prototypical implementation of the optimizations for the collection->includes(object:OclAny):Boolean operation, and finally Section 7.7

discusses performance overhead issues due to the usage of AspectJ.

7.1 Detecting Changes with AspectJ Depending on the collection operation (the reason for differentiating between different collection operations will be explained in detail in section 7.4), the following changes to a collection have to be considered: (i) A change in the number of elements the collection has (induced by elements being added or removed from a collection); (ii) A change to a property of an element in the collection (recall properties name and age of class person in the previous example). AspectJ provides functionalities for intercepting writes to attributes and invocations of operations. In case of constraints on collections (via OCL collection operations) these functionalities are exploited in four ways for change detection purposes: 1. If an attribute referencing a collection that participates in a constraint is changed (i.e, a different collection is referenced), then we know we need to recheck the entire collection to see if it satisfies that constraint. 2. Similarly, it is important to observe changes to attributes of objects involved in collections (recall the previous example). 3. Operations used to modify collections can be intercepted to see how the collection is being changed (adding/removing elements). This also gives us the opportunity to 61

Carleton University TR SCE-04-03

Version 1, February 2004

inspect (e.g., check a constraint) the elements being added and removed. 4. In some collection operations an object is specified in the parameter list (e.g., collection operation includes). If this object changes, or one of its attributes changes, then the collection must be rechecked.

7.2 Implementing Collection-Operations in Assertions This section illustrates in more details, using a specific example (Table 18), the optimization approach suggested before. The simple example involves two classes, World and Person, and in invariant for the former. In this example, the overhead in checking this contract before and after each public operation call is tremendously larger if the association multiplicity is large, as the collection of Person elements would have to be constantly traversed and validated. World -countryNum : Integer

*

Person +name : String

context World inv: countryNum > 0 and person->forAll(p:Person|p.name '')

Table 18 Optimization – An example constraint If this large collection undergoes small changes an optimized solution is to replace the person->forAll(p:Person|p.name '') part of the contract with a value that would

reflect the result of this Boolean expression, say Boolean value nonEmptyName. This Boolean value should then be updated only when there is a change to the collection: If the added/changed person’s name does not satisfy name '' then the Boolean value is set to false. This is performed by the aspect code. Figure 13 illustrates this strategy. The rationale is that if person->forAll(p:Person|name '') ever ceases to be true (this is checked whenever the collection is modified) then nonEmptyName is set to false to ensure that the next invariant assertion check fails. If instead it never ceases to be true, nonEmptyName is left set to true.

62

Carleton University TR SCE-04-03

Modify an element in the collection of persons (add, delete, state change)

Version 1, February 2004

Modify an element in the collection of persons (add, delete, state change)

Aspect: Intercept the change. Element X has been changed, check the constraint on X: nonEmptyName=(X.name’’).

After a call to a public method of World. Check person->forAll(name’’): traverse the whole collection.

After a call to a public method of World. Check (nonEmptyName==true).

(a)

(b) Figure 13 Optimization – Illustrating the strategy

7.3 Simple and Derived Collections In this report, a collection is said to be simple when it does not have to be computed from other collections, that is, it is stored in a class attribute in the source code (it corresponds to an association in the class diagram). A derived collection is a collection formed from another collection or collections. For instance, in the context of a Book instance in a Library system, self.reservation.customer is the collection of all the customers who reserved

the

book.

self.reservation

is

a

simple

collection

whereas

self.reservation.customer is a derived collection.

When a constraint has to be evaluated, any derived collection it involves has to be constructed. This can be time consuming if this is done each time the constraint is evaluated, and may not even be necessary when a derived collection does not undergo any change. Instead, an optimized solution is to build, store and update those derived collections in the aspect, so that when they are needed for the evaluation of a constraint, we do not have to build them from scratch. For example, in the Library system, since self.reservation.customer is used in a contract, it is built in the aspect and kept up to

date by the aspect so that it can be used when needed.

63

Carleton University TR SCE-04-03

Version 1, February 2004

7.4 Common Operations Not Listing an OclExpression in the Argument List As it has been discussed in Section 4.4, we can distinguish two kinds of OCL collection operations: Those that have a parameter of type OclExpression and those that do not. We then showed why it is more difficult to produce assertion code in the former case than in the latter. What is common to all the collection operations that do not accept a parameter of type OclExpression is that their result is not subject to state changes of the elements they contain, e.g., if in a collection of books a specific book changes from state Borrowed to state Onshelves, the number of books in the collection does not change

(i.e., the result of operation size does not change). Their result is only subject to additions and deletions of elements to and from the collection. On the other hand, the result of collection operations that have a parameter of type OclExpression is subject to additions, deletions, but also state changes. This section handles the simpler case where the operation does not have a parameter of type OclExpression, while the subsequent section deals with the more complicated case. Therefore, in this section, a change to a collection refers to the addition or removal of an element.

Following the principles introduced in previous sections: Additions and deletions to collections are monitored by the aspect at runtime; The impact of those additions/deletions on the result of the OCL collection operation is evaluated and the result is stored in the aspect. (Is the result of the OCL collection operation changed because of an addition/deletion?) And the assertion checking the constraint in which the OCL collection operation is used only consists in checking the result stored in the aspect. Examples of optimization of OCL collection operation without parameter of type OclExpression can be found in Appendix D. As a concrete example, consider operation collection->includes(object:

OclAny):

Boolean. The optimization of this

operation consists in adding an attribute called numOfOccurences in the aspect. This attribute value is updated at runtime according to the following pseudo algorithm:

64

Carleton University TR SCE-04-03

Version 1, February 2004

If the added/removed element is the object passed as a parameter to includes() then If this is an addition then numOfOccurrences++ Else numOfOccurrences— EndIf EndIf

Last, the assertion checking the constraint is simply: numOfOccurences>0. There are three exceptions to this approach, for OCL collection operations size(), isEmpty(), and notEmpty(): These are the only operations for which no optimization is

performed. The reason is that it would be more expensive, in terms of performance, to perform an optimization than just execute them. Note that if the collection on which the operation is performed , or the object passed as a parameter (if any), is changed to a new collection (or object) reference, then the result must be recomputed from scratch. We may end up traversing the whole collection. In the above example, if the reference of the object passed as a parameter to includes() changes, we have to verify that a different object belongs to the collection.

7.5 Common Operations Listing an OclExpression in the Argument List The OCL collection operations that have a parameter of type OclExpression all have the following signature: collection->operationName(OclExpression): ResultType. In the previous section results of collection-operations where kept track of in real-time. This is not a viable solution here as the time when the OclExpression parameter is checked on the elements of such a collection matters. Therefore, this parameter evaluation can only take place when the assertion is executed. Consider the following invariant for class World, associated with class Person (Table 18): context World inv: person->exists(p:Person|p.name='John' and p.age=18).

65

Carleton University TR SCE-04-03

Version 1, February 2004

Now consider a method in the context class that does the following: (1) It adds a person to the collection without initializing attributes name and age (name = '' and age = 0); (2) It sets the name and age to 'John' and 18, respectively. Notice that if the above invariant was checked in real-time, following the strategy proposed in the previous section (i.e., right when the modification occurs), it would fail as soon as the element is added to the collection (in step 1 above). In order to solve this problem, we would have to monitor every single modification of those two attributes during the execution of the method, which is costly. More generally, all modifications to relevant attributes pertaining to the OclExpression passed as a parameter must be intercepted (in the example above, name and age). In order to optimize the verification of OCL constraints involving those OCL collection operations, we propose the following strategy. When such an operation is used on a collection, changes to the collection (additions/deletions) are monitored. After the collection is changed by the addition or removal of an element, the results of the operation on the collection must be updated after the method finishes executing. This means that the changes to the collection must be kept track of (stored in the aspect), the following is one of to keep track of these changes: Elements added to the collection are stored in an added elements delta (AED) collection, and elements removed from the collection are stored in a removed elements delta (RED) collection. These changes are collected at run-time: Any time an element is added to (resp. removed from) the collection it is also added to the AED (resp. RED). Additionally, elements that undergo changes to attributes that are used in an OclExpression parameter must be collected, i.e., attributes that are used on the collection iterator (name and age in the above example, p being the iterator). After the method executes those elements must also be checked to ensure the satisfaction of the constraint on the collection. If OclExpression includes modified attributes on other objects than the iterator then the collection may have to be rechecked in its entirety. Consider the following example with respect to Table 18 where a change to self.countryNum may require the entire collection to be checked from scratch:

66

Carleton University TR SCE-04-03

Version 1, February 2004

context World inv : self.person->forAll(p:Person | -- If we have less than five countries than a person name can be -- a minimum of five characters, otherwise longer names are -- required to ensure everyone has a distinct name. if (self.countryNum < 5) then p.name.size() => 5 else p.name.size() => 10 endif

This invariant specifies that if there are fewer than 5 countries in World then the Person elements in the self.person collection can have names (specified by the name attribute) with a minimum of five characters. If the number of countries grows beyond 4 then the names must have at least 10 characters. The self.person collection would have to be rechecked from scratch if self.countryNum changes from a value smaller than 5 to a value equal to or greater than 5. As an example, consider OCL collection operation exists, whose signature is the following: collection->exists(expr:

OclExpression):

Boolean. More OCL

collection operations having a parameter of type OclExpression can be found in Appendix E. The optimized instrumentation of this collection operation consists of the following: -

The elements (if there is more than one) satisfying the constraint are stored in a collection in the aspect, the result-collection.

-

Elements that satisfy expr are observed. If relevant properties (i.e., attributes used on the iterator in expr) are changed on one of those elements then expr is reevaluated on them to see if they should still reside in the result-collection.

-

Upon evaluating expr on the AED and RED collections, elements in the AED satisfying expr are added to the result-collection (if not already present assuming the collection can have duplicates) and elements found in both RED and the result-collection are removed from the result collection. 67

Carleton University TR SCE-04-03

-

Version 1, February 2004

A result-collection with a size of zero equals to the exists operation returning false and a non-zero size equals to the exists operation returning true.

-

Upon an attribute modification the result-collection must be regenerated from scratch. This is also the case if variable collection is set to point at a non-equal reference (using the Java assignment operator ‘=’).

7.6 Preliminary Results The ocl2j tool was modified to support the optimization strategy for the collection->includes(object:OclAny):Boolean collection operation. The effects of

this optimization are measured by comparing two versions of an instrumented SUS: The optimized version and the unoptimized version. The code for both versions of the aspects can be found in [10]. The SUS consists of a class with two attributes: Attribute c is a collection of type java.util.ArrayList while attribute crucialElement specifies an element that must

always reside in the collection specified by c. The constraint on the SUS is then: context One inv: self.c->includes(self.crucialElement)

The instrumented versions of the SUS are compared in terms of execution time for the two scenarios described in Table 19. In the scenarios the location of the element to be searched for (specified by self.crucialElement in the OCL constraint) is highlighted as this location determines the performance of the un-optimized solution. These two scenarios have been chosen as they clearly favour either the un-optimized or the optimized solution. Since the un-optimized solution traverses the whole collection and stops as soon as the searched element is encountered, it is expected that the unoptimized solution perform better on scenario 1 than the optimized solution. On the other hand, since the optimized solution updates an attribute (i.e., numOfOccurrences) after each addition/removal to/from the collection (Section 7.4), it is expected that the optimized solution perform better on scenario 2 than the un-optimized one.

68

Carleton University TR SCE-04-03

Scenario

1

2

Version 1, February 2004

Description A collection of 1000000 elements is used where the crucial element is the first element in the collection. Then, 100000 elements are added to the end of the collection using a public addElement operation. Since the operation used to add the elements is public the invariant is checked before and after every operation call. A collection of 1000000 elements is used where the crucial element is the last element in the collection. Then, 1000 elements are added to the end of the collection using a public addElement operation. Again, the invariant is checked before and after every operation call. Unlike in scenario 1 where 100K elements were added, only 1K elements are added in this scenario as this is enough to show the huge execution time improvement when optimizations are used.

Table 19 Scenarios for evaluating the optimization of OCL collection operation includes.

The execution times of both versions of the SUS under these scenarios are presented in Table 20. They were measured in the manner described in Section 6.2.2.

Scenario 1 Scenario 2

Execution Time (ms) Unoptimized version Optimized version 328 350 78,359 less than 1

Table 20: Preliminary Optimization Results Even though these results are very preliminary the following conclusions can be drawn from this experiment. Scenario 1 was designed to show the cost of the overhead that the optimization technique introduces and it shows the overhead in negligible. Scenario 2 models the situation in which the limitation of the un-optimized approach is exposed; in this case it was shown that the optimized solution tremendously outperformed the unoptimized solution. From this preliminary study it can be seen that these optimization techniques hold much promise, though further investigation is required.

7.7 Impact on Performance Due To AspectJ The impact on performance due to the usage of AspectJ does become an issue when optimizations

are

implemented.

Consider

this

arrayListCollection->includes(someObject).

constraint To

on

detect

a

collection, whether 69

Carleton University TR SCE-04-03

Version 1, February 2004

arrayListCollection includes the someObject element the following must be done for

every method that can change the state of the collection: •

All operation invocations that can invalidate the constraint must be intercepted (for example, the remove(object) operation).



A check must determine whether this operation is being invoked on the constrained collection (remember that the operation will be intercepted on every collection object as it is not possible to specify the collection object of interest directly).



Data structures holding optimization related information must be updated (For example, in the case of the arrayListCollection->includes(someObject) operation this might be the number of times the someObject occurs in arrayListCollection).

This quickly amounts to a large performance penalty and will be the subject of future research.

8 CONCLUSIONS AND FUTURE WORK We have presented a methodology, supported by a prototype tool (ocl2j), to automatically transform OCL constraints into Java assertions and instrument these into the target program. The ocl2j approach consists of two main steps: Generation of Java assertions from OCL constraints, and instrumentation of those assertions into the system under study (SUS). Generation first consists in automatically extracting OCL constraints from a model of the SUS. Each constraint is then converted into an abstract syntax tree using a parser generated by a compiler compiler (SableCC). The abstract syntax tree is traversed while transformation rules (from OCL to Java) are applied, resulting in the generation of assertion code. During this process, the model and the source code of the system under study are queried when necessary (e.g., to map OCL types to Java types). The transformations rules have been derived in a systematic manner with the goal that upon instrumentation the generated assertion code be efficient and introduce a low memory 70

Carleton University TR SCE-04-03

Version 1, February 2004

overhead (i.e., the assertion code creates and stores information if and only if that information is not already available in the SUS). This was largely achieved thanks to the definition of semantic actions on production rules in the OCL grammar, so as to account for every single OCL construct. As a result, almost the whole syntax of OCL version 1.3 [28] is supported: The only constructs not currently supported are Enumeration and OclState (and operation OclInState), though this can simply be achieved by assuming some coding standards. The aspect-oriented programming (AOP) technology helps overcome the problems of source code pollution as the assertions are not inserted into the SUS source code, but into an aspect file, which is woven with the SUS bytecode. Additionally, this technology provides mechanisms to observe and modify the behaviour of the SUS so as to execute the assertions (checking OCL constraints) at the right time during the SUS execution. AOP

also

helps

to

easily

provide

support

for

the

OCL

operations

OclType::allInstances() and oclAny::oclIsNew(). In this work, we provided

precise templates to specify how to best use AOP for run-time verification of contract assertions. The user of ocl2j can then specify whether a runtime exception is thrown or an error message is printed to the standard error output upon the falsification of an assertion when the SUS executes. The ocl2j approach was evaluated on a case study according to three criteria: bytecode size, execution time, and memory footprint. It was revealed that most of the time the increase in all three areas was not excessive. Most importantly, most of the time, the execution speed increases between 50% and 100% while the memory footprint increases by 8.7% and 14%. Analysis has shown that increases to the three areas are dependent on several program properties, like the total program size, the number of constraints specified, and the complexity of the constraints. Complex constraints on large collections may result into excessive execution time increases. It was then shown that this major limitation can be overcome using functionality found in AspectJ and optimize assertions using collection operations.

71

Carleton University TR SCE-04-03

Version 1, February 2004

In conclusion, this work shows that in most cases transformation of OCL constraints to Java assertions can be done in an efficient and effective manner. The overhead introduced by the assertion code is acceptable in most systems as generally the instrumented version of the system is only used during development and testing. Furthermore, a strategy to optimize the most time consuming assertions operations on collections has been proposed but needs, however, to be further investigated. Different directions can be investigated for future work. We should consider expanding the work to support OCL version 2. Second, additional case studies should be performed in order to understand the impact of ocl2j instrumented assertion code better. For example, in the case study used in this work the bytecode grew three times in size after the instrumentation of the assertion code. It is currently believed that this is not representative of what will happen with real programs, as the case study is small in size and shows very complex OCL constraints. ocl2j should be integrated with the Dresden DOT’s framework [13] in order to take advantage of their semantic analysis package and the normalization package. This would make the tool more user-friendly. In terms of tool integration, ocl2j should also be integrated with a UML case tool such as Rational Rose or Eclipse. Last, upon detection of an assertion failure the displayed error message could provide more information as to the way the assertion failed, instead of just reporting that the assertion failed and the location of the failure. For example, values of variables associated with the constraint should be displayed. Such information (easily available within the aspect) will help the user with the debugging process.

REFERENCES [1]

Errata for {Kleppe, 2003 #9}, http://www.klasse.nl/english/boeken/errata.html

[2]

A. Aho, R. Sethi and J. Ullman, Compilers - Principles, Techniques and Tools, Addison Wesley, 1985.

[3]

A. W. Appel, Modern Compiler Implementation in Java, Cambridge University Press, 2nd Edition, 2002.

[4]

AspectJ-Team, AspectJ Quick Reference, 72

Carleton University TR SCE-04-03

Version 1, February 2004

http://dev.eclipse.org/viewcvs/indextech.cgi/~checkout~/aspectjhome/doc/quick.pdf [5]

J. Bloch, Effective Java: Programming Language Guide, Addison Wesley, 2001.

[6]

G. Booch, I. Jacobson and J. Rumbaugh, Unified Modeling Language User Guide, Addison-Wesley, 1st Edition, 1998.

[7]

L. C. Briand, J. Cui and Y. Labiche, “Towards Automated Support for Deriving Test Data from UML Statecharts,” Proc. ACM/IEEE Int. Unified Modeling Language conference (UML 2003), Carleton University, 2003.

[8]

L. C. Briand, Y. Labiche and H. Sun, “Investigating the Use of Analysis Contracts to Improve the Testability of Object Oriented Code,” Software Practice and Experience (Wiley), vol. 33 (7), 2003.

[9]

L. C. Briand, Y. Labiche, H.-D. Yan and M. Di Penta, “A Controlled Experiment on the Impact of the Object Constraint Language in UML-Based Development,” Carleton University SCE-03-22, 2003.

[10] W. Dzidek, Using Aspect-Oriented Programming to Instrument OCL Contracts in Java, Master's Thesis, Carleton University, 2003 [11] T. Elrad, R. E. Filman and A. Bader, “Aspect-oriented programming: Introduction,” Communications of the ACM, vol. 44 (10), pp. 29-32, 2001. [12] W. Emmerich, Engineering Distributed Objects, Wiley, 1st Edition, 2000. [13] F. Finger, Design and implementation of a modular OCL compiler, Master's Thesis, Dresden University of Technology, 2000 [14] E. Gagmon, SableCC, an object-oriented compiler framework, Master Thesis, McGill University, 1998 [15] E. Gamma, R. Helm, R. Johnson and J. Vlissides, Design Patterns, AddisonWesley, 1st Edition, 1995. [16] T. Gjøsæter, K. Haslum and Ø. Vinningland, “How to design and implement an OCL tool,” Agder University College, 2002. 73

Carleton University TR SCE-04-03

Version 1, February 2004

[17] C. I. GmbH, “OCL Compiler”, http://www.cybernetic.org/prodocl15.htm. [18] H. Hussmann, B. Demuth and F. Finger, “Modular architecture for a toolset supporting OCL,” Proc. UML 2000, Elsevier North-Holland, Inc., 44 (1), 2000. [19] H. Hussmann, F. Finger and R. Wiebicke, “Using Previous Property Values in OCL Postconditions: An Implementation Perspective,” in UML 2.0 - The Future of the UML Object Constraint Language (OCL), 2000. [20] IBM, “Eclipse”, http://eclipse.org. [21] IBM, “Rational Rose”, http://www140.ibm.com/developerworks/rational/products/rose. [22] G. Kiczales, J. Lamping, A. Mendhekar, C. Maeda, C. Lopes, J.-M. Loingtier and J. Irwin, “Aspect-oriented programming,” Proc. ECOOP'97 - Object-Oriented Programming 11th European Conference, pp. 220--42, 1997. [23] A. Kleppe, J. Warmer and W. Bast, MDA Explained: The Model Driven Architecture - Practice and Promise, Addison-Wesley, 1st Edition, 2003. [24] LCI, “Object Constraint Language Environment (OCLE)” 2.0, http://lci.cs.ubbcluj.ro/ocle/. [25] B. Liskov, “Data Abstraction and Hierarchy,” SIGPLAN Notices, vol. 23 (5), 1988. [26] B. Meyer, Object-Oriented Software Construction, Prentice Hall, 2nd Edition, 1997. [27] R. Mitchell and J. McKim, Design by Contract, by Example, Addison-Wesley, 1st Edition, 2001. [28] OMG, OMG Unified Modeling Language Specification, 1.3 Edition, http://www.omg.org, 1999 [29] Sun-Microsystems, Java 2 SDK Standard Edition Documentation, 1.4.1 Edition [30] Sun-Microsystems, A Typesafe Enum Facility for the Java Programming Language, http://www.jcp.org/aboutJava/communityprocess/jsr/tiger/enum.html, 2002 [31] C. Szyperski, Component Software: Beyond Object-Oriented Programming, Addison Wesley, 2nd Edition, 2002. 74

Carleton University TR SCE-04-03

Version 1, February 2004

[32] R. Van Der Straeten and M. Casanova Paez, “Stirred but not Shaken: Applying Constraints in Object-Oriented Systems. Proceedings,” Proc. NetObjectDays, pp. 138-150. [33] J. Warmer and A. Kleppe, The Object Constraint Language: Precise Modeling with UML, Addison-Wesley, 1999. [34] R. Wiebicke, Utility Support for Checking OCL Business Rules in Java Programs, Master's Thesis, Dresden University of Technology, 2000 [35] S. Wilson and J. Kesselman, Java Platform Performance Strategies and Tactics, Addison-Wesley, 1st Edition, 2000.

75

Carleton University TR SCE-04-03

Appendix A •

Version 1, February 2004

OCL Syntaxt Reduction

The restrictions are as follows (these are explained in more detail with examples in [10]):No use of shorthand in the OCL expression of a collection operation: the iterator and its type must be specified and used through the expression (i.e. the iterator cannot be used implicitly).



No use of shorthand in place of the collect collection operation.



The navigation across multiple collections without the use of collection>collect(expr:OclExpression):Collection(expr.evaluationType)

is

not

allowed. •

Only self is used to refer to the context class.



All variable types must be specified.



The infix notation is not used.



The qualified version is used in the navigation to an association.



The context must always be specified.



Only one stereotype (inv/pre/post) is allowed per context.



The following point is only applicable when specifying a constraint on an operation (any pre- or postcondition). In the case of OCL collection operations where the parameter is an OclExpression the iterator name is not the same as the operation’s parameter names.



When specifying the parameter types of a constraint operation, or the return type of that operation, the type must be the Java type used in the implementation.



let statements are not supported.

76

Carleton University TR SCE-04-03

Appendix B

Version 1, February 2004

OCL to Java Transformation Rules

Notes about semantic actions: • Prepare and PreCode collections of code (held in strings). PreCode is associated with a method and executed before the method executes. Prepare is similar in nature but is associated with the whole system as opposed to a specific method. • The following helper methods are invoked in the actions: o generateUniqueName() generates a unique variable name that can be used in the aspect scope. o String.replaceAll(s1: String, s2: String) replaces every occurrence of s1 with s2. o Type.isPrimitiveType() returns true if the type is primitive. o Type.isReferenceType() returns true if the type is reference (as opposed to primitive). o Type.isCollectionType() returns true if the type implements java.util.Collection. o Type.getValueType() returns the type used as the value in a map (java.util.Map). if_expression ::= "if" expression {ex1=expression.code} "then" expression {ex2=expression.code} "else" expression {ex3=expression.code} "endif" ; {return ex1 + " ? " + ex2 + " : " + ex3} Ternary conditional operator ?: is used. If ex1, ex2 and ex3 are i==1, i++ and i--, respectively, the generated code is ((i==1) ? (i++) : (i--)).

expression ::= logical_expression {return logical_expression.code} ;

77

Carleton University TR SCE-04-03

Version 1, February 2004

expression ::= logical_expression {ex1=logical_expression.code} "implies" logical_expression {ex2=logical_expression.code} {ex="!(" + ex1 + ") || " + ex2} ( "implies" logical_expression {ex3=logical_expression.code} {ex="!(" + ex +") || " + ex3} )* ; {return ex} The production rule without semantic actions is: logical_expression "implies" logical_expression ( "implies" logical_expression)* ; Modus Ponens is used to transform A⇒B into ¬A∨B. Modus Ponens is used to transform the first “implies” encountered, and then any additional “implies”.

logical_expression ::= equivalence_expression {ex=equivalence_expression.code} (logical_expression_tail {ex=ex.append(logical_expression_tail.code)})* ; {return ex} Each time we have a logical_expression_tail (if any) we add the corresponding code.

logical_expression_tail ::= logical_operator {op=logical_operator.code} equivalence_expression {ex=equivalence_expression.code} ; {return op.append(ex)} The following transformation occur on the logical_operator: OCL operators and, or and xor are transformed into Java &&, || and ^.

78

Carleton University TR SCE-04-03

Version 1, February 2004

equivalence_expression ::= relational_expression {lCode=relational_expression.code ; lType= relational_expression.type} equivalence_operator {op=equivalence_operator.code} equivalence_expression_tail {rCode=equivalence_expression_tail.code ; rType=equivalence_expression_tail.type} ; { if lType.isPrimitiveType() and rType.isPrimitiveType() then ex="(" + lCode + "==" + rCode elseif lType.isReferenceType() and rType.isReferenceType() then ex=lCode + ".equals(" + rCode + ")" elseif lType.isReferenceType() then ex=lCode + "." + lType.name + "Value() == " + rCode else ex=lCode + "==" + rCode + "." + rType.name + "Value()" } { if op == "=" then return ex else return "!(" + ex + ")" } The OCL operator = (equals between reference or primitive type values) translate differently depending on the Java type. If we have two primitive types, we can use ==. If we have two reference types we use operation equals(). If only one of the two is a reference type we get its integer value with operation intValue(), longValue(), … depending on the exact type the non-primitive value. If the OCL operator is used the result of the equivalence is negated to obtain the result.

relational_expression ::= additive_expression {left=additive_expression.code} relational_expression_tail? {right=relational_expression_tail.code} ; {return left + right} Only a concatenation of additive_expression.code and relational_expression_tail.code is necessary. If left a number of reference type then it’s primitive value is extracted from it via the typeValue() method.

relational_expression_tail ::= relational_operator {op=relational_operator.code} additive_expression {right=additive_expression.code} ; {return op + right} Relation operators are identical in OCL and Java: . If right is number of reference type then it’s primitive value is extracted from it via the typeValue() method.

79

Carleton University TR SCE-04-03

Version 1, February 2004

additive_expression ::= multiplicative_expression {left=multiplicative_expression.code ; type=multiplicative_expression.type} add_operator (op=add_operator.code) multiplicative_expression {right=multiplicative_expression.code} ; {if op=="-" and type.isCollectionType() then return "((Set)" + left + ".clone()).removeAll(" + right + ")" else return left + op + right } A special situation occurs when operator – is used on a collection, in which case we have to remove element(s) from the collection. Otherwise, we just perform whatever operator add_operator refers to (either + or -). OCL + and – operators map directly to Java + and -.

multiplicative_expression ::= unary_expression {left=unary_expression.code} multiplicative_operator_tail* {right=multiplicative_operator_tail.code} {return left + right}

multiplicative_expression_tail ::= multiply_operator {op=multiply_operator.code} unary_expression {right=unary_expression.code} ; {return op + right} OCL operators * and / map directly to Java * and /.

unary_expression ::= unary_operator {op=unary_operator.code} postfix_expression {ex=postfix_expression.code} ; {if op == "not" then return "!(" + op + ex + ")" else return op + ex} OCL unary operators "–" and "not" match to Java’s "–" and "!".

unary_expression ::= postfix_expression {return postfix_expression.code} ;

80

Carleton University TR SCE-04-03

Version 1, February 2004

postfix_expression ::= primary_expression {ex=primary_expression.code} (postfix_expression_tail {ex=ex + postfix_expression_tail.code} )* ; {return ex} The code for every postfix_expression_tail (if any) is added to the code for primary_expression.

postfix_expression_tail ::= postfix_expression_tail_begin {ex=postfix_expression_tail_begin.code} feature_call {ex=ex+ feature_call.code)} ; {return ex}

postfix_expression_tail_begin ::= "." | "->" ; {return "."} This is only used to distinguish between the invocation of an OCL collection operation vs. the invocation of a non-OCL collection, essentially resolves ambiguity as to which operation the user wishes to invoke.

literal ::= string_lit ; {return stringLiteral.replaceAll("'", "\"")} ; literal is used in primary_expression. The OCL string is encapsulated between two ' characters. These characters are stripped and replaced with the java string encapsulation character: ".

literal ::= real {lit=real.code} | int {lit=int.code} | bool {lit=bool.code} ; {return lit} literal is used in primary_expression. Numbers are not transformed.

81

Carleton University TR SCE-04-03

Version 1, February 2004

literal_collection ::= collection_kind {collType=collection_kind.type collectionName = generateUniqueName() if colType == "Set" then actualType="java.util.HashSet" elseif (oclType == "Bag”) or (oclType == "Sequence”) then actualType="java.util.ArrayList" Prepare.add(collType + " collectionName = new " + actualType + "();"} "(" expression_list_or_range? ")" ; {return expression_list_or_range.code} {return collectionName} generateUniqueName(), as the name implies, generates a unique name by which this collection can be referred. The collectionName and collType is used by further production rules.

expression_list_or_range ::= expression {Prepare.add(collectionName + ".add(" + expression.code + ")")} ( (expression_list_tail {Prepare.add(collectionName + ".add(" + expression_list_tail.code + ")")} )+ )? ; collectionName is reused from production rule for literal_collection. The literal_collection, along with this expression_list_or_range (i.e., with an expression_list_tail), specifies a collection with specific elements. The first element being expression, and the following being specified by expression_list_tail.

expression_list_or_range ::= expression {ex1=expression.code} ( ".." expression {ex2=expression.code} )? ; {Prepare.add(collectionName + ".add(OclCollection.literalRange("+ collType + ", "+ ex1 + ", " + ex2 + "))")} collectionName is reused from production rule for literal_collection. This literal_collection, along with this expression_list_or_range (i.e., with a ".." expression), specifies a collection with a range (a possible number of elements)

expression_list_tail ::= comma expression ; {return expression.code}

82

Carleton University TR SCE-04-03

Version 1, February 2004

primary_expression ::= literal_collection {return literal_collection.code} ; primary_expression ::= literal {return literal.code} ; primary_expression ::= "(" expression ")" {return "(" + expression.code + ")"} ; primary_expression ::= if_expression {return if_expression.code} ; No comment: those transformations are trivial.

primary_expression ::= path_name {return path_name.code} ; The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; In this case, path_name points to a field. In the case of a the field being static (and therefore it’s location is specified using packages), every OCL package resolution "::" in path_name is changed to the Java package resolution "." in the corresponding code.

primary_expression ::= path_name {pCode=path_name.code} feature_call_parameters {par=feature_call_parameters.code} ; {return pCode + par} The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; In this case, path_name points to an operation with a potentially empty parameter list.

primary_expression ::= path_name {pCode=path_name.code} qualifiers {qual=qualifiers.code} ; {mapValueType = getValueType() result = "((" + mapValueType + ")" + pCode + ".get(" + qual + "))"} {return result} The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; In this case, path_name points to an attribute of type java.util.Map.

83

Carleton University TR SCE-04-03

Version 1, February 2004

primary_expression ::= path_name {pType=path_name.type ; pCode=path_name.code} time_expression ; {preVarName = genereateUniqueName() if pType.isCollectionType() then PreCode.add(pType + preVarName + " = OclCollection.cloneCollection(" + pCode + ");"); else if pType.isReferenceType() then PreCode.add(pType + preVarName + " = (" + pType + ")" + pCode + ".clone();") else PreCode.add(pType + preVarName + " = " + pCode + ";")} {return preVarName} The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; In this case path_name points to a field possibly a collection. This preparation code is executed before the constrained method executes. Newly created (pre) values are uniquely named. Note: The entire collection is not duplicated in the case of OCL collection operations: size():Integer, isEmpty():Boolean, notEmpty():Boolean, or sum():T. Only the result of the operation is stored in the preparation code, though this is not shown in the rule.

primary_expression ::= path_name {pType=path_name.type ; pCode=path_name.code} time_expression feature_call_parameters {featureCode=feature_call_parameters.code} ; {preVarName = genereateUniqueName() PreCode.add(pType + preVarName + " = " + pCode + ";")} {return preVarName} The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; This preparation code is executed before the constrained method executes.

84

Carleton University TR SCE-04-03

Version 1, February 2004

primary_expression ::= path_name {pCode=path_name.code} qualifiers {qual=qualifiers.code} ; {preVarName = genereateUniqueName() mapValueType = getValueType() PreCode.add(mapValueType + preVarName = "(" + mapValueType + ")(" + pCode + ".get(" + qual + ")).clone();")} {return preVarName} The whole production rule is: primary_expression = path_name time_expression? qualifiers? feature_call_parameters? ; In this case, path_name points to an attribute of type java.util.Map.

feature_call_parameters ::= "(" (expression {ex=expression.code} ) ? (expression {ex=ex + ", " + expression.code )* ")" ; {return "(" + ex + ")"} This is a parameter list for an operation. Each parameter is added in sequence.

qualifiers ::= "(" expression {ex=expression.code} ")" ; {return ex} Currently the qualifier list can only have item. If the item in the list is of primitive type then it is wrapped with the appropriate reference type (not shown in the rule).

feature_call ::= path_name time_expression? qualifiers? feature_call_parameters? ; See the production rules for primary_expression, both are handled in the same way.

85

Carleton University TR SCE-04-03

Appendix C

Version 1, February 2004

From OCL collections to Java collections

Figure 14 provides an excerpt of the Java collection hierarchy. In particular, it shows that interfaces Set and List are implemented by classes AbstractSet and AbstractList and their child classes, respectively. The following four tables show the mapping from operations common to all OCL collection types (Table 21), Set operations (Table 22), Sequence operations (Table 23), and Bag operations (no table as Bags are implemented as Lists), to Java classes and operations, respectively. Object

Collection

List

Map

Set

AbstractMap AbstractCollection HashMap

TreeMap

WeakHashMap

AbstractSet LinkedHashMap

AbstractList HashSet AbstractSequentialList

ArrayList

IdentityHashMap

TreeSet

Vector LinkedHashSet

LinkedList

Stack

Figure 14 The Java collection hierarchy (excerpt)

86

Carleton University TR SCE-04-03

Operations on OCL type Collection count(o:Object):Integer size():Integer includes(o:Object):Boolean includesAll(o:Object):Boolean isEmpty():Boolean notEmpty():Boolean sum():Real or Integer

exists(ex:OclExpression):Boolean

forAll(ex:OclExpression): Boolean

Version 1, February 2004

Java interface and operation. OclCollection::count(c:Collection, o:Object):Integer Collection::size():Integer Collection::contains(o:Object):Boolean Collection::contrainsAll(o:Object):Boolean Collection::isEmpty():Boolean not Collection::isEmpty():Boolean OclCollection::sum(c:Collection, c:Class):Number private boolean exists(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (OclExprInJava) { return true; } } return false; } private boolean forAll(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (!(OclExprInJava)) { return false; } } return true; }

Type2 acc ; iterate(elem: Type1; acc: Type2 = | ex2: expression-with-elem-andacc)

private Type2 iterate(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ Type1 elem = (Type1) i.next(); acc = (ex2); } return acc; }

Table 21 Operations common to OCL collections and mapping to Java

87

Carleton University TR SCE-04-03

Version 1, February 2004

Operations on OCL type Set

Java class and operation.

union(s:Set):Set

Set::addAll(c:Collection):Set addAll on a newly created List (representing the returned Bag) Set::equals(o:Object):Boolean Set::retainAll(c:Collection):Set Set::retainAll(c:Collection):Set Set::removeAll(c:Collection):Set Set::add(o:Object):Set Set::remove(o:Object):Set This set minus the intersection

union(b:Bag):Bag equals(s:Set):Boolean intersection(s:Set):Set intersection(b:Bag):Set minus(s:Set):Set including(o:Object):Set excluding(o:Object):Set symmetricDifference(s:Set):Set

select(ex:OclExpression):Set

reject(ex:OclExpression):Set

collect(ex:OclExpression):Bag

asBag():Bag asSequence():Sequence

Set result = new HashSet(); private Set select(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (OclExprInJava) { result.add(e); } } return result; } Set result = new HashSet(); private Set rejelect(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (!(OclExprInJava)) { result.add(e); } } return result; } Set result = new HashSet(); private Set collect(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); result.add(OclExprInJava); } return result; }

List::addAll(c:Collection) on a new List

Table 22 Operations on OCL type Set and mapping to Java

88

Carleton University TR SCE-04-03

Operations on OCL type Sequence/Bag equals(s:Sequence):Boolean union(s:Sequence):Sequence append(o:Object):Sequence prepend(o:Object):Sequence subSequence(i1:Integer, i2:Integer):Sequence at(i:Integer):Object first() last() including(o:Object):Sequence excluding(o:Object):Sequence

select(ex:OclExpression): Sequence

reject(ex:OclExpression): Sequence

collect(ex:OclExpression): Sequence

asBag():Bag asSet():Set

Version 1, February 2004

Java class and operation. List::equals(l:List):boolean List::addAll(c:Collection):boolean List::add(o:Object) List::add(i:int, o:Object) with i being 0 List::subList(i1:int, i2:int):List List::get(i:int):Object List::get(i:int):Object with i being 0 List::get(i:int):Object with i equal to (List::size() – 1) See append loop calling List::remove(o:object) until remove returns false List result = new ArrayList(); private Set select(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (OclExprInJava) { result.add(e); } } return result; } List result = new ArrayList(); private Set rejelect(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); if (!(OclExprInJava)) { result.add(e); } } return result; } List result = new ArrayList(); private Set collect(Collection c) { for (Iterator i = c.iterator(); c.hasNext();){ ElementType e = (ElementType) i.next(); result.add(OclExprInJava); } return result; }

List::addAll(c:Collection) on a new List Set::addAll(c:Collection) on a new Set

Table 23 Operations on OCL type Sequence/Bag and mapping to Java

89

Carleton University TR SCE-04-03

Appendix D

Version 1, February 2004

Optimization of simple OCL collection operations

This appendix shows how OCL collection operations that do not have a parameter of type OclExpression are optimized. First of all, recall that collection operations collection>size():

Integer,

collection->isEmpty():

Boolean,

and

collection-

>notEmpty(): Boolean are not optimized as it would be more expensive to optimize

them than simply execute them. Following are four tables for operations common to all OCL collection types, operations for Set, operations for Bag and operation for Sequence, respectively. Each time, the table provides the data structure used in the aspect to store intermediate results (e.g., an attribute), a pseudo code detailing the check performed upon collection modification to update that data structure, and the constraint assertion checking the OCL operation.

OCL Collection Operation includes(o:Object) : Boolean

Attribute added to the aspect the monitor the result of the operation. numOfOccurrences : Integer

count(obj): Integer

Same as above.

sum(): T

sum:T

Check Upon Collection Change

Constraint Assertion

If the added/removed element is o then If this is an addition then numOfOccurrences+ + else numOfOccurrences--

numOfOccurences > 0

If added element then sum+=element else sum-=element

Uses numOfOccurrence s Uses sum

includesAll(coll): Boolean

90

includesAll

If added element o to c2, c is checked to see if it contains o. If yes, includesAll=true else includesAll=false and o is added to missing. If added element o to c, includesAll is checked. If false, missing is checked to see if it includes o. If yes, o is removed from missing and if missing.size()==0 then includesAll=true

missing:Set includesAll:boolean

includesAll(c2: Collection):Boolean

Uses sum

If added element then sum+=element else sum-=element

Sum():T

numOfOccurences>0

sum: T

Same thing

count(o:Object):Integer

If the added/removed element is o then If this is an addition then numOfOccurrences++ else numOfOccurrences--

Constraint assertion

Uses numOfOccurrences

numOfOccurrences: int

includes(o:Object):Boolean

Check upon collection change

Same thing

Attribute added to the aspect the monitor the result of the operation

OCL collection operation

Carleton University TR SCE-04-03 Version 1, February 2004

Common operations

91

Carleton University TR SCE-04-03

Appendix E

Version 1, February 2004

Optimization of complicated OCL collection

operations In this appendix, three examples are provides. Each time, we provide the attributes that are added to the aspect to monitor the collection, and how that information is used to check the constraint. Each time, we assume the constraint to be checked is in a class invariant, that has to be checked after every public method of the (context) class. Last, each time we assume that the collection on which the OCL operation is applied is a Set.

E.1 Collection operation: c->exists(ex:OclExpression):Boolean The following attributes are added to the aspect the monitor the result of the operation: existing:Set (used to store the elements in collection c that satisfy OCL expression ex), changed:Set (the set of elements in existing that undergo a change), added:Set (set of

elements added to collection c), removed:Set (set of elements removed from collection c). Before any public method (in the context class) starts executing, we do the following: We compute the elements in c that satisfy expression ex (existing contains the elements in c that satisfy ex), we set the other three attributes to empty sets. Then, during the execution of the method, for any element e in collection existing: Upon a change to e’s state that is relevant to ex, ex needs to be re-evaluated on e to see if it should still reside in existing. For example, in the constraint c->exists(p:Person | p.age > 18). The element in the collection is of type Person and a state change that is

relevant to this constraint is an element that’s age attribute is being changed. Each such e is added to changed. For every element that is added to c it is also added to added. Also, for every element that is removed from c it is added to removed. This does not apply to elements that are 92

Carleton University TR SCE-04-03

Version 1, February 2004

added (resp. removed) but then removed (resp. added) within the execution of the same public method in the context class. After the public method (in the context class) finishes executing, we do the following. First, the elements in removed are removed from added, changed and existing. Then, elements in added satisfying ex are added to existing, and elements in changed have ex re-evaluated on them and elements that no-longer satisfy ex are removed from existing.

Last, when the constraint has to be checked, the assertion is: existing.size() > 0

E.2 Collection operation: c->forAll(ex:OclExpression):Boolean The following attributes are added to the aspect the monitor the result of the operation: changed:Set (the elements in c that undergo a change), added:Set (the elements added

to c), removed:Set (the elements removed from c), result:boolean (used in the assertion checking the constraint). Before any public method (in the context class) starts executing, we do the following: changed, added, and removed are initialized to empty sets, and result is set to true.

After the public method (in the context class) finishes executing, we do the following. First, elements in removed are removed from added and changed. Then, if any element in added or changed does not satisfy ex then result is set to false. Last, when the constraint has to be checked, the assertion is: result

E.3 Collection operation: c->isUnique(ex:OclExpression):Boolean The following attributes are added to the aspect to monitor the result of the operation: map:HashMap (elements of c are the keys and the corresponding values are the results of

the evaluation of ex on the elements), values:Set (the set of all the values stored in map), changed:Set (the elements in the collection that undergo a change), added:Set

(the elements added to the collection), removed:Set (the elements removed from the 93

Carleton University TR SCE-04-03

Version 1, February 2004

collection), result:boolean (used in the assertion checking the constraint). When an instance of the context class is created, map and values are initialized using the initial contents of collection c. Before any public method (in the context class) starts executing, we do the following: changed, added, and removed, are initialized to empty sets; result is set to true.

After the public method (in the context class) finishes executing: elements in removed are removed from added and changed. Then, for every element e in removed and changed we perform: map.remove(e). The corresponding value is also removed from values. And, for every element e in added and changed, ex is computed on e and placed in e_ex. If values->includes(e_ex) then result is set to false. Else, map.put(e, e_ex) and values.add(e_ex).

Last, when the constraint has to be checked, the assertion is: result

94

Carleton University TR SCE-04-03

Appendix F

Version 1, February 2004

Constraints in the Case Study

The following constraints where used in the case study: context Colour::getSilver():Colour post: result = self.SILVER context Colour::getGold():Colour post: result = self.GOLD context Customer inv: self.name '' context Customer inv title_gender: self.title=(if self.isMale=true then 'Mr' else 'Ms' endif) context Customer inv: self.age >= 18 context Customer inv: self.program->size() = self.cards->select(c:CustomerCard|c.valid = true)->size() context Customer::age():java::lang::Integer post: result=self.age context Customer::addLoyaltyProgram(lp:LoyaltyProgram, membership:Membership, card:CustomerCard) post: self.program->includes(lp) and self.membership->includes(membership) and self.cards->includes(card) context Customer::getName():java::lang::String post: result = self.name context Customer::getTitle():java::lang::String post: result = self.title context Customer::getCards():java::util::List post: result = self.cards context Customer::getMemberships():java::util::List post: result = self.membership context CustomerCard inv: self.validFrom.before(self.validThru) context CustomerCard inv: self.colour = Colour.SILVER or self.colour = Colour.GOLD context CustomerCard inv: self.printedName = self.owner.title.concat(self.owner.name) context CustomerCard::setMembership(membership:Membership) post: self.membership = membership context CustomerCard::addTransaction(t:Transaction) post: self.transactions->includes(t) context LoyaltyAccount inv: self.points >= 0 context LoyaltyAccount::earn(i:int) post: self.points = self.points@pre + i context LoyaltyAccount::burn(i:int) pre: i select(m:Membership|m.customer = c)-> forAll(m:Membership|m.loyaltyAccount->notEmpty() and m.loyaltyAccount.points = 0 and m.loyaltyAccount.transactions->isEmpty()) context LoyaltyProgram inv: self.serviceLevel->size() = 2 and self.partners->size() > 0 and self.partners->collect(pp:ProgramPartner| pp.deliveredServices)->forAll(s:Service|s.pointsEarned=0 and s.pointsBurned=0) implies self.membership->collect(m:Membership|m.loyaltyAccount)->isEmpty() context LoyaltyProgram inv: self.customer->forAll(c1:Customer, c2:Customer | c1c2 implies c1.namec2.name) self.customer->isUnique(c:Customer|c.name) and self.customer->forAll(c:Customer|c.age >= 18) context LoyaltyProgram::enroll(c:Customer) pre: not self.customer->includes(c) context LoyaltyProgram::enroll(c:Customer) post: self.customer@pre->including(c) = self.customer and self.membership->select(m:Membership| m.customer = c)->forAll(m:Membership|m.loyaltyAccount->notEmpty() and m.loyaltyAccount.points = 0 and m.loyaltyAccount.transactions->isEmpty()) context Membership inv: self.actualLevel.name = 'Gold' implies self.card.colour = Colour.GOLD context Membership inv: self.actualLevel.name = 'Silver' implies self.card.colour = Colour.SILVER context Membership inv: self.card.owner = self.customer context Membership inv: self.loyaltyAccount.points>=0 or self.loyaltyAccount->isEmpty() context Membership inv actualLevel: self.customer.program->includes(self.program) context Membership inv membership_back: self.customer.cards->collect(cc:CustomerCard|cc.membership)->includes(self)

95

Carleton University TR SCE-04-03

Version 1, February 2004

context ProgramPartner inv: self.name '' context ProgramPartner inv: self.numberOfCustomers >= 0 context ProgramPartner::addLoyaltyProgram(lp:LoyaltyProgram) pre: self.loyaltyProgram->excludes(lp) context ProgramPartner::addLoyaltyProgram(lp:LoyaltyProgram) post: self.loyaltyProgram->includes(lp) context ProgramPartner::newCustomer() post: self.numberOfCustomers = self.loyaltyProgram->collect(lp:LoyaltyProgram| lp.customer)->size() context Service inv: self.pointsEarned>0 implies self.pointsBurned=0 context Service inv: self.pointsBurned>0 implies self.pointsEarned=0 context Service::setProgram(lp:LoyaltyProgram) post: self.lp = lp context Service::setProgramPartner(programPartner:ProgramPartner) post: self.programPartner = programPartner context ServiceLevel inv: self.name '' context ServiceLevel::addService(s:Service) post: self.availableServices->includes(s) context ServiceLevel::addMembership(m:Membership) post: self.membership->includes(m) context ServiceLevel::setLoyaltyProgram(loyaltyProgram:LoyaltyProgram) post: self.loyaltyProgram = loyaltyProgram

96

Carleton University TR SCE-04-03

Appendix G

Version 1, February 2004

Code Exposing Problems with DOT

This appendix show small programs that expose some problems with DOT. In this program the invariant is invalidated yet this is not detected. package inv1; /** * @invariant bx_abv_0: b.x > 0 */ public class A { public B b = new B(1); public void changeBx(int x) { b.x = x; } public static void main(String[] args) { A a = new A(); // This invalidates the invariant, but the falsification of the // invariant is not detected. a.changeBx(0); } } package inv1; public class B { public int x; public B(int x) { this.x = x; } }

The following invariant is not invalidated, yet DOT incorrectly reports that it is. package inv2; import java.awt.Point; /** * @invariant points_equal: p1 = p2 */ public class A { Point p1 = new Point(0, 0); Point p2 = p1; public void changeP2() { p2 = new Point(0, 0); } public static void main(String[] args) { A a = new A(); // This results in a false-positive, DOT deems considers // the points unequal even though the objects are equal // according to the equals() method. a.changeP2(); } }

97

Carleton University TR SCE-04-03

Version 1, February 2004

The following is an example of a runtime exception being thrown after the program is instrumented with DOT. package inv3; import java.awt.Point; import java.util.ArrayList; /** * @invariant collInv: c->forAll(p | p.x > 0) */ public class A { /** * @element-type java.awt.Point */ ArrayList c = new ArrayList(); public A() { c.add(new Point(1, 1)); }

}

public static void main(String[] args) { A a = new A(); }

When trying to execute the program the following error is reported: java.lang.reflect.InvocationTargetException InvocationTargetException invoking >someInv< on >inv3.A@1bab50a< at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:324) at tudresden.ocl.injection.ocl.lib.Invariant.invoke(Invariant.java:106) at tudresden.ocl.injection.ocl.lib.Invariant.checkVacantInvariants(Invariant.java:144) at inv3.A.(A.java:29) at inv3.A.main(A.java:20) Caused by: java.lang.RuntimeException: error accessing feature x_oclobservinginvariants812374 on object java.awt.Point[x=1,y=1] at tudresden.ocl.injection.ocl.lib.Invariant.addObserver(Invariant.java:68) at tudresden.ocl.injection.ocl.lib.Invariant.onField(Invariant.java:82) at tudresden.ocl.lib.OclAnyImpl.getFeatureQualified(OclAnyImpl.java:135) at tudresden.ocl.lib.OclAnyImpl.getFeature(OclAnyImpl.java:95) at inv3.A$1.evaluate(A.java:60) at tudresden.ocl.lib.OclCollection.forAll(OclCollection.java:179) at inv3.A.zzzCheckOclInvariantMethod812374_someInv(A.java:66) ... 8 more java.lang.RuntimeException: java.lang.RuntimeException: error accessing feature x_oclobservinginvariants812374 on object java.awt.Point[x=1,y=1] at tudresden.ocl.injection.ocl.lib.Invariant.invoke(Invariant.java:118) at tudresden.ocl.injection.ocl.lib.Invariant.checkVacantInvariants(Invariant.java:144) at inv3.A.(A.java:29) at inv3.A.main(A.java:20) Exception in thread "main"

The following program shows the lack of support access of static attributes on an associated class.

98

Carleton University TR SCE-04-03

Version 1, February 2004

package stat; /** * @invariant colour: col = Colour.RED */ public class A { static Colour col = Colour.RED; public static void main(String[] args) { A a = new A(); } } package stat; public class Colour { String colour; public Colour(String colour) { this.colour = colour; } public static Colour RED = new Colour("red"); public static Colour BLUE = new Colour("blue"); }

The command used to instrument the program was: java -cp dresden-ocl-injector.jar;. tudresden.ocl.injection.ocl.Main -m -r stat stat/*.java

The following stack trace is dumped when trying to instrument the above program.

99

Carleton University TR SCE-04-03

Version 1, February 2004

tudresden.ocl.check.OclTypeException: ReflectionFacade could not find class REDin packages stat at tudresden.ocl.check.types.ReflectionFacade.getClassifier(ReflectionFacade.java:160) at tudresden.ocl.check.types.DefaultTypeFactory.get(DefaultTypeFactory.java:176) at tudresden.ocl.check.TypeChecker.inANonCollectionTypeName(TypeChecker.java:891) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseANonCollectionTypeName(DepthFirstAdapter.java:1804 ) at tudresden.ocl.parser.node.ANonCollectionTypeName.apply(ANonCollectionTypeName.java:30) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseATypeNamePathNameBegin(DepthFirstAdapter.java:1966 ) at tudresden.ocl.parser.node.ATypeNamePathNameBegin.apply(ATypeNamePathNameBegin.java:30) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAPathName(DepthFirstAdapter.java:1939) at tudresden.ocl.parser.node.APathName.apply(APathName.java:56) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAFeatureCall(DepthFirstAdapter.java:1565) at tudresden.ocl.parser.node.AFeatureCall.apply(AFeatureCall.java:45) at tudresden.ocl.check.TypeChecker.caseAPostfixExpression(TypeChecker.java:365) at tudresden.ocl.parser.node.APostfixExpression.apply(APostfixExpression.java:56) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAPostfixUnaryExpression(DepthFirstAdapter.java:725 ) at tudresden.ocl.parser.node.APostfixUnaryExpression.apply(APostfixUnaryExpression.java:30) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAMultiplicativeExpression(DepthFirstAdapter.java:6 50) at tudresden.ocl.parser.node.AMultiplicativeExpression.apply(AMultiplicativeExpression.java:56) at tudresden.ocl.check.TypeChecker.caseAAdditiveExpression(TypeChecker.java:254) at tudresden.ocl.parser.node.AAdditiveExpression.apply(AAdditiveExpression.java:56) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseARelationalExpressionTail(DepthFirstAdapter.java:5 79) at tudresden.ocl.parser.node.ARelationalExpressionTail.apply(ARelationalExpressionTail.java:35) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseARelationalExpression(DepthFirstAdapter.java:555) at tudresden.ocl.parser.node.ARelationalExpression.apply(ARelationalExpression.java:35) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseALogicalExpressio n(DepthFirstAdapter.java:500) at tudresden.ocl.parser.node.ALogicalExpression.apply(ALogicalExpression.java:56) at tudresden.ocl.check.TypeChecker.caseAExpression(TypeChecker.java:186) at tudresden.ocl.parser.node.AExpression.apply(AExpression.java:56) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAConstraintBody(DepthFirstAdapter.java:90) at tudresden.ocl.parser.node.AConstraintBody.apply(AConstraintBody.java:45) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseAConstraint(DepthFirstAdapter.java:57) at tudresden.ocl.parser.node.AConstraint.apply(AConstraint.java:56) at tudresden.ocl.parser.analysis.DepthFirstAdapter.caseStart(DepthFirstAdapter.java:31) at tudresden.ocl.parser.node.Start.apply(Start.java:33) at tudresden.ocl.OclTree.checkTypeQueryable(OclTree.java:335) at tudresden.ocl.OclTree.assureTypes(OclTree.java:141) at tudresden.ocl.injection.ocl.OclConfig.makeConstraint(OclConfig.java:92) at tudresden.ocl.injection.ocl.OclConfig.makeConstraint(OclConfig.java:75) at tudresden.ocl.injection.ocl.OclInstrumentor.processConstraint(OclInstrumentor.java:92) at tudresden.ocl.injection.ocl.OclInstrumentor.onFileDocComment(OclInstrumentor.java:82) at tudresden.ocl.injection.Instrumentor.onClass(Instrumentor.java:96) at tudresden.ocl.injection.Injector.parseClass(Injector.java:680) at tudresden.ocl.injection.Injector.parseFeature(Injector.java:440) at tudresden.ocl.injection.Injector.parseFile(Injector.java:794) at tudresden.ocl.injection.Main.inject(Main.java:53) at tudresden.ocl.injection.Main.inject(Main.java:94) at tudresden.ocl.injection.Main.run(Main.java:233) at tudresden.ocl.injection.ocl.Main.main(Main.java:79) Exception in thread "main" tudresden.ocl.check.OclTypeException: stat\A.java: ReflectionFacade could not find class RED in packages stat at tudresden.ocl.injection.Main.inject(Main.java:70) at tudresden.ocl.injection.Main.inject(Main.java:94) at tudresden.ocl.injection.Main.run(Main.java:233) at tudresden.ocl.injection.ocl.Main.main(Main.java:79)

100