|
|
|
The Model-Driven System Builder
|
|
JeeWiz Architect's Guide
|
|
|
|
Contents >
9. Patterns
|
|
|
9.6 Ordering Of Events
This chapter gives detailed information about the order of events during pattern firing.
There is more detail here than you would want on a first reading.
Furthermore, most of the time you do not need to know the detailed ordering of events:
the default mechanisms are set up in such a way that most patterns work.
This information becomes important when your patterns involve
- projecting tiers (mega-patterns)
- creating dependent objects.
The theme addressed throughout this section is ordering:
how do we order the various parts of the pattern mechanism so that complex patterns can be expressed?
The relative ordering of these stages, and the relative ordering of firing patterns on the different types of objects,
is the key to building multi-tier systems automatically.
9.6.1 Creation
|
The first event for a model object is its creation from the original specification or from a pattern firing.
For example, the specification
<entity name="Customer"/>
|
will produce a run-time (Customer) object from an appropriate 'entity' class -
the actual class created at generate time will be determined by the set of meta-models configured for the current build.
If there is a rename applicable to the meta-class, this is applied before looking up the class for the model object.
For example, in the J2EE system, there are two types of entities, 'jdo-entity' and 'ejb-entity',
and the correct one is chosen by a rename.
If you are writing Java code for the meta-class, you need to be aware of the following sequence of events during the creation process.
- The model object is instantiated.
- It is added to the parent's list.
- The attribute values are filled in by calling the setter for the attribute if one exists, or, if not,
by putting a key-value entry in the model object's general-purpose HashMap.
- The model object's 'onInstantiation()' method is called.
The implication of this order is that when an object is added to a list in step 2, no attribute values have been set - that is the next step.
(This logic is in fact inherited from Ant, which determines the class to create and the list to add it to in one step).
This means in the 'add' method (that adds a model object to a list on its parent)
you cannot create indexes or do any other processing that depends on attribute values of the model object.
The usual technique for dealing with this issue is to use lazy evaluation,
where the calculation of the contents of an index is delayed to when it is first used.
This strategy is used by the meta-model generator: it generates code to evaluate list indexes on the first access.
The 'onInstantiation()' method (a late addition to JeeWiz) can also be used to perform first-stage initialisation.
But note that 'onInstantiation()' is still called after the model object is added to the parent's list.
9.6.2 Preparation Phase
|
Once the whole tree of model objects has been read in from the specification,
there is a preparation phase (referred to as 'pre-phase' when writing meta-models), in which we further prepare each model object and its environment.
This phase operates on each model object using
- a downward sweep, proceeding top-down, left-to-right
- an upward sweep, where further actions - 'extra' preparation - are taken.
For example, if we consider the tree surrounding a session's business method in a J2EE build,
the preparation phase will process objects in the following order:
- the application itself
- the ejb-jar, and - after processing its children - its right-hand-side peers, like ui-jars
- the session, and - after processing its children - its right-hand-side peers, like data-views and entities
- the method and its peers like fields/attributes
- each parameter
- constraints for each parameter
- then the extra preparation for the constraints, and so on up the tree, reversing the above order.
At each model object, the preparation phase follows the order of steps described below, which are all optional.
9.6.2.1 Downward Sweep
|
-
First, the current object's children are reordered if necessary.
The order of the children is important because it is also the order that patterns are fired.
Sometimes it is necessary to reorder the children so that the pattern processing and validation is done in the correct order.
For example, the order of the types of children below a jar in the Business Object model,
is: general classes; internal-classes; business objects; entities; relations; data-views; data-view-relations; sessions.
This is the 'correct' order so that patterns are fired and validation done on depended-on objects first.
For example, entity objects come before the relations, because relation ends (R/end in the example below) reference entities (E1 and E2 are named in the ends).
Similarly, data-views are also dependent on entities in the standard architecture,
because data-views can be 'backed by' entities (as shown in the E1DataView's 'inital-entity' attribute).
These references between dependent and depended-on objects are specified by the name of the depended-on object, but this is converted to a Java object reference during the main validation.
If the depended-on object is not present, the validation fails. The following specification shows the model objects in the 'correct' order.
<jar name="J">
<entity name="E1"/>
<entity name="E2"/>
<relation name="R">
<end entity="E1"/>
<end entity="E2"/>
</>
<data-view name="E1DataView"
intial-entity="E1"
/>
</jar>
|
Why do we order relations before data-views, given that they both depend on entities?
There is no absolute requirement - if data-views came before relationships, it would still work.
However, it is easier to think about within-tier dependencies binding closing than cross-tier dependencies,
and will make it slightly easier to separate and replace on of the tiers.
Another dependency example: ui-jars depend on jars - we need all the backend in place before we build the user interface that references it;
if we didn't do this, we would build a ui-jar that didn't give access to all the functionality of the backend.
Children are added to various model objects during pattern processing.
The order of the children is not adjusted dynamically (as every object is added).
The ordering described is done here and also at the start of the main phase.
Models that are added during pattern firing are added at the end of the child list,
and the pattern processing described here is done for new objects in the order they are created.
The relative order of meta-classes is defined in the meta-model
(by overriding the getPositionWithinParent() method for the object, which by default returns 0).
This is defined in the meta-classes rather than at the rendering level (templates/patterns)
because the specified order must reflect the direction of references between tiers.
These references should be computed and validated in the meta-model, not in the rendering.
- If there is a 'preIncludeSpec.vm' script for the object, this is invoked as a pattern
(i.e. the property is interpreted as the name of a script,
it is processed through Velocity
and the output is interpreted as more specification).
preIncludeSpec are preparation phase patterns, and are primarily intended for
adjusting various properties on the current object only.
For example, the business object attribute's preIncludeSpec adjusts the name, type, access and other fields.
It can also be used to adjust the environment in simple ways.
For example, the preIncludeSpec for the application in some demonstration meta-models creates a ui-jar if one doesn't already exist.
This is a simple adjustment to the environment that depends only on the objects in the application object.
(More complex preparation phase patterns that involve multiple objects should use the extraIncludeSpec mechanism, described below.)
When writing preIncludeSpec's, it is important to remember that there is no guarantee of any value in any property - no validation has been done yet.
Even if a field is required, you cannot assume that it is present - because that validation has not been done yet.
The only fields you can depend on are primitive values like boolean properties, and fields with defaults; when using other fields, you should check for existence before using them.
For example, the attribute preIncludeSpec mentioned above uses $name - but prefaces its use by a #if( $name ).
Furthermore, the preIncludeSpec pattern deals with meta-class properties as defined in the meta-model.
Derived names defined in the component.properties are not available - they come in the next step.
This is by design: it means you can manipulate the raw model object properties in the preIncludeSpec
- which is likely to be the domain of an architecture specialist.
If any property values are altered in this process, they will be used in the component.properties calculation in the next step.
This means that the name mapping defined in component.properties can focus on the details of naming conventions,
whereas the preIncludeSpec can enforce very specific domain requirements for property values of the target environment.
preIncludeSpec patterns often change model object properties. There are two possible ways of doing this:
- via XML - e.g. '<this name="$this.uncapitalise($name)">'
- via Velocity - e.g. '#set( $this.name = "$this.uncapitalise($name)" )'
While both of these will set the name to the new value,
the XML approach is preferred because, when a property is being set for the first time,
it will appear in the dump (of the aggregate XML as used).
If you use the Velocity approach for setting new values, they will not appear in the dumped XML.
The alternative way to do this in Velocity is to use setX(), e.g.
#set( $dummy = $this.setX( "name", "${name}" ) )
|
Although the 'preIncludeSpec' is designed to adjust the current object only,
the full range of object-creation capabilities is available.
Any new objects created by the 'preIncludeSpec' will go through the pre-initialisation process as described here,
and then take part in the parent's re-ordering process, as described later in this subsection.
- component.properties are set.
The component properties are properties that are calculated as defined in the
component.properties regime.
This provides a simple way to apply naming conventions across a build.
(This is additive across the component.properties files for the various meta-models invoked in a build, which makes it easy to easy to add more names to the conventions without knowledge of the existing naming conventions.)
The results calculated from the component properties files are registered as a key-value pair in the general-purpose HashMap,
which means the property's value can be accessed in Velocity scripts by the expression '${object.key}'.
The component.properties values are calculated after the preIncludeSpec.vm pattern is applied.
This means that the preIncludeSpec pattern can change properties that are then used to calculate the names used in subsequent patterns and templates.
The naming conventions will still be observed, but possibly adjusted by alterations introduced by the preIncludeSpec.vm
Note the 'top-down' ordering.
This means that a parent model object's component properties have been calculated and are available for use when a child model object's
component properties are calculated. It is sensible to make use of this fact - for example, directory structures are a typical example
where a child object (e.g. an ejb-jar) will want to create its own properties of the same name as one of the properties on its parent (which is the overall application).
For example:
buildDir=${buildContainerDir}/buildJar${name}
buildContainerDir=${buildDir}
|
The calculation of 'buildDir' makes use of 'buildContainerDir' from its parent, and then sets its own value for 'buildContainerDir' which will override its parent's value when accessed
in the component.properties of its children (or their children etc.).
- Meta-model validators (defined by '<validator>' elements in the meta-model spec) for 'pre-phase' are checked.
(Validators can also apply to the next phase, but they apply to the pre-phase by default.)
These validators should only reference properties in this model object, because other objects in the environment may be created after this validation is run.
A special case of pre-phase validation is where the meta-model specification for a meta-class defines a property to be required.
This is equivalent to a pre-phase validation and will be checked at this point (the validation code is output by the meta-model generation patterns - you don't have to write it).
- Next, the model object's preInitialiseComponent() method, if present, will be called.
The intent of this method is to do more complex validations and preparations than are possible using the meta-model validators.
You should only reference items within this model object in preInitialiseComponent().
To prepare and validate references to other model objects, do this in the pattern phase, described below.
-
Then the children are processed (using this preparation phase), working left-to-right.
The original children may have created peer model objects - they are new children of the current object.
According to the conventions recommended here, the new children will be created by the extraIncludeSpec phase, described next.
They could also have been created by the preIncludeSpec, described above.
Because the children of a model object are processed left-to-right, it turns out that new children are caught by the left-to-right sweep.
This means that all the original children first get processed first, followed by any new peer objects.
This is so, even if a child creates a new peer - as may happen.
For example, a data-view could create a peer entity object during the preparation phase: both are children of the jar,
so the entity will automatically get included in this step.
We expect creation of new children in the preparation phase to be used rarely,
and the need for multiple passes to create new children here even rarer.
However, the facility is there if needed.
9.6.2.2 Upward Sweep
|
-
If there is an 'extraIncludeSpec.vm' script for the object, this is invoked as a pattern
(i.e. the property is interpreted as the name of a script,
it is processed through Velocity
and the output is interpreted as more specification).
The extraIncludeSpec is used to fix up the object's own property and its environment,
where this depends on information outside the object itself.
In the same way that the preIncludeSpec is processed before the pre-phase validation and initialisation,
so the extraIncludeSpec is processed before the validation and initialisation described below.
This means that the extraIncludeSpec pattern has the opportunity to fix up the values in
an object before the extra validation.
- Meta-model validators (defined by '<validator>' elements in the meta-model spec) for 'extra-phase' are checked.
These validators can check properties in this model object and its children of course,
but it is also reasonable to use related objects that come before this one in the ordering described above.
For example, an entity relation can reference information from its related entity,
because the entity comes before the relation in the ordering.
- Next, the model object's extraInitialiseComponent() method, if present, is called.
As for the preInitialiseComponent(),
the intent of this method is to do more complex validations than are possible using validators.
Wherease preInitialiseComponent() should only reference properties of the model object itself,
the extraInitialiseComponent() method can validate this model object and its children -
such as required number of objects in certain lists, of the uniqueness of names.
After the downward and upward sweeps have completed,
values internal to the object and its dependent children have been validated
and transformed into a consistent state for use by the current rendering.
If there is a validation failure during this phase, the build is stopped - without continuing to the pattern phase.
9.6.3 The Main Pattern Phase
|
The main pattern phase checks inter-model object dependencies and creates additional objects by running patterns.
This is where the bulk of the pattern firing happens.
The pattern processing in this stage can include the number of objects, and the complexity of the build, by factors of 10, 20 or even more.
Whereas the preparation phase operates top-down, the pattern phase operates bottom-up, left-to-right.
For example, if we consider the tree surrounding a method in a J2EE build, we will process
- parameter constraints for a given parameter
- each parameter
- the method holding the parameters (and its peers, like fields/attributes)
- the class - e.g. the entity (and its peers, like data-views and session objects)
- the ejb-jar and its peers
- finally, the application itself.
Note that the order of the objects visited here may have been changed by the preparation phase.
At each model object, the pattern phase does the following:
-
First, the current object's children are reordered if necessary - exactly the same as is done as the first step in the preparation phase.
This reordering catches any objects created during the preparation phase and ensures they are correctly ordered.
-
Meta-model validators for the 'main' phase (i.e. not pre-phase, and not extra-phase) are checked.
Experience has show that most validation should be done in the pre- and extra-phase.
Main phase validators are only used if the validation needs to refer to fixups done in the extra-phase of some 'later-precedence' object;
which is rare.
-
The model object's initialiseComponent() method, if present, will be called.
As with preInitialiseComponent,
the intent of initialiseComponent() is to do more complex validations and preparations than are possible using the validator expressions.
-
Then the children are processed (using this main phase), working left-to-right.
This implements the bottom-up firing of patterns in the main phase, because we do the children before firing the main pattern in the next step.
-
The main patterns - as defined in the "jw-pattern=" property, and any available includeSpec.vm script are processed by Velocity and the resulting output interpreted as more specification.
All the previous steps have been leading up to this point!
Any objects created in this step are processed once the complete pattern file has been read (i.e. Velocity #parse's).
If there are next-pattern phases, the model is actioned immediately, before the next pattern
Similarly, if there are patterns from the pattern property and the includeSpec.vm,
these are separately processed in order, after any 'next-pattern' processing has finished.
9.6.4 Recursion - Creating Objects with Patterns
|
It is possible that patterns create objects that themselves have patterns to create further objects, and so on.
In other words, patterns can be recursive.
The following rules govern when these recursive patterns fire.
Each pattern invocation can be broken into stages by the next-pattern mechanism.
The rules described here apply at the end of each pattern stage.
In other words, parts of the overall pattern processing will be done between one pattern invocation and the next-pattern's invocation.
The step of converting the XML created by the stage into model objects - equivalent to the creation phase - is always done at this point.
The interesting question is, are the preparation and main phases done immediately too?
It is easiest to explain these rules algorithmically, in relation to the tree-walk that takes place in the
preparation and main phases.
The tree-walk says: walk through all my children in order, executing the preparation or main phases as appropriate.
Now, when a pattern is used to create model objects, the pattern defines its root object, which will be the parent of the new objects created.
The root element can either be <this> - the current object; <parent> - the parent of the current object; or elsewhere, as set by setPatternRootObject.
What happens next can be expressed very simply:
we must do enough pattern processing on the new node (and its children) to bring it up to the same level of processing as the parent node
it is being attached to.
In other words, if the treewalk for the preparation phase has been completed for the parent (i.e. the root element),
we must do the preparation phase for the new subtree immediately.
Furthermore, if the treewalk for the main phase has been completed for the parent,
we also do the main phase for the subtree immediately.
The new object will be added to the parent (root element for the pattern) and any remaining pattern phases will
be caught by the normal treewalk.
The nature of the treewalk is that any object that is added to the end of a child list that is currently being processed will
still be processed - the end of the list is recalculated each time round the processing loop.
Another way of looking at these rules is:
-
Where possible, we try to delay processing pattern phases - we try to avoid immediately processing patterns.
This means that the normal scenario is that patterns are processed within a parent in the order that the child objects are created.
- However, at the end of pattern processing, we must have run the preparation and main pattern phases for every object.
Therefore, if we know that the normal processing mechanism will never pick up an object
(because it has been created in a place for which the pattern processing has been done)
then we must process the required phase immediately.
Otherwise, we can safely leave it for later.
Some examples are given below, using the following structure:
<application name="A">
<jar name="J">
<entity name="E1">
<attribute name="oid"/>
</entity>
<entity name="E2"/>
<relation name="R"/> ## E1 <- -> E2
<data-view name="E1DataView">
<data-view-field name="oid">
</data-view/>
<session name="S">
<business-method name="BM">
<input-data-view name="E1DataView"/>
</business-method>
</session>
</jar>
<ui-jar name="UIJ">
<page name="P"/>
</ui-jar>
</application>
|
-
'A' creates UIJ in the pre-phase. In the pre-phase, A's patterns are fired before the treewalk.
Therefore, the treewalk on A has been not been done for the pre-phase.
Result: pattern phases on UIJ are not processed immediately.
-
E1 creates oid in the pre-phase. This is similar to the A/UIJ situation.
E1's pre-phase patterns are fired before it does its treewalk, so the treewalk on E1 has not been done.
Result: pattern phases on 'oid' are not processed immediately.
-
E1 creates E1DataView and the data-view-field 'oid' in the main phase.
The 'oid' does not affect the outcome: it depends on what happens to E1DataView.
E1 is a child of J, and this is the root element for the pattern.
In the main phase, patterns are processed bottom up, so J has completed its preparation phase, but not the treewalk for its main phase - it is in the middle of it.
Result: the pre-phase is processed immediately.
The E1DataView is added to the end of J's child list, and is picked up at the end of the main phase treewalk on J -
E1DataView's main phase patterns are fired then.
-
Note the business method BM uses E1DataView as its input-data-view.
(This is strange, but a real-life situation!).
The point about this is not pattern firing; it is about the validation on BM, which
requires that its input dataview names a data-view.
The E1DataView is created by a pattern on E1.
Due to the ordering step, we are guaranteed that E1's main pattern phase will occur before S's (and therefore BM's).
This means that E1DataView will indeed exist as a model object by the time the validation is done.
-
E1Dataview creates page P under UIJ - for which it will have to use setPatternRootObject in the main phase.
Due to the ordering under applications, we know ui-jars come after regular (or EJB) jars.
Therefore, when E1DataView's patterns in the main phase are fired, UIJ's main phase patterns have not started.
Result: the pre-phase of P is processed immediately but the main phase processing is fired up by UIJ's main phase treewalk.
There is a particular case that these examples do not cover,
where we create a child object in the main pattern phase.
An example of this occurs in the java-bean pattern, where we turn an object into the public getter and setter methods.
<java-bean name="B">
<field name="f">
<method name="setF">
<method name="getF">
</application>
|
The pattern to create getters and setters runs as a main phase pattern.
For each field f, we create methods getF() and setF().
The pattern runs on the java-bean class, creating children of the class.
Remember that main phase patterns are processed bottom up.
So, by the time the main phase pattern on the java-bean is run, it has processed all its children.
This means that the java-bean will be marked as having completed its preparation and main phases -
it is not scheduled to do any more pattern processing for its children.
Result: both the preparation and main pattern phases will executed for the new method objects.
9.6.5 Generating Dependent Objects
|
The description above covers the natural flow of generating objects, where we generate 'dependent' objects from depended-on objects.
For example, the standard maintenance patterns fire patterns on the entity (the depended-on object) to generate corresponding data-views (which are dependent on the underlying entity).
However, there are occasional situations where you want to generate objects in the reverse direction.
For example, say we have a data-view DV and want to generate the entity E to give persistence via a table.
If you try this using regular patterns, a problem arises because the data-view is validated before the entity is created.
Because the data-view validates the existence of its entity object, the validation will fail ... just before the pattern that was to create it fired.
The answer is to create these objects using the 'extraIncludeSpec' file to run the pattern to create the entity.
This creates the entity in the preparation phase.
This means that by the time the validation on the data-view is done (in the main phase), the entity object will exist.
Creating dependent objects is better done in the extraIncludeSpec rather than the preIncludeSpec,
because the prevalidation on the object and all its children will have been done.
This makes the extraIncludeSpec easier to write;
if this work is done in the preIncludeSpec,
the pattern must protect itself against errors in the main object and any children involved in the pattern.
Note that the recursion rules described before mean that peer objects generated in 'extraIncludeSpec' patterns
will be processed for the preparation phase immediately ... because the treewalk on the parent has already been done.
This means that deliberate chains of 'extraIncludeSpec' patterns will work -
the first depended-on object will be created and then immediately fire its preparation phase which can also include an 'extraIncludeSpec' pattern to create a further peer object.
The ordering will probably be wrong - this is addressed by the reordering in the main phase.
9.6.6 A worked example
|
Assume we have the following
<application name="A">
<jar name="J">
<entity name="E1"/>
<entity name="E2"/>
<relation name="R"/> ## E1 <- -> E2
<session name="S">
<business-method name="BM">
<input-data-view name="E1DataView"/>
</business-method>
</session>
</jar>
</application>
|
We want to produce a data-view/relation tier from this, i.e.
E1 -> E1DataView
E1.A1 -> E1DataView.A1
E2 -> DV2
R -> DVR
Once created, the data-view field E1DataView.A1 knows its screen representation will need to link to a
particular lookup page for finding DV2's,
and so does the relationship R. Points to note:
- In the main pattern phase, initially both entity patterns fire,
creating the two dataviews and data-view fields, as well as the pattern on R to create DVR.
Because the newly-created objects are peers of the creating objects (all these are children of the jar),
the patterns on the data-view and data-view-relations do not fire immediately:
all the patterns for all the pre-existing peer objects (i.e. E1, E2, R) fire first.
- The data-view field E1DataView.A1 and the data-view relationship DVR both know that they
need a page for doing lookups on DV2 data-views.
DV2 does not know of the needs of its related objects, so it does not fire the pattern itself;
the goal is to not produce pages that are not used.
How can E1DataView.A1 and DVR coordinate to 'fire a pattern' on DV2 to create the page for it.
The issue here is that there is no mechanism to fire a pattern on another object.
The recommended design for this situation is as follows:
- The pattern should be defined below a 'data-view' -
so it will be located as though it were a pattern on a data-view.
In the example below, the parsed pattern's name is preceded by "data-view/"
which makes JeeWiz look for the include file as though it were a data-view's.
- The pattern should be parsed by the data-view-field and the relationship.
It needs to be a separate step in next-pattern chain,
because the output page will be put in a separate location of its own (beneath the ui-jar). For example
#set( $dv=$theApplication.getDataViewByName( "DV2" ) )
#parse( "data-view/generate-lookup-screen.inc" )
## this can form part of the sequence for the dvf or dvr, e.g...
#set( $this.nextPattern="nextForMySequence" )
|
This sets $dv to data-view object for DV2.
$dv is an implicit input to the data-view's generate-lookup-screen.inc:
it needs this because the pattern is not being run as a pattern on the appropriate data-view itself.
$this will be the data-view-field or the data-view-relation in this scenario.
- The lookup-screen pattern should
(a) be written to use the implicit input $dv rather than using $this as its context
(b) check that it hasn't already been generated
(c) lay out the pattern
(d) call setPatternRootObject - on the $this object! -
to make the page a child of the uijar.
For example
#if( !$dv.doneLookupPageGen )
#set( $dv.doneLookupPageGen = "true" )
<page name="${dv.effectiveName}Lookup" >
...
</page>
#set( $this.setPatternRootObject( $theUijar ) )
#end
|
 |
|
 |
Copyright (c) 2001-2008 New Technology/enterprise Ltd.
| |