AspectJ Tutorial Exercises

Organization

The exercises work with a figure editor together with JUnit test cases. They progress, as most users do in their adoption of AspectJ, from non-functional, development-only aspects to aspects which augment a deployed program with crosscutting features.

We have made available a package that includes the tests, the base code, JUnit, and a distribution of AspectJ. All it needs is information about where Java lives (so set your JAVA_HOME environment variable). It assumes that you unzip it in c:\ (on Windows) or in your home directory (on Linux): If you put it somewhere else, edit setpaths or setpaths.bat, as appropriate. Once all this is done, run setpaths.bat or source setpaths to export some other needed environment variables.

All the files in the program are listed in base.lst, including test cases and an empty answer aspect, answers/Answer.java. Therefore, if you write your answers there, all you need to do is compile base.lst, either in an IDE or with

$ ajc -Xlint -argfile base.lst

Before you move onto another exercise, though, make sure to copy your answer into a different file so we can discuss the answers together:

> copy answers/Answer.java answers/2a.java  (Windows)
$ cp answers/Answer.java answers/2a.java    (Linux)

If you want to put your answer in a different file, say, answers/Answer2a.java, you can compile with

$ ajc -Xlint -argfile base.lst answers/Answer2a.java

In any case, after building the system, you should invoke Java on the compiled test class. On the command-line, this this would be

$ java tests.Test2a

(For these exercises, when we give examples of execution we will show the command-line use, but of course if you are using JBuilder, Forte/NetBeans, Emacs, or Eclipse, use the appropriate compile and execute tools.)

The default test, tests.Test, performs some rudimentary tests on figure elements, and so is a useful test to run periodically. Looking at the JUnit tests for each exercise may also be helpful.

Again, ae will be looking at some solutions and having discussion, which is much more difficult without incremental solutions. So when you go from one exercise to the next, make sure to save your work in a file and go on to work in a different file, even if you plan to duplicate some code.


1. Static Invariants

a. Catch old tracing

The way that we are all taught to print "hello world" from Java is to use System.out.println(), so that is what we typically use for one-off debugging traces. It's a common mistake to leave these in your system longer than is necessary. Type in the aspect below to forces an error at compile time if this mistake is made.

When you use this on the given system, you'll find one incorrect trace in SlothfulPoint.

$ ajc -argfile base.lst
./figures/SlothfulPoint.java:29:9: illegal access to System.out
        System.out.println("Slothful moving");
               ^
1 errors

Remove the illegal tracing call.

Make sure your program still passes the JUnit test tests.Test (which it should also pass at the beginning of all exercises) before continuing.

$ java tests.Test
....
Time: 0.076

OK (4 tests)

Answer:

package answers;

import figures.*;

aspect Answer1a {
    declare error
        : get(java.io.PrintStream System.out) && within(figures..*)
        : "illegal access to System.out";
}

Note that this answer does not say that the call to the println() method is incorrect, rather, that the field get of the out field is illegal. This will also catch those users who bind System.out to a static field to save typing.

b. Mandate setters

Problem: Add warnings for assignments outside of setter methods.

Tools: set, withincode, signature void set*(..)

One common coding convention is that no private field should be set outside of setter methods. Write an aspect to warn at compile time when such an illegal assignment expression exists.

This is going to look like

aspect A {
    declare warning: <pointcut here> : "bad field set";
}

where the pointcut picks out join points of private field sets outside of setter methods. "Outside", here, means that the code for the assignment is outside the text of the setter (a setter is a method that looks like "void set*(..)"), so check out the quick reference for set and withincode primitive pointcuts. )

Make sure your program still passes the JUnit test tests.Test before continuing, and that you see all of the following warning messages. Make sure you get 11 warnings from this. Wait to fix them until the next exercise.

c. Refine setters mandate

Problem: Allow assignmnents inside of constructors.

Tools: signature new(..)

Look at some of the warnings. Notice that a lot of them are from within constructors. Actually, the common coding convention is that no private field should be set outside of setter methods or constructors. Modify your answer (in a new file) to signal an actual error at compile time (rather than just a warning) when such an illegal assignment expression exists.

You'll want to add another withincode primitive pointcut to deal with the constructors.

After you specify your pointcut correctly, you'll still find that the convention is violated twice in the figures package. You should see the following two errors:

.\figures\Point.java:28:9: bad field set
        _x += dx;
        ^
.\figures\Point.java:29:9: bad field set
        _y += dy;
        ^
2 errors

Rewrite these two occurrences so as not to violate the convention. Make sure your program still passes the JUnit test tests.Test before continuing.

d. Congratulatoins

You've taken your first steps. At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them.


2. Dynamic invariants

a. Check a simple precondition

Problem: Pass tests.Test2a.

Tools: args, before

THERE IS AN ANSWER BELOW. LOOK AT IT. TYPE IT IN.

Write an aspect to throw an IllegalArgumentException whenever an attempt is made to set one of Point's int fields to a value that is less than zero.

This should make the test case of tests.Test2a pass, which wouldn't without your aspect. So before compiling in the aspect,

$ ajc -Xlint -argfile base.lst

$ java tests.Test2a
.F.F.F....
Time: 0.099
There were 3 failures:
1) testTooSmall(tests.Test2a)junit.framework.AssertionFailedError: should have thrown IllegalArgumentException
2) testTooBig(tests.Test2a)junit.framework.AssertionFailedError: should have thrown IllegalArgumentException
3) testMove(tests.Test2a)junit.framework.AssertionFailedError: should have thrown IllegalArgumentException

FAILURES!!!
Tests run: 7,  Failures: 3,  Errors: 0

But after compiling in the aspect...

$ ajc -Xlint -argfile base.lst

$ java tests.Test2a
.......
Time: 0.097

OK (7 tests)

Answer:

package answers;

import figures.*;

aspect Answer2a {
    before(int newValue): set(int Point.*) && args(newValue) {
        if (newValue < FigureElement.MIN_VALUE) {
            throw new IllegalArgumentException("too small");
        } else if (newValue > FigureElement.MAX_VALUE) {
            throw new IllegalArgumentException("too large");
        }
    }
}

b. Check another precondition

Group is a FigureElement class that encapsulates groups of other figure elements. As such, only actual figure element objects should be added to Group objects. Write an aspect to throw an IllegalArgumentException whenever Group.add() is called with a null value. If you look at the source code for tests/Test2b.java, you'll see an example of the desired behavior, i.e.

    public void testNull() {
        try {
	    g.add(null);
            fail("should have thrown IllegalArgumentException");
        } catch (IllegalArgumentException ea) {
        }
    }

For each of these exercises, you'll find that the corresponding test case provides that most concrete example of the desired behavior for your aspect. Please avail yourself of this resource.

With this aspect in place, your code should pass tests.Test2b.

c. Check yet another precondition

Another constraint on a well-formed group is that it should not contain itself as a member (though it may contain other groups). Write an aspect to throw an IllegalArgumentException whenever an attempt is made to call Group.add() on a null value, or on the group itself.

You will want to use a target pointcut to expose the Group object that is the target of the add call.

With this aspect in place, your code should pass tests.Test2c.

d. Check a simple postcondition

One of the simplest postconditions to check is that a setter actually sets its value. Write an aspect that throws a java.lang.RuntimeException if, after calling setX() on SlothfulPoint objects, getX() doesn't return the new value.

You'll want to use an args pointcut to expose the argument to setX() and a target pointcut to expose the SlothfulPoint object itself (so you can later call getX() on it).

An interesting question to think about for discussion is whether this postcondition should apply when getX() throws an exception, or when it returns normally, or both?

With this aspect in place, your code should pass tests.Test2d.

e. Check invariant

There is a method on the Box class, void checkBoxness(), that checks whether the four points making up a box are correctly positioned relative to each other (i.e., they form a rectangle). Write an aspect that will make sure that after every time the void move(int, int) method on Box is called, that you also call Box.checkBoxness() to ensure that the move didn't break this invariant.

With this aspect in place, your code should pass tests.Test2e.

f. Refine your invariant

move is not the only interesting method on Box. It may be that a box gets malformed between calls to move. So instead of checking boxness only after the move method of Box, check after the call to every one of Box's public methods.

When testing this aspect, you may find yourself facing a StackOverflowException. If so, carefully look at your pointcuts. Needless to say, there should not be an infinite loop in your program. You might want to look at using a within pointcut for a filter.

(You might even find that this test case aborts with no message, i.e.,

$ java tests.test2f
.
$

this is a bug in Sun's JVM where a particular stack overflow causes the VM to abort.)

Make sure to pass the JUnit test tests.Test2f before continuing.

g. Assure input

Instead of throwing an exception when one of Point's int fields are set to an out-of-bounds value, write an aspect to trim the value into an in-bounds one. You'll want to use around advice that exposes the new value of the field assignment with an args pointcut, and proceed with the trimmed value. Becuase this is tricky, type in the below aspect... the trick for this exercise is not to come up with an answer, but to understand the answer.

Make sure to pass the JUnit test tests.Test2g before continuing.

Answer:

package answers;

import figures.*;

aspect Answer2g {
    int around(int val):
            (set(int Point._x) || set(int Point._y))
            && args(val) {
        return proceed(trim(val));
    }

    private int trim(int val) {
        return Math.max(Math.min(val, FigureElement.MAX_VALUE),
                        FigureElement.MIN_VALUE);
    }
}

h. Check another invariant

FigureElement objects have a getBounds() method that returns a java.awt.Rectangle representing the bounds of the object. An important postcondition of the move operation is that the figure element's bounds rectangle should move by the same amount as the figure itself. Write an aspect to check for this postcondition -- throw an IllegalStateException if it is violated.

Note that because we're dealing with how the bounds changes during move, we need some way of getting access to the bounds both before and after the move, in one piece of advice. Also, note that you can create a copy of a figure element's bounds rectangle with new Rectangle(fe.getBounds()), and you can move a bounds rectangle rect with rect.translate(dx, dy).

Make sure to pass the JUnit test tests.Test2h before continuing.

Help Yourself by Helping Others

At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them.


3. Tracing

The crosscutting feature you will be adding in part (4) will be support for caching the bound objects of Group figure elements, which may be costly to compute. On the way to that, though, it's useful to explore the system with some tracing aspects.

a. Simple tracing

Write an aspect to trace whenever a Point is moved. To do this, use the utility class Log (with an import from support.Log) and call

Log.log("moving")

This will write the string "moving", followed by a semicolon terminator, to the Log. For example, with your aspect enabled,

Point p1 = new Point(10, 100);
p1.move(37, 8);
System.out.println(Log.getString());

should print out "moving;".

Test this with the JUnit test case tests.Test3a. Without adding any aspects, this test should fail:

$ ajc -Xlint -argfile base.lst 
$ java tests.Test3a
..F.......
Time: 0.07
There was 1 failure:
1) testMovePointLog(tests.Test3a)junit.framework.AssertionFailedError: expected:<set;> but was:<>
        at tests.Test3a.testMovePointLog(Test1a.java:30)
        at tests.Test3a.main(Test1a.java:16)

FAILURES!!!
Tests run: 9,  Failures: 1,  Errors: 0

But with the proper aspect added to the compilation, (in this case, answers/Answer3a.java, but you should feel free to use more evocative names), the test should pass

$ ajc -Xlint -argfile base.lst answers/Answer3a.java
$ java tests.Test3a
.........
Time: 0.089

OK (9 tests)

Answer:

package answers;

import support.Log;
import figures.*;

aspect Answer3a {
    before(): execution(void Point.move(int, int)) {
        Log.log("moving");
    }
}

b. More complex tracing

Write an aspect to trace whenever a Point is added to a group (including initially). To do this, use the utility class Log (with an import from support.Log) and call

Log.log("adding Point")

This will write the string "adding Point", followed by a semicolon terminator, to the Log. For example, with your aspect enabled,

Point p1 = new Point(10, 100);
Point p2 = new Point(10, 100);
Group g = new Group(p1);
g.add(p2);
System.out.println(Log.getString());

should print out "adding Point;adding Point;".

Hint: The args pointcut allows you to select join points based on the type of a parameter to a method call.

Test this with the JUnit test case tests.Test3b.

c. Keeping track of state

In this exercise, perform the tracing from part (a), but also log the enclosing group, if any, of the moving point. You can use an inter-type declaration inside your aspect to associate a Group field with Point objects, and then work with that field, setting it appropriately when the Point is added to a Group (at the same join points you were tracing in part b). So

Point p1 = new Point(10, 100);
p1.move(0, 0);
System.out.println(Log.getString());

should print out "moving as a part of null;", but

Point p1 = new Point(10, 100);
Group g = new Group(p1);
p1.move(0, 0);
System.out.println(Log.getString());

should print out "moving as a part of Group(Point(10, 100));", which you can do by using the toString() method already defined on Group.

Hint: This exercise combines the tracing from parts a and b. If you start with an aspect that includes the solutions to those previous exercises, you'll be most of the way there.

Test this with the JUnit test case tests.Test3c.

Help Yourself by Helping Others

At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them.


4. Caching

Computation of the bounding box of Group objects needs to deal with all aggregate parts of the group, and this computation can be expensive. In this section, we will explore various ways of reducing this expense.

Optional: In all of these exercises, you should only deal with points that are added directly to Groups, rather than those that are added "indirectly" through Lines and Boxes. You should handle those points contained in Lines and Boxes only if time permits.

a. Make a constant override

Group's getBounds() method could be understood to be a conservative approximation of the bounding box of a group. If that is true, then it would be a legal (and much faster) implementation of getBounds() to simply always return a rectangle consisting of the entire canvas, that is

new Rectangle(FigureElement.MIN_VALUE, FigureElement.MIN_VALUE, 
              FigureElement.MAX_VALUE - FigureElement.MIN_VALUE,
              FigureElement.MAX_VALUE - FigureElement.MIN_VALUE)

Write an aspect to implement this change. You can override Group's getBounds() method entirely with around advice intercepting the method.

Your code should pass the JUnit test case tests.Test4a with this change.

Answer:

package answers;

import figures.*;
import java.awt.Rectangle;

aspect Answer4a {
    private Rectangle wholeCanvas =
        new Rectangle(FigureElement.MIN_VALUE, FigureElement.MIN_VALUE, 
                      FigureElement.MAX_VALUE - FigureElement.MIN_VALUE,
                      FigureElement.MAX_VALUE - FigureElement.MIN_VALUE);

    Rectangle around(): execution(Rectangle Group.getBounds()) {
        return wholeCanvas;
    }
}

b. Make a constant cache

Instead of making the (very) conservative approximation of getBounds() from part (a), write an aspect instead that remembers the return value from the first time getBounds() has been called on a Group, and returns that first Rectangle for every subsequent call.

Hint: You can use an inter-type declaration to keep some state for every Group object.

Your code should pass the JUnit test case tests.Test4b with this change.

c. Invalidate, part 1

While caching in this way does save computation, it will lead to incorrect bounding boxes if a Group is ever moved. Change your aspect so that it invalidates the cache whenever the move() method of Group is called.

Your code should pass the JUnit test case tests.Test4c with this change.

d. Invalidate, part 2

Of course, part (c) didn't really solve the problem. What if a Point that is part of a Group moves? Whenever either of a Point's fields are set it should invalidate the caches of all enclosing groups. Use your solution to problem 3c to modify your invalidation criteria in this way, but note that this is slightly different than the problem in 3c: Here you care about fields, where there you cared about method calls.

Your code should pass the JUnit test case tests.Test4d with this change.

e. Invalidate, part 3

Did you really do part (d) correctly? Run the JUnit test tests.Test4e to see. If you pass, congratulations, now go help other people. Otherwise, you have fallen prey to our cruel trap: Remember that whenever a point moves it should invalidate the caches of all enclosing groups.