In this chapter, we're going to discuss JMatter's support for business
objects with lifecycles. We're going to write an Issue Manager, a
familiar application for software developers who use such systems
to help manage the development of software. Examples of issue managers
include Bugzilla, Trac, and JIRA.
7.1 Analysis
The central type in this application is the Issue. The interesting
aspect of issues is that they have a life cycle. An Issue is created
and assigned to a developer. The developer accepts the issue and begins
working on a fix. After resolving the issue, the person who opened
the issue verifies that the resolution is indeed satisfactory, and
proceeds to close the issue. If the issue is not resolved to satisfaction,
the issue can be reopened.
It's also important to keep some kind of trail of activity: who opened
the issue? Who resolved it and when? An end user should have the ability
to attach notes to an issue, to specify a description of the issue,
steps to reproduce the problem (if applicable).
The main trait of objects with life cycles is that their behaviour
depends on their state. For example, one cannot attempt to accept
an issue if it's already been closed. We can identify the various
states for an Issue, and define its lifecycle with the aid
of state diagrams, transition tables, etc.. Software developers will
typically model such objects using the State Design Pattern22.
I have decided to model an Issue with five states:
- New
- Assigned
- Accepted
- Fixed
- Closed
In a normal flow, an issue moves sequentially through these states.
An issue is created (New state), then assigned to a developer
(Assigned state), and accepted by the developer (Accepted
state). The person who opened the issue has the option of rejecting
the fix (in which case its state reverts to Accepted) or approving
it, moving Issue to the Closed state. An issue can also
be reassigned to a different developer.
7.1.1 Modeling States in Java
In Java, the State pattern is often and conveniently modeled using
inner classes. Each state is implemented as an inner class which controls
the behaviour of the Context object (in this case, Issue)
when in that state. Also, many texts encourage the design to use static
inner classes for improving the performance of the system. That is,
rather than create an instance of each inner class per Issue, a single
instance of each inner class exists for all issues. The issue that
is to be operated upon is passed in as an argument to that inner class's
methods.
Although there's no denying the improvement in memory requirements,
I personally find this design less than ideal, from an object-oriented
point of view. I find it akin to removing the implicit this
keyword in instances and passing a reference to self with each
method.
7.2 Getting Started
We're about to start building our fourth application. Since we've
done this together three times already, I'm not going to walk you
through the steps of creating a new project and configuring it Please
refer to previous chapters for specific instructions. I named my application
IssueMgr.
Ok, we need to model Issue. As a first pass, let's not worry
right away about the various states we identified in the previous
section. Let's instead start be defining the various properties we
want an issue to have. Here are the properties I've come up with:
-
private final StringEO _title = new StringEO();
private final TextEO _description = new TextEO();
// a loose definition (using a numeric value instead of an enumeration)
private final IntEO _priority = new IntEO();
private final IntEO _severity = new IntEO();
private Issue _dependsOn;
private final RelationalList _notes = new RelationalList(Note.class);
public static Class notesType = Note.class;
private User _openedBy;
private User _assignedTo;
private final RelationalList _history =
new RelationalList(LoggedEvent.class);
public static Class historyType = LoggedEvent.class;
An issue then will have a title and a detailed description. Note how
the description field is defined as a TextEO which causes it to be
rendered using a text area, and saved as in the database as a large
text type. I've decided for a first-pass to keep severity and priority
really simple, as numeric values. The next one: dependsOn is
interesting and fairly self-explanatory. It will give us the knowledge
that Issue A will not be resolved before the issue it depends on (say
Issue B) is resolved, for example.
The next field, notes, will allow us to tack on notes to an
issue (as many notes as we want in fact). A Note is yet another
predefined type in JMatter. It has a subject, a time stamp, an author,
and the note text itself. When creating a note, the author is automatically
assigned to the user who is currently logged in.
The next two fields, openedBy and assignedTo are self-explanatory.
The last field is an interesting one. The idea is that I'd like to
keep track of the history of an issue: when was it opened, assigned,
accepted, fixed, etc.. Furthermore, when an issue is fixed, i want
to require the developer to enter both a summary description of the
fix and a lengthier one. I want to capture that information.
After thinking about this for a little while, I realized that JMatter's
built-in LoggedEvent type would be a perfect candidate for
recording this information. It's already designed to hold precisely
this type of information. It has a timestamp, a message, long message,
it records the user who performed the action, it can even record what
action was performed, and finally, has an association back to the
object upon which the operation was performed. In the context of the
issue manager, we'll be able to navigate from a log entry (with message
Issue fixed at 4:30 pm) back to the issue it is associated
to.
Ok, it looks like we have a fairly complete list of fields to start
with. Of course, it's not enough to just define these fields, the
JMatter conventions must be followed: define accessor methods for
aggregate types and both accessors and mutators for association types,
following the JavaBeans bound property convention.
-
public StringEO getTitle() { return _title; }
public TextEO getDescription() { return _description; }
public IntEO getPriority() { return _priority; }
public IntEO getSeverity() { return _severity; }
public Issue getDependsOn() { return _dependsOn; }
public void setDependsOn(Issue issue)
{
Issue oldValue = _dependsOn;
_dependsOn = issue;
firePropertyChange("dependsOn", oldValue, _dependsOn);
}
public RelationalList getNotes() { return _notes; }
public User getOpenedBy() { return _openedBy; }
public void setOpenedBy(User user)
{
User oldValue = _openedBy;
_openedBy = user;
firePropertyChange("openedBy", oldValue, _openedBy);
}
public User getAssignedTo() { return _assignedTo; }
public void setAssignedTo(User user)
{
User oldValue = _assignedTo;
_assignedTo = user;
firePropertyChange("assignedTo", oldValue, _assignedTo);
}
public RelationalList getHistory() { return _history; }
Here is the context within which all this code is written:
-
@Persist
public class Issue extends AbstractComplexEObject
{
public static String[] fieldOrder = {"title", "description", "notes",
"openedBy", "assignedTo", "history", "severity", "priority"};
public Issue() {}
...
public Title title()
{
return _title.title().appendParens(""+getID());
}
}
We added the familiar fieldOrder metadata and the required
title() method. Since it is customary to refer to issues by
some unique numeric ID, I'm exposing the issue's ID property in its
title. This property is inherited from AbstractComplexEObject.
We normally do not edit src/persistClasses.st by hand: the
@Persist annotation takes care to add Issue to the list automatically.
However here we're using a predefined entity Note and for it
we do need to manually add an entry to our persistClasses.st
file, like so:
-
..
<value>com.u2d.type.composite.Note</value>
..
We can now generate and export our schema, run the application and
create a few issues.
7.3 Modeling Issues' Lifecycle
The time has come to model issues' lifecycle.
There are a number of concerns here. The first is keeping track of
the state of our issue. And more specifically, to ensure that when
an issue is persisted to the database, that its state is remembered
and properly restored at a later point in time.
A simple way to do this is to define yet another field to hold the
name of the state. This field will be persisted to the database just
like Issue's other fields, and can be the basis for restoring
Issues' state when reloading issues from the database.
-
private final IssueState _status = new IssueState(NEW);
Where NEW is one of a number of static string-based constants:
-
static final String NEW = "New";
static final String ASSIGNED = "Assigned";
static final String ACCEPTED = "Accepted";
static final String FIXED = "Fixed";
static final String CLOSED = "Closed";
We have identified five states and they're not likely to change. JMatter
provides a mechanism for modeling enumerations by extending the JMatter
type ChoiceEO. Here is the implementation of the contract to
define the IssueState enumeration:
-
public class IssueState extends ChoiceEO
{
public IssueState() {}
public IssueState(String value) { setValue(value); }
private static Set STATUS_OPTIONS = new HashSet();
static
{
STATUS_OPTIONS.add(Issue.NEW);
STATUS_OPTIONS.add(Issue.ASSIGNED);
STATUS_OPTIONS.add(Issue.ACCEPTED);
STATUS_OPTIONS.add(Issue.FIXED);
STATUS_OPTIONS.add(Issue.CLOSED);
}
public Collection entries() { return STATUS_OPTIONS; }
}
Nothing too interesting really. We also must remember to add an accessor
method for status:
-
public IssueState getStatus() { return _status; }
7.3.1 Defining the State Inner Classes
Below I've defined five inner classes, one for each of the states
that Issue can be in.
-
public class NewState extends ReadState {}
public class AssignedState extends ReadState
{
@Cmd(mnemonic='a')
public void Accept(CommandInfo cmdInfo)
{
transition(_acceptedState, makeLog("Issue accepted by developer"));
}
}
public class AcceptedState extends ReadState
{
@Cmd
public void Fix(CommandInfo cmdInfo,
@Arg("Fix") StringEO fix,
@Arg("Description") TextEO description)
{
transition(_fixedState,
makeLog("Fix: "+fix.stringValue(), description));
}
}
public class FixedState extends ReadState
{
@Cmd
public void RejectFix(CommandInfo cmdInfo,
@Arg("Explanation") TextEO explanation)
{
transition(_acceptedState, makeLog("Fix rejected", explanation));
}
@Cmd
public void Close(CommandInfo cmdInfo,
@Arg("Explanation") TextEO explanation)
{
transition(_closedState, makeLog("Issue Closed", explanation));
}
}
public class ClosedState extends ReadState {}
Notice that the convention for defining commands on these states is
the same as the mechanism used for defining commands in general. The
difference is that JMatter will ensure that these commands are accessible
in a valid state. The @Arg annotations' values are a means
of specifying the captions used to prompt the end user for each of
the method parameters.
Although we've defined the classes, we must also create an instance
for each:
-
private transient final State _newState, _assignedState,
_acceptedState, _fixedState, _closedState;
{
_newState = new NewState();
_assignedState = new AssignedState();
_acceptedState = new AcceptedState();
_fixedState = new FixedState();
_closedState = new ClosedState();
_stateMap.put(_newState.getName(), _newState);
_stateMap.put(_assignedState.getName(), _assignedState);
_stateMap.put(_acceptedState.getName(), _acceptedState);
_stateMap.put(_fixedState.getName(), _fixedState);
_stateMap.put(_closedState.getName(), _closedState);
}
In addition to instantiation, I've also added each state to a map,
defined in Issue's superclass. At the moment, this is a requirement
of the framework. We haven't yet specified the starting state, so
let's do that:
-
public State startState() { return _newState; }
We also need to provide a mechanism for the Issue's state to be restored
after an issue is fetched from the database:
-
public State restoredState()
{
return (State) _stateMap.get(getStatus().code());
}
Both of these are really simple. Here we see that we use the value
of the issue's status (which was fetched from the database) as a means
to fetch the state object from the state map.
I have not yet showed you the support code for the transitions. In
each state where a command is defined a transition takes place. A
logged event is created and passed in to the method named transition().
Here's how I create the logged event:
-
private LoggedEvent makeLog(String msg)
{
LoggedEvent evt = (LoggedEvent) createInstance(LoggedEvent.class);
evt.getMsg().setValue(msg);
evt.getType().setValue(LoggedEvent.INFO);
evt.setUser(currentUser());
evt.setObject(this);
return evt;
}
private LoggedEvent makeLog(String msg, TextEO longMsg)
{
LoggedEvent evt = makeLog(msg);
evt.getLongMsg().setValue(longMsg);
return evt;
}
I provide two utility methods for constructing logged events. The
first does not require a long message, the second is an overloaded
version that also sets the long message. Notice how commands such
as Fix, Close and RejectFix specify arguments.
Recall that JMatter "automagically" prompts the end user for
these arguments in the user interface and then feeds them to the commands
when invoking their methods. These values are then passed in to the
makeLog() methods.
Finally, here is the implementation of the transition() method:
-
private void transition(State state, LoggedEvent evt)
{
_history.add(evt);
setState(state, true);
_status.setValue(state.getName());
persistor().updateAssociation(this, evt);
}
We see here that the logged event is added to the history field, the
state transition takes place, the status field is updated accordingly
(kept in sync with the issue's state). The final statement ensures
that the updated issue and its association to the newly created logged
event are saved to the database.
7.4 Per-State Icons
Up until now, you've been told that for each type of object you define,
that JMatter will look for an image file with a specific naming convention
to use to represent objects of that type. This is true, but it's not
the whole story.
Let's take an example. For the class Issue, we provide Issue32.png
and Issue16.png. However, you're also free to provide additional
icons, one for each state: IssueAssigned32.png, IssueAccepted32.png,
etc.. So we see here an extension of the convention. For objects with
lifecycles, the icon can be made to further reflect the state of the
object you're viewing. I have specified custom icons for each of Issue's
five states. Here's (figure 7.1)
a screenshot of the icons in my resources/images directory,
along with their corresponding file names.
Figure 7.1: State-Specific Icon Support
7.5 Additional Metadata
We've defined a number of commands, such as Fix(CommandInfo
cmdInfo, StringEO fix, TextEO description). We customized the captions
for the two arguments fix, and description so the end
user is clear about what information he or she will have to enter.
We did this by annotating the method's arguments with @Arg
annotations, like this:
-
public void Fix(CommandInfo cmdInfo,
@Arg("Fix") StringEO fix,
@Arg("Description") TextEO description)
The annotation takes a single argument, the parameter caption.
There's a second, much more important issue that needs to be addressed
though. In the case of our issue manager application, it's not enough
that the command accept be only accessible in Assigned
state. Only the developer who has been assigned the particular issue
should be allowed to accept the issue. The same applies to the fix
command. For rejectFix and close, the same idea applies:
only the user who opened the issue should be allowed to close it,
not the developer.
JMatter provides a means to specify what user is the owner
of a command. A command's owner is the only user who will be allowed
to invoke it. JMatter will not even display the command's views (a
button or a menu item) to any other user. Here's how this is done:
-
static
{
ComplexType type = ComplexType.forClass(Issue.class);
type.command("Accept", AssignedState.class).setOwner(type.field("assignedTo"));
type.command("Fix", AcceptedState.class).setOwner(type.field("assignedTo"));
type.command("RejectFix", FixedState.class).setOwner(type.field("openedBy"));
type.command("Close", FixedState.class).setOwner(type.field("openedBy"));
}
I hope you'll agree this is fairly terse, yet clear and legible code.
The last line, for example, interprets to "the owner for the command
named Close (in Fixed state) is the value of the issue's
openedBy field. In other words, whoever opened the issue is
the one authorized to close it.
7.6 A Few Loose Ends
7.6.1 Default Assigned-To Developer
It sure would be nice if each time I created a new issue, a certain
developer would be the default user assigned to the issue. One way
to do this is to specify the default in the file resources/model-metadata.properties,
like this:
-
#
Issue.assignedTo.default=from User as user where user.username='eitan'
We've already used this file to specify field metadata such as whether
and which fields are required. Here we're specifying a default. We
can either hard-code it, or specify any valid hql (hibernate query
language) that will return an instance of a valid type.
7.6.2 Automatically Setting OpenedBy
Each time we create an issue, the person who opened the issue is by
definition the currently logged in user. How do we programmatically
specify that this should automatically happen? Here is one way to
do this:
-
public void onBeforeCreate()
{
super.onBeforeCreate();
setOpenedBy(currentUser());
}
This method overrides a superclass method, one that is notified prior
to the creation of an object. It turns out that persistent objects
(such as Issue) are not the only ones that can listen to various
object persistence lifecycle events. JMatter provides a generic notification
mechanism that any object can take advantage of. Each type of event
is defined by a string constant, such as DELETE, SAVE, BEFORECREATE,
CREATE. You'll see an example use of this mechanism shortly. It's
also worth noting that besides object persistence events, JMatter
provides hooks for application events such as login and logout events.
7.6.3 Transitioning to AssignedState
You might have noticed that I have left a glaring omission: how exactly
does an issue transition to assigned state? The transition should
take place when a user is associated to the assignedTo property
of Issue.
We need to be careful here. The setter method is called not only when
an association is made but also when the object is restored from the
persistence store (the database). To distinguish between these two
contexts, JMatter allows the definition of an additional method, the
associate method. It works like this: if both a setter and
an associator are defined, JMatter will make sure to call the associator
only when associating (calling only the setter when restoring the
property from db).
Here's the implementation:
-
public void associateAssignedTo(User user)
{
setAssignedTo(user);
if (_assignedTo != null && !_assignedTo.isEmpty())
{
if (isEditableState())
{
addAppEventListener(ONCREATE, new AppEventListener()
{
public void onEvent(AppEvent appEvent)
{
transition(_assignedState,
makeLog("Assigned to "+_assignedTo));
}
});
}
else
{
transition(_assignedState, makeLog("Assigned to "+_assignedTo));
}
}
}
This code looks a little complicated. It has to concern itself with
a specific issue. A call to the transition() method has the
side effect of saving everything and putting the object in read state.
We want to delay the call to transition if the association is made
while the issue is in an editable state (before it has been saved,
while it's being edited).
I'm resorting to using JMatter's application event notification mechanism.
If the association is made in read state, I simply transition. Otherwise,
I delay transitioning until after the editing is complete.
7.7 Issue Categories
Our implementation of Issue is now complete. Let's add one last feature.
It might be helpful to define various categories of issues and classify
issues according to these categories.
The first thing we do is define an issue category type:
-
@Persist
public class IssueCategory extends AbstractComplexEObject
{
private final StringEO _name = new StringEO();
public IssueCategory() {}
public StringEO getName() { return _name; }
public Title title() { return _name.title(); }
}
Nothing fancy here. An issue category is defined to have a single
field: a name.
Rather than define a to-many relationship to issue, I'm going to add
a command that will fetch the category's issues from the database
and return a paged list:
-
@Cmd
public Object Issues(CommandInfo cmdInfo)
{
ComplexType type = ComplexType.forClass(Issue.class);
FieldPath path = new FieldPath("com.u2d.issuemgr.Issue#category");
QuerySpecification spec = new QuerySpecification(path,
new IdentityInequality().new Equals(), this);
SimpleQuery query = new SimpleQuery(type, spec);
return new PagedList(query);
}
I'm not entirely satisfied with the implementation. I'd like to be
able to use plain old hql or the hibernate API to define this query.
The above uses JMatter's own API for defining queries, which basically
says: fetch all issues where the category equals "this".
On the issue side, we need to add a to-one association to the issue
category:
-
private IssueCategory _category;
public IssueCategory getCategory() { return _category; }
public void setCategory(IssueCategory category)
{
IssueCategory oldValue = _category;
_category = category;
firePropertyChange("category", oldValue, _category);
}
We also need to update our fieldOrder metafield:
-
public static String[] fieldOrder = {"status", "title", "description",
"notes", "openedBy", "assignedTo", "history", "severity",
"priority", "category"};
That's it for the coding. Make sure to update the schema and let's
run the application.
7.8 The Application
Just because we finished coding does not mean that we're done configuring
our application. For example, it might be useful to define a few standard
queries such as List Outstanding Issues and List Closed
Issues that pre-filters the listing (using JMatter's Smart List mechanism).
Figure 7.2 below shows the issue manager
in action. I'm listing all issues to illustrate how the issue's icon
reflects its state (the ones with a lock are closed issues, the ones
with the pencil are accepted, and supposedly, being worked on). I'm
also showing two smart lists that I've created and that I often use
to check out outstanding issues.
Figure 7.2: The Issue Manager
I also often view issues in tabular view, which allows me to sort
issues by priority or severity (sorting is invoked by clicking on
the table column header).
7.9 Summary
As in previous chapters, allow me to show you the stats for this application,
in terms of lines of code:
-
eitan@ubuntu:/projects/ds/IssueMgr/src/com/u2d/issuemgr$ wc -l *.java
40 IssueCategory.java
232 Issue.java
28 IssueState.java
300 total
Three classes totaling 300 lines of code. The current state of lifecycle
support in JMatter is pretty strong. Its implementation is a natural
extension of the conventions already established for simpler business
objects. Nevertheless, I believe the implementation can be streamlined
further and place even less requirements on the developer. For example
the requirement to add the various states to a state map could be
done by the framework.
In summary, support for business objects lifecycle is a necessary
component of supporting the development of business applications in
general. Many business objects naturally embody lifecycles, including
orders (new, confirmed, fulfilled, etc..), visits (scheduled, canceled,
confirmed, ongoing, archived), etc..
We have now covered four distinct applications: a contact manager,
the MyTunes application, the Sympster conference manager, and finally
our issue manager in a very short time. JMatter also comes with a
movie library application, that, among other things, illustrates many-to-many
relationships, as well as two other demonstration applications that
illustrate how to integrate custom views into your application (the
topic of chapter ).
Let's now turn our attention to part ,
which covers the JMatter framework in detail.
Part 3
Framework Reference