Section 7.3. REFLECTIVE ADAPTIVE PROGRAMMING WITH DJ


7.3. REFLECTIVE ADAPTIVE PROGRAMMING WITH DJ

Consider the processing of XML schema definitions [6]. Listing 7-1 is an example of an XML schema taken from an aspect-oriented web development system [14]. The schema consists of a sequence of items. Some are type definitions, and some are declarations. Verifying that all types used in the schema are either standard or locally defined is a typical consistency check.

Listing 7-1. An example of an XML schema
 <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"             xmlns:ddd="http://webjinn.org">  <xsd:element name="structure">   <xsd:complexType>    <xsd:sequence>     <xsd:element name="fields" type="Fields"/>     <xsd:element name="comment" type="xsd:string"/>    </xsd:sequence>   </xsd:complexType>  </xsd:element>   <xsd:complexType name="Fields">   <xsd:sequence>    <xsd:element name="field" type="Field"/>   </xsd:sequence>  </xsd:complexType>   <xsd:complexType name="Field">   <xsd:sequence>    <xsd:element name="attribute" type="Attribute"/>   </xsd:sequence>   <xsd:attribute name="name" type="xsd:NMTOKEN"/>  </xsd:complexType>  <xsd:complexType name="Attribute">   <xsd:attribute name="name" type="xsd:NMTOKEN"/>   <xsd:attribute name="value" type="xsd:string"/>  </xsd:complexType> </xsd:schema> 

Figure 7-2 shows a UML class diagram that represents a small subset of the XML schema definition language. A simple algorithm for checking a schema for undefined types involves two traversals of the object structure representing the schema definition: one to collect all the types defined in the schema, and another to check each type reference to see if it is in the set of defined types.

Figure 7-2. Class hierarchy for representing XML schema.


The actual traversal behavior, however, is quite complex. The functional behavior must be split between the Schema, the Attribute, and the other classes in order to comply with the Law of Demeter. Moreover, deciding whether to traverse a field requires knowledge of the overall class structure: for example, in SequenceGroup, the finding type definitions traversal method only needs to traverse the edecls field because an ElementDecl element declaration may include a TypeDef type definition; if the object model were extended so that an AttributeDecl attribute declaration could also include a type definition, the traversal method in ComplexType would have to be changed to traverse the adecls field, even though nothing about ComplexType itself changed.

DJ is a library of classes that make traversals like collecting all defined types and verifying all type references much easier to define, understand, and maintain. Listing 7-2 shows an implementation of the Schema class that defines the two traversals succinctly using the ClassGraph and Visitor classes from the dj package.

A ClassGraph instance is a simplified representation of a UML [2] class diagram; its nodes are types (classes and primitive types), and its edges are (unidirectional) associations and (bi-directional) generalizations. The default ClassGraph constructor builds a graph object using reflection from all the classes in the default package; a string containing a package name can be provided as a constructor argument to build a class graph from another package.

Calling the traverse method on a ClassGraph object does a traversal. It takes three arguments: the root of the object structure to be traversed; a string specifying the traversal strategy to be used; and an adaptive visitor object describing what to do at points in the traversal. A traversal strategy specifies the end points of the traversal, using the from keyword for the source and the to keyword for the target objects. In between, any number of constraints can be specified with via or bypassing.

Listing 7-2. Using traverse from the DJ library
 import edu.neu.ccs.demeter.dj.ClassGraph; import edu.neu.ccs.demeter.dj.Visitor; class Schema {   Attribute attrs[];   SchemaItem items[];   static final ClassGraph cg = new ClassGraph();   public Set getDefinedTypeNames() {     final Set def = new HashSet();     cg.traverse(       this,       "from Schema via ->TypeDef,attrs,* to Attribute",       new Visitor() {         void before(Attribute host) {           if (host.name.equals("name"))             def.add(host.value);         }       });     return def;   }   public Set getUndefinedTypeNames() {     final Set def = getDefinedTypeNames();     final Set undef = new HashSet();     cg.traverse(       this,       "from Schema via ->Decl,attrs,* to Attribute",       new Visitor() {         void before(Attribute host) {           if (host.name.equals("type")               && !def.contains(host.value))             undef.add(host.value);         }       });     return undef;   } } 

The two traversals in Listing 7-2 traverse from Schema to Attribute; in other words, they visit attributes in a schema because type names appear in attribute values for both definitions and references. They differ in their constraints: to find the names of types defined by the schema, the first traversal looks only at attributes of type definitions (TypeDef objects); to find the names of types referenced by the schema, the second traversal looks only at attributes of declarations (Decl objects). The ->TypeDef,attrs,* syntax is a pattern specifying the set of association edges whose source is class TypeDef and whose label (field name) is attrs; the asterisk means that an edge in the set can have any target type.

Traversals of object structures are done efficiently [22]: For each object in the traversal, associations that can possibly lead to a target object are traversed sequentially (including inherited associations, subject to any constraints specified in the traversal strategy). If an object that has no possible path leading to a target object is encountered, the traversal omits searching it. For example, in the XML schema example, the items field of schema contains an array of SchemaItem objects; this array may contain TypeDef objects; since TypeDef is a subclass of SchemaItem, the elements of the array are traversed as part of the getdefinedTypeNames traversal. However, some of the elements may be AttributeDecl objects. There is no possible path to a TypeDef object. If one of these elements is encountered in the array, it is skipped. The adecls field of ComplexType is never traversed since it can only contain an array of AttributeDecl objects. Note that if the adecls field were a vector instead of an array, it could contain objects of any type, and so DJ would have to traverse it in case one of its elements were a TypeDef object or some other object that could lead to a TypeDef. Using the parametric polymorphism of Java 1.5 [3], this problem can be avoided: the type of adecls could be List<AttributeDecl>, and DJ would know it could avoid it.

The anonymous inner visitor class is a subtype of the Visitor class in the dj package. During a traversal with a visitor of type V, when an object o of type T is reached in the traversal, if there is a method on V named before whose parameter is type T, that method is called with o as the argument. Then, each field on the object is traversed if needed. Finally, before returning to the previous object, if there is a method on V named after whose parameter is type T, that method is called with o as the argument. The Visitor subclasses defined inline in Listing 7-3 only define one before method each, which is executed at Attribute objects, the end point of the traversal.

DJ also provides support for generic programming [28]: The asList method on ClassGraph adapts an object structure and a traversal strategy into a List, part of Java's Collections framework [10]. The object structure is viewed as a collection of objects whose type is the target of the traversal strategy; the collection's iterator performs the traversal incrementally with each call to next. Listing 7-3 shows how to rewrite the previous example using asList.

Listing 7-3. Using the collection adaptor asList
 import edu.neu.ccs.demeter.dj.*; class Schema {   Attribute attrs[];   SchemaItem items[];   static final ClassGraph cg = new ClassGraph();   public Set getDefinedTypeNames() {     final Set def = new HashSet();     List typeDefAttributes =       cg.asList(         this,         "from Schema via ->TypeDef,attrs,* to Attribute");     Iterator it =      typeDefAttributes.iterator();     while (it.hasNext()) {       Attribute attr = (Attribute) it.next();       if (attr.name.equals("name"))       def.add(attr.value);     }     return def;   }   public Set getUndefinedTypeNames() {     final Set def = getDefinedTypeNames();     final Set undef = new HashSet();     List declAttributes =       cg.asList(         this,         "from Schema via ->Decl,attrs,* to Attribute");     Iterator it = declAttributes.iterator();     while (it.hasNext()) {       Attribute attr = (Attribute) it.next();       if (attr.name.equals("type")           && !def.contains(attr.value))         undef.add(attr.value);     }     return undef;   } } 

DJ also has edge visitor methods that get executed whenever certain edges in the object graph are traversed. (So far, in our examples, visitor method execution depended only on the class of the object being traversed.)

An edge has the form n_l(source, target) or n(source, l, target). In the first case, the name of the part-of edge is fixed (l); in the second case, it is variable. n is either before or after. around is only available in an alpha version of DJ.

For a part-of edge, the signatures are matched in the order shown in Listing 7-4, where n starts with before or after. S is the source type of the edge, l is the label of the edge, and T is the target type of the edge. For example, if an edge ->Employee, salary, Currency is traversed, then first the visitor method before_salary (Employee, Currency) is invoked if it exists, followed by before_salary (Employee,Object), etc.

Listing 7-4. The order of signature matching
 1.  n_l(S, T) 2.  n_l(S, Object) 3.  n_l(Object, T) 4.  n_l(Object, Object) 5.  n(S, String, T) 6.  n(S, String, Object) 7.  n(Object, String, T) 8.  n(Object, String, Object) 

From an AOP perspective, expressions 1 through 8 in Listing 7-4 are pointcut designators for execution join points of traversals. For example, n_l(S, Object) selects all join points during a traversal where we traverse a part-of edge called l starting from an object of type S (bound in the first argument) and going to any kind of object (bound to the second argument).

DJ pointcut designators can only select join points in traversals, while AspectJ has a much richer set of join points [1]. The DJ pointcut designators can be simulated in AspectJ using pointcuts such as this, target, args, and call.

7.3.1. Implementation Highlights

In this section, we present some highlights of the implementation of DJ and some examples of interesting uses. When the ClassGraph constructor is called, it creates a graph object containing reflective information about all the classes in a package. However, in Java, there is no way to get a list of all classes in a package; packages are just namespaces, not containers. Moreover, the JVM only knows about classes that have already been loaded, and it only loads classes when they are referenced. Since a class graph might be constructed before many of the classes in the package have been referenced, the constructor has to discover classes some other way: it searches the class path (provided by the JVM as System.getProperty("java.class. path")) for all .class files in subdirectories corresponding to the package name. For each class file that is found, it calls Class.forName() with the class name, which causes the JVM to load the class if it hasn't already been loaded. If there are classes that need to be added to a class graph that do not exist as .class files in the class path (for example if they are loaded from the network or constructed dynamically), they must be added explicitly by calling addClass().

A class graph may also be created from another class graph and a traversal strategy, forming the subgraph of classes and edges that would be traversed according to that pair. This can be used to remove unwanted paths from a class graph, such as backlinks, rather than having to add bypassing constraints to every traversal strategy.

The traverse method on ClassGraph is implemented in a two-stage process. First, a traversal graph is computed from the class graph and the traversal strategy (which itself is converted into a strategy graph, whose nodes are the classes mentioned in the traversal strategy and whose edges each have constraints attached to that leg of the traversal); then the object structure is traversed, using information from the traversal graph to decide where to go next at each step, and visitor methods are invoked as needed. The traversal graph computation takes time proportional to the product of the number of edges in the class graph and the number of edges in the strategy graph; since the same traversal strategy is often reused multiple times with the same class graph, the traversal graph can be saved and reused without needing to be recomputed every time. The class traversalGraph has a constructor that takes a traversal strategy and a ClassGraph object, as well as methods TRaverse and asList. The traversal computation algorithm is also available as a separate package, the AP Library [21].

At each step in a traversal, the fields and methods of the current object, as well as methods on the visitor object, are inspected and invoked by reflection. Some of this reflective overhead could be avoided by generating a new class (at runtime) that invokes the appropriate fields and methods directly; this is planned for a future addition to DJ. Applications of pluggable reflection [27] combined with partial evaluation to speed up the traversal may be possible as well. The implementation of asList is somewhat trickier than regular traversal: the list iterator must return in the middle of the traversal whenever a target object is reached and then resume where it left off when called again. An earlier version created an ad-hoc continuation-like object that was saved and restored at each iteration, but this was error-prone and not very efficient; the current version uses a separate Java thread as a coroutine, suspending and resuming at each iteration. An additional method, gather, can be used to copy all the target objects into an ArrayList. This is faster still, but the list returned by asList has the advantage that calls to set on the iterator can replace target objects in the original object structure.

Java's reflection system, unlike many other meta-object protocols [11], has no mechanism for intercession: there is no way to make a new subclass of Class that behaves differently for certain meta-operations such as method invocation [27]. However, DJ's Visitor class does allow a limited form of intercession. It has the method before(Object obj, Class cl) (and corresponding after), which is invoked by the ClassGraph.traverse method at each traversal step. It looks for a method named before with a single parameter whose type is the class represented by cl and invokes it with obj as argument. This method can be overridden by a subclass to perform more dynamic behavior based on the reified class object of the object being traversed.



Aspect-Oriented Software Development
Aspect-Oriented Software Development with Use Cases
ISBN: 0321268881
EAN: 2147483647
Year: 2003
Pages: 307

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net