Pax Reflector - Details

Traverse Rules

Currently, the traversing is limited to public fields and public accessor methods. Traversal can either happen member by member through;

public interface ReflectorService
{
    :
    Object getObject( Object container, String member )
        throws ReflectionException;
    :
}

which will return the object instance assigned as the member of the container. Example;

public class MyPojo
{
    private String m_Name;

    public MyPojo( String name )
    {
        m_Name = name;
    }

    public String getName()
    {
        return m_Name;
    }
}

// somewhere else
MyPojo pojo = new MyPojo( "Niclas" );
  :
String name = (String) reflector.getObject( pojo, "Name" );
 // name is now equal "Niclas".

But the Reflector also supports a cascaded dot-notation, so one can provide a single string and the Reflector will start at the root objects, pick its way through the instance graph and return the requested member.

String name = (String) reflector.getObject( "myroot.SomeMember.Another.MyPojo.Name" );

// is essentially the same as

String name = myroot.getSomeMember().getAnother().getMyPojo().getName();
Public Fields

The non-static and public member fields of a Class of a traversed object instance, will be available and settable.

Public Accessor Methods

The Reflector works around the JavaBeans model, as most fields in most applications are private and not accessible. The essentials are the famous getters and setters. A member Abc of type Defis said to be readable if there is a method of the following signature;public Def getAbc();And equally, the same member is said to be settable if there is a method of the following signature;public void setAbc( Def arg );Note that, the JavaBeans specification does not require that the Abc member actually exist as a stored value. It can be computed on each call.

Furthermore, the JavaBeans specification also allows clients to access arrays through methods. Example;

private Def[] m_Abcs;

public Def getAbc( int index )
{
    return m_Abcs[ index ];
}

public void setAbc( int index, Def item )
{
    m_Abcs[ index ] = item;
}

The intention is that the Reflector will comply with the above. Please study the JavaBeans specification for exact details and help us comply.
But, it does not stop there. The Reflector also supports pluggable TypeHandlers (see below), which allows special handling for any particular type. Array, Collection, Map and Dictionary types are provided for convient traversal and view. See the sections below for details.

The Root Objects

The Root Object is a concept the Reflector need to have an entry point into the traversing graph. You can register as many root objects you like, but too many can be detrimental if manual traversing is part of the usage (see Client - HTML below).

The RootObject is kept in a normal HashMap, so it is essential that any registered root objects are removed if no longer used and should be GCed. If you really can't keep track of the root objects, then add a WeakHashMap as an additional container, like;

// somewhere in initialization

reflector.addRootObject( "x", new WeakHashMap() );

// adding additional 'root objects'

public void addWeakRootObject( String name, Object obj )
{
    Map m = reflector.getMemberObject( "x" );
    m.put( name, obj );
}

The RootObject is given a name, and that name must be unique. The mechanism of how to make the root object unique is up to client adding the RootObject. For OSGi deployments (work not done yet), the Reflector bundle adds additional levels at the root level, so that root objects are clustered per bundle, for easier navigation.
Furthermore, root object names must only contain characters that is allowed in Java identifiers, i.e. java.lang.Character.isJavaIdentifierPart(char ch).

API - The ReflectorService interface

The ReflectorService API is fairly large and contains several access methods to the same thing, mainly for convenience reasons.

package org.ops4j.pax.reflector;

/** The ReflectorService allows you to traverse object instances in a running application.
 * <p>
 * The ReflectorService operates with a cascaded dot-notation to identify object instances in
 * running memory. That means that by providing a name such as "abc.Def.Rst.Name" the equivalent
 * of (provided no special TypeHandlers kick in);
 * <pre>
 *  abc.getDef().getRst().getName()
 * </pre>
 * is called, if the JavaBeans-styled accessor methods are found. Also, public member fields will
 * also be looked for (higer priority) and accessed when possible.
 * </p>
 * <p>
 * Examples of syntaxes, and equivalents in direct Java code.
 * <table>
 * <tr><th>dot notation</th><th>Java</th><th>Comment</th></tr>
 * <tr><td>someRoot.Abc</td><td>someRoot.getAbc()</td><td></td></tr>
 * <tr><td>someRoot.Abc.Def</td><td>someRoot.getAbc().getDef()</td><td></td></tr>
 * <tr><td>someRoot.Abc['0']</td><td>someRoot.getAbc()[0]</td><td>If getAbc() returns an Array.</td></tr>
 * <tr><td>someRoot.Abc['0']</td><td>someRoot.getAbc().getAt(0)</td><td>If getAbc() returns a Collection</td></tr>
 * <tr><td>someRoot.Abc['Def']</td><td>someRoot.getAbc().get("Def")</td><td>If getAbc() returns a Map or Dictionary.</td></tr>
 * <tr><td>someRoot.Abc['0'].Rst</td><td>someRoot.getAbc().getAt(0).getRst()</td><td>If getAbc() returns a Collection</td></tr>
 * <tr><td>someRoot.Abc['Def'].Rst</td><td>someRoot.getAbc().get("Def").getRst()</td><td>If getAbc() returns a Map or Dictionary.</td></tr>
 * <tr><td><i>empty string</i></td><td></td><td>Only valid for getMemberNames() which will return all root object names.</td></tr>
 * </table>
 * </p>
 */
public interface ReflectorService
{
    /** Returns the human readable form of the object instance traversed from the objectname argument.
     * @param membername The name of the object of interested.
     *
     * @return The toString() value of the member value object.
     */
    String getMember( String membername )
        throws ReflectionException;

    /** Returns the object of the member by the given membername.
     *
     * @param membername The name of the object to traverse to and return.
     *
     * @return The object that is returned as the value of the member, either by getter, fields or special
     *         handling, such as the value of entries of Maps and Collections.
     */
    Object getMemberObject( String membername )
        throws ReflectionException;

    /** Returns the object of the member by the given membername, starting at the provided container.
     *
     * @param membername The name of the object to traverse to and return.
     *
     * @return The object that is returned as the value of the member, either by getter, fields or special
     *         handling, such as the value of entries of Maps and Collections.
     */
    Object getMemberObject( Object container, String memberName )
        throws ReflectionException;

    /** Assigns the value to the member identified by the membername argument.
     *
     * @param membername The name of the member to be assigned a value.
     * @param value The value to be assigned to the member. For primitives, the value is converted according to
     *              normal Java rules, but additionally a fully-qualified classname can be given, in which case
     *              the reflector will try to instantiate such class through the default constructor, and if
     *              successful assign the instance to the member.
     */
    void setMember( String membername, String value )
        throws ReflectionException;

    /** Assigns the value to the member identified by the membername argument.
     *
     * @param membername The name of the member to be assigned a value.
     * @param value The value to be assigned to the member.
     * @throws ReflectionException If the value is not assignable to the member type.
     */
    void setMemberObject( String membername, Object value )
        throws ReflectionException;

    /** Assigns the value to the member identified by the membername starting at the
     *  container object.
     *
     * @param membername The name of the member to be assigned a value.
     * @param value The value to be assigned to the member.
     * @throws ReflectionException If the value is not assignable to the member type.
     */
    void setMemberObject( Object container, String memberName, Object object )
        throws ReflectionException;

    /** Returns all the membernames found in the member identified by membername argument.
     * <p>
     * If the membername is an empty string, all root object names are returned.
     * </p>
     * @param membername The name of the member to query for <b>its</b> members.
     *
     * @throws ReflectionException If the member identified by the membername does not exist.
     */
    String[] getMemberNames( String membername )
        throws ReflectionException;

    /** Returns all the membernames found in the object.
     *
     * @param object The Java instance to query for its members.
     *
     * @throws ReflectionException
     */
    String[] getMemberNames( Object object )
        throws ReflectionException;

    /** Returns the Class of the member identified by the membername argument.
     *
     * @param membername The name of the member to query for its Class.
     */
    Class getMemberClass( String membername )
        throws ReflectionException;

    /** Returns the Class of the member identified by the membername in the provided
     *  container.
     *
     * @param membername The name of the member of the container to query for its Class.
     */
    Class getMemberClass( Object container, String memberName )
        throws ReflectionException;

    /** Returns the name of the Class of the member identified by the membername argument.
     *
     * @param membername The name of the member to query for its Class.
     */
    String getMemberClassName( String membername )
        throws ReflectionException;

    /** Returns the name of the Class of the member identified by the membername in the provided
     *  container.
     *
     * @param membername The name of the member of the container to query for its Class.
     */
    String getMemberClassName( Object container, String memberName )
        throws ReflectionException;

    /** Checks with the member of the given membername can be set to a value.
     * @param membername The member to be tested if it can be set by a setMemberObject() method call.
     */
    boolean isMemberSettable( String membername )
        throws ReflectionException;

    /** Checks with the member of the given membername can be set to a value.
     * @param container The container from where we should test if a member can be set to a value, by calling the
     *                  setMemberObject() method.
     * @param membername The member in the provided container to be tested if it can be set by a setMemberObject()
     *                   method call.
     */
    boolean isMemberSettable( Object container, String membername )
        throws ReflectionException;

    /** Returns the container part of the membername.
     * <p>
     * This method must correctly return the container part of the full membername. For instance;
     * getContainerPart( "someRoot.Abc.Def" ), must return "someRoot.Abc",
     * getContainerPart( "someRoot.Abc['Def']" ), must return "someRoot.Abc", and
     * getContainerPart( "someRoot.Abc['Def'].Rst" ), must return "someRoot.Abc['Def']".
     * </p>
     * @param membername The membername to be worked upon.
     */
    String extractContainerPart( String membername )
        throws ReflectionException;

    /** Returns the Name part of the membername.
     * <p>
     * This method must correctly return the Name part of the full membername. For instance;
     * getContainerPart( "someRoot.Abc.Def" ), must return "Def",
     * getContainerPart( "someRoot.Abc['Def']" ), must return "['Def']", and
     * getContainerPart( "someRoot.Abc['Def'].Rst" ), must return "Rst".
     * </p>
     * @param membername The membername to be worked upon.
     */
    String extractNamePart( String membername )
        throws ReflectionException;

    /** Appends the name to the container name.
     *
     * @param container The actual container so that the type can be determined.
     * @param containerpart The name of the container.
     * @param namepart The name part to append to the containerpart.
     */
    String append( Object container, String containerpart, String namepart );

    /** Adds a <i>Root Object</i> to the Reflector instance.
     * <p>
     * The name must also be unique and no other root object may have been registered with the
     * same name.
     * </p>
     *
     * @param name The name of the <i>Root Object</i>. The name may only contain valid Java characters, as
     *             reported by Character.isJavaIdentifierPart(char ch).
     * @param object The object instance to be registered at the provided name.
     *
     * @throws IllegalArgumentException if the name is not valid, or if there has already been a root object
     *                                  registered with the same name previously.
     */
    void addRootObject( String name, Object object )
        throws IllegalArgumentException;

    /** Removes the root object with registered under the provided name.
     * <p>
     * Although this method removes the registration, the ReflectorService implementation must not keep
     * a strong reference to the registered object, so that if the registered object
     * has no other references, the registration will quitely be removed automatically.
     * </p>
     * <p>
     * If there is no root object registered under the provided name, this method will simply return.
     * </p>
     * @param name The name under which the root object was previously registered.
     */
    void removeRootObject( String name );
}

The APIis stateless, and the implementation must be thread-safe, although the objects that are navigated/traversed may not be. The ReflectorService is free to make parallel calls, and there is currently no mechanism to only expedite one call at a time.
Typically the client code does something like;

public class MyRoot
{
    private Friend m_myFriend;

    public Friend getMyFriend()
    {
        return m_myFriend;
    }
}

public class Friend
{
    public String name;
    public int    age;
}

// elsewhere....

String name = reflector.getMember( "someRoot.MyFriend.name" );

But it is also possible to not use Root objects at all, by

Friend friend = ....

String name = (String) reflector.getMemberObject( friend, "name" );

And likewise, you can set values;

Friend friend = ....

reflector.setMemberObject( friend, "name", "Niclas" );

reflector.setMemberObject( friend, "age", "41" );

// or

reflector.setMemberObject( "someRoot.MyFriend.name", "Niclas" );

reflector.setMemberObject( "someRoot.MyFriend.age", "41" );

reflector.setMemberObject( "someRoot.MyFriend.age", new Integer(41) );

The rest of the methods are fairly straight forward, and should not pose much problems to understand.

SPI - The ReflectorProvider interface

Extensions - TypeHandlers

TypeHandlers are an important concept in the Reflector, as it allows for special handling of any type. There are a few standard types that comes with the default distribution, but you can add your own.

Each request will pass through the TypeHandler for each container from the root all the way out to the leaf. That means that fairly complex types can be managed. For instance,

  • A Class does not conform to the JavaBeans programming model at all, but it contains information that is vital. By creating a TypeHandler for such class, such information can be exposed.
  • Each User has a login, which creates a context that is available to such user via thread local variables. A TypeHandler could be created to establish such context, so that each user's login session is exposed at the Root, and inside that session, one establish other entry points within the application, otherwise not reachable.
ArrayTypeHandler

The ArrayTypeHandler handles arrays of objects. When asked for the members, it will return a string array of ['n'], where n = 0..s and s = size of the array minus 1.

CollectionTypeHandler

The CollectionTypeHandler handles java.util.Collection instances. When asked for the members, it will return a string array of ['n'], where n = 0..s and s = size of the collection minus 1.

For java.util.List instances, modifications are done by a list.remove(thumbs down) followed by a list.add( n, o ), where n = 0..s and o = object to assign.

Collections are not really suitable for assignments and there are two possible solutions;

  1. Iterate to the nth element and remove it. Then add the new element to the the collection via c.add( o ).
  2. An iteration of elements 0..(n-1) performed and added to a result collection, then the nth iteration is skipped and the value to be assigned is added to the resultcollection, after which the iteration of the remain elements are continued. At the end, the original Collection is cleared followed by a c.addAll(result).

For the time being the first alternative has been chosen, but is expected to be changed prior to the 1.0 release.

MapTypeHandler

The MapTypeHandler handles instances of java.util.Map . When asked for the members, it will return a string array of ['name'], where name = each key. Setting the value of a member is simply a matter of a m.put( name, value ).

DictionaryTypeHandler

(see MapTypeHandler.) The DictionaryTypeHandler is required in the few cases where the subclass of java.util.Dictionary does not implement java.util.Map. Otherwise, the behaviour is identical.

ObjectTypeHandler

Clients - HTML viewer

The Pax Reflector also comes with one (at the moment) client which allows you to drill down the instance hierarchies from the root objects. If the application follows the JavaBeans model/spec, with public getters and setters, it is possible to get a very intimate view of the running application. See screenshot below.

Screenshot

It is also very easy to provide management support inside the application itself.

For instance, let's say we have something like;

public class MyFunkyHandler
{
    public void doSomethingImportant( OurFunkyArgument argument )
    {
        // DO
    }
}

public interface OurFunkyArgument
{
    // getters according to JavaBeans model
}

And instead of logging the call to the method and try to capture that somehowwe could instead do a very simple history buffer;

public class MyFunkyHandler
{
    private List m_history = new ArrayList();
    private int m_maxHistory = 10;

    public void doSomethingImportant( OurFunkyArgument argument )
    {
        manageHistory( argument );
         // DO

    }
    private void manageHistory( OurFunkyArgument arg )
    {
        m_history.add( arg );
        while( m_history.size() > m_maxHistory )
        {
            m_history.remove(0);
        }
    }
}

And we automatically get the 10 latest calls in a convenient, easy to find location. And by complementing it further with;

private int m_maxSize = 0;

public int getHistorySize()
{
    return m_maxSize;
}

public void setHistorySize( int size )
{
    m_maxSize = size;
}

And we suddenly have an history buffer that is normally empty, to occupy less memory, and can in an instance be set to an arbitrary size, available comfortably from the Reflector Html client.
We can continue down that path, and make the history buffer into a triggered one-shot buffer instead, so that it doesn't get overridden if analysis becomes difficult.

There are many other uses, such as enabling and disabling functionality, or changing the strategy by assigning a new instance of a delegation class.

Supported Deployment scenarios

Deployment in Servlet environments
Deployment in stand-alone J2SE environments
Deployment in OSGi environments
Deployment inside J2EE application servers

We have not yet looked at how the Reflector can be deployed inside various J2EE application servers, but we are confident that there are no serious obstacles of doing that. Each appserver will probably require its own little bootstrap code to get the Reflector in place. We hope that users can provide some feedback, and possibly code, over time on this subject.

Road Map ahead

There are still many things we want to add to the Reflector and invites people to help out. The Road Map of the Reflectorcontains updated details of which improvements, bugs and tasks that are scheduled for future versions. Feel free to add additional issues.