JTimber/Wrappers

From LoadingByte Wiki
Jump to navigation Jump to search

Sadly, JTimber doesn't support arrays and collections natively. Adding it to the runtime hook (bytecode manipulator) would be a rather tedious and inflexible solution. Therefore, JTimber also introduces the concept of wrappers in order to support arrays and collections in a very flexible way.

Problem

JTimber only supports parent tracking through direct references. For example, arrays and collections aren't supported natively. Moreover, any arrays and collections of a node are seen as its children, but not the elements of those arrays and collections. The following example illustrates the problems:

class Parent extends DefaultNode<Node<?>> {
    private Child object = new Child();
    private Child[] array = { new Child(), new Child() };

    // Getters and setters
}

class Child extends DefaultParentAware<Parent> {

}

public static void main(String[] args) {
    Parent parent = new Parent();
    Child childObject = parent.getObject();
    Child childArray = parent.getArray();

    // Parents
    assert childObject.getParents().get(0) == parent; // True
    assert childArray[0].getParents().get(0) == parent // False; childArray[0] has no parents

    // Children
    assert parent.getChildren().get(0) == childObject; // True
    assert parent.getChildren().get(1) == childArray[0]; // False; the second child of 'parent' is 'childArray', not 'childArray[0]'
}

Wrappers help solve these problems in a very flexible and expandable way.

Basic concept

Basically, a com.quartercode.jtimber.api.node.wrapper.Wrapper wraps around a certain object; for this example, we'll take a List. The node class, which wants to references some parent-aware objects through a list, then stores the wrapper instead of the original list implementation:

private List<Child> list = new ListWrapper<>(new ArrayList<Child>());

The wrapper itself is parent-aware. Therefore, the JTimber system notifies it of any parents. The list wrapper can then iterate through its list and delegate those parents to all elements. If a new element is added to the list, the wrapper adds its parents to the new item.

Apart from the parent-tracking passthrough functionality, each wrapper must implement the following method:

public List<Object> getActualChildren()

The method returns the objects the wrapper delegates its parents to. In case of our list example, this method would return all the elements of the wrapped list. Since you will probably never use this method explicitly, the exact method contract isn't discussed here. However, it can be extracted from the method's JavaDoc.

If you plan on implementing your own wrapper, make sure to first read through the source code of the built-in wrappers. You might also want to use the com.quartercode.jtimber.api.node.wrapper.AbstractWrapper class as the superclass of your new wrapper.

@SubstituteWithWrapper

There are special cases where both the wrapper and the wrapped object are instances of the field type. For example, the following one is such a case:

private List<Child> list = new ListWrapper<>(new ArrayList<Child>());

In this example List is the field type. Both the wrapper (ListWrapper) and the wrapped object (ArrayList) are instances of that field type.

In such a special case, you annotate the field with @SubstituteWithWrapper. That annotation causes any object which is stored in the field to be automatically wrapped in the set wrapper class. Note that this mechanism also works for JAXB unmarshalling.

For example, the following code would do the same thing as the code above (plus the JAXB unmarshalling bit, which isn't shown above):

@SubstituteWithWrapper (ListWrapper.class)
private List<Child> list = new ArrayList<>();

Wrapper constructor argument type

By default, @SubstituteWithWrapper selects the wrapper constructor which has one single argument of the field type. For example, for the listing above, JTimber would internally use the public ListWrapper(List) constructor every time the field is set. However, there are some rare special cases where the wrapper constructor argument type is not equal to the field type. For example, the following code is such a case:

@SubstituteWithWrapper (ListWrapper.class)
private Collection<Child> collection = new ArrayList<>();

This code would throw an exception because ListWrapper does not have a public ListWrapper(Collection) constructor (Collection is the field type). In such a case, you need to specify the wrapper constructor argument type manually. You can do that directly in the annotation:

@SubstituteWithWrapper (value = ListWrapper.class, wrapperConstructorArg = List.class)
private Collection<Child> collection = new ArrayList<>();

Built-in wrappers

JTimber comes with some built-in wrappers which cover your standard cases. Most users don't need to create their own wrappers since the built-in ones suffice for their requirements.

CollectionWrapper

The com.quartercode.jtimber.api.node.wrapper.collection.CollectionWrapper wraps around any Collection. It also implements the Collection interface in order to provide all collection methods.

Usage example:

@SubstituteWithWrapper (CollectionWrapper.class)
private Collection<String> collection = new ArrayList<>();

JAXB compatibility: Yes, with @SubstituteWithWrapper.

ListWrapper

The com.quartercode.jtimber.api.node.wrapper.collection.ListWrapper wraps around any List. It also implements the List interface in order to provide all list methods.

Usage example:

@SubstituteWithWrapper (ListWrapper.class)
private List<String> list = new ArrayList<>();

Note that the list wrapper extends the CollectionWrapper and inherits its functionality.

JAXB compatibility: Yes, with @SubstituteWithWrapper.

SetWrapper

The com.quartercode.jtimber.api.node.wrapper.collection.SetWrapper wraps around any Set. It also implements the Set interface in order to provide all list methods.

Usage example:

@SubstituteWithWrapper (SetWrapper.class)
private Set<String> list = new HashSet<>();

Note that the list wrapper extends the CollectionWrapper and inherits its functionality.

JAXB compatibility: Yes, with @SubstituteWithWrapper.

QueueWrapper

The com.quartercode.jtimber.api.node.wrapper.collection.QueueWrapper wraps around any Queue. It also implements the Queue interface in order to provide all list methods.

Usage example:

@SubstituteWithWrapper (QueueWrapper.class)
private Queue<String> list = new LinkedList<>();

Note that the list wrapper extends the CollectionWrapper and inherits its functionality.

JAXB compatibility: Yes, with @SubstituteWithWrapper.

ArrayWrapper

The com.quartercode.jtimber.api.node.wrapper.collection.ArrayWrapper wraps around any one-dimensional array. It implements some custom methods which allow access to the underlying array. However, the initial array is cloned in order to prevent the user from manually modifying it.

The custom methods offered by the array wrapper are the following (where E is the generic parameter defining the element type):

public int length()

public E get(int index)
public void set(int index, E value)

public E[] cloneArray() // Returns a shallow clone of the wrapped array

Usage example:

private ArrayWrapper<String> array = new ArrayWrapper<>(new String[10]);

public static void main(String[] args) {
    array.set(0, "abc");
    array.set(3, "def");

    assert array.get(0).equals("abc");
}

Warning: Theoretically, you could access the internal array through the getInternallyWrapped() method. Don't do that! If you modify the returned array in any way, parent inconsistencies will probably arise!

JAXB compatibility: No, because no simple way, which doesn't leave a trace in the produced XML, is in sight.