1
0
0
10,858

Annotation-Driven Object Auto-Formatting

Annotation-Driven Object Auto-Formatting Using Reflection to Implement the toString() Method

Abstract

ObjectPrinter I present the use and implementation of annotation-driven auto-formatting for Java Objects. A reflection-based iteration of the fields and properties of a Java class is used to build a model describing the details how instances of the class should be formatted for printing. Annotation-based meta-data is used to tailor the details how that model is build. A registry of Class models is used to instruct the formatting of each instance, possibly connected in an object graph. The annotation-based meta-data provides information about a variety of details for deciding how to do the formatting, including: which fields or properties to ignore and for which values, which markers and separators to use, which fields/properties are optional and selection of a projection to include and exclude certain fields/properties. Configuration and user of custom Printers for specific field/property or Class, or an overall printing service is also discussed.

Introduction

Printing a text representation of objects is one of the most mundane things a software developer will ever do. This is done in many contexts, such: outputting messages to log, generating a text based report, rendering application state and data to the end-user, and simply have "sanity feedback" while testing. Mundane means that it has to be done very often, not that is not important. So having an effective, flexible, and maintainable way to do this is relevant for a developers work.

A common approach to printing including in Object-Oriented languages, such as Java and many others, is to have a well-defined method such as toString() to return a string representation of an object. Classes implement this method and client objects call it to get the string value, describing the identity and/or state of the object to print. In Java and other languages this method is commonly recognized by the language and run-time, so the string concatenation operator automatically call it to get the text representation of an object. This makes printing objects very simple and concise.

Although the run-time provides a default implementation for the toString() method, this is often not enough since developer want to have fine grain control what is printed in each particular use case. Effective object printing requires the developer to reimplement the toString() method, which easly become laborious work as the number of fields or properties in classes, and the number of classes increases with the complexity of the project. Doing this is a simple-minded manual way is also not refactor friendly - each time the set of field or properties of a class changes in a relevant way the toString() method needs to be reimplemented. Dealing with class inheritance can also be tricky and time-consuming.

There are 3 commonly used approaches to implement a toString() method:

  • Explicit - The developer concatenates the value, and possibly the names, of relevant fields or properties, taking care for consistent use of mark and separator characters
  • Builder - A builder class is used to assist the implementation. Fields or properties to include in printing still need to be specified explicitly, but without the assle of dealing with the consistent use of marker, separator, or object signatures.
  • Code-Generation - The developer asks a tool (IDE or RAD) to generate the toString() method automatically by inspecting the definition of the class at the time of the request.

All these approaches have the fundamental limitation that the toString() method needs to be reimplemented or regenerated every time the class changes in a relevant way. In principle, with the code-generation approach we could have a very sophisticated IDE that keeps the implementation of the toString() in tune with the actual code definition. However, this is complicated to implement and hard to tailor, so IDE usually don't have this feature.

An alternative approach to object printing is to use an automated procedure that inspects the class definition and object state to produce the output. Tools, such as IDE, debugs, GUI editors, RAD tools, among other, do often employ this approach to provide feedback about object state. This is very useful, but does have exactly the same set of use case than the toString(). What we would like to have is a library that provides this kind of service for easy use inside the application and as part of its logic. Ideally, there should be an easy way to customize the textual representation of objects produced by this library.

In this article, I present the use, design, and implementation of a Java library that auto-formats (pretty-prints) objects. A small number of annotations with a rich set of attributes is used to customize the detailed formatting of individual fields and the overall object. Using the library is a simply as calling an utility method FormatUtil.toString(this). Some of the features supported by the library, include:

  • Very simple and powerful API
  • Use of @Format annotation to control the details how an object field or property should be printed
  • Control over which fields/properties to print and when, including: pre-defined exclusion of fields, exclusion based on value, and optional fields printed only on demand
  • Associate logging levels with fields
  • Rich definition of projections to specify which fields/properties and paths to render in each use case
  • Use of property paths for fine-grain control of what to be printed in the object tree
  • Field and property access levels
  • Recurse on complex objects in an object-tree
  • Use of a ConversionService to format (simple) fields/properties
  • Customization of overall object formatting with annotation @TypeFormat
  • Customization of formatting for collection with annotation @CollectionFormat
  • Optional use of text transforms on field values and/or labels
  • Support for i18n (internationalization)
  • Detection and marking of cycles in the object-tree
  • Integration with dependency-injection frameworks to lookup custom printers and text transforms specified with annotation @Ref
  • Bridging with conversion/formatting sub-system in other frameworks

The auto-formatting services presented in this article are part of more encompassing library that uses Java reflection to accomplish a variety of related services. So many of the features available for auto-formatting (e.g. projections), are also available in other services. Some of these services, include:

  • Object auto-formatting/pretty-printing (described in this article)
  • Object validation (e.g. used in form processing in webapps)
  • Object binding (typically used in MVC webapps)
  • Object mapping (automated deep clone and deep assignment between objects)
  • Object comparison (automated deep comparison of objects)
  • Properties to/from POJO mapping
  • Java Simplified meta-level abstractions (based on a small number of concepts such as MetaClass,Property, ObjectReference)

This library can be used standalone, or in integration with other open-source frameworks. It is also used a foundational core for the implementation of Innovations Framework - an Java enterprise framework for the development of enterprise-graded web-applications. It can be used standalone, or integration with other popular technologies such as JEE-standard compliant, Spring Framework, and other JVM-based frameworks.

Using Auto-Formatting

To give an overall sense how object auto-formatting can be used, I start by showing and explaining a simple example and progressively add more advanced options and use-cases

Simple Example - "Alice Meets Bob"

Consider the class Account shown below -- modeling a registered user:

import org.einnovator.format.Format;
import org.einnovator.format.FormatUtil;

public class Account {

	private Long id;

	private String username;
        
	@Format(false)
	private String password;

	@Format(label="name", optional=true)
	private String fullName;

	@Format(recurse=true)
	private Address address;

	@Format(optional=true)
	private String about;

	private Date birthday;

	//Constructors

 	public Account(Long id, String username) {
		this.id = id;
		this.username = username;
	}

	//Getters and Setters
	//...

	public String toString() {
		return FormatUtil.toString(this);
	}
}

In this version, class Account has a few simple fields: id, username, password, fullName, and a reference to a complex object of type Address, itself assumed to have some additional fields such as: street, city and country. Notice also the implementation of the toString() method, that call the utility function FormatUtil.toString(this). This method delegates the work of producing a text representation of the object to a singleton of type ObjectPrinter, that builds a meta-model for the class Account and uses that meta-model to guide the formatting of all instances of class Account. The annotation @Format is used to tailor how and which fields should be formatted.

Let's create and initialize an Account object for user "Alice" and print it:

Account account = new Account(1L, "Alice");
account.setPassword("alicePASS12$");
account.setFullName("Alice Wonder");
account.setAddress(new Address(1L, "London", "UK"));
System.out.println(account);

The following output is shown:

Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"))

The simple fields id and username are output, while the field password is ignored because of annotation @Format(false). Field birthday is also ignore because it was not initialized and the value its null. By default null values fields are not output. The complex field address is also printed with its non-null fields. Field fullName was not output because it is defined as optional with @Format(optional=true). This means that it rendered only when explicitly asked to with a Projection, as follows:

System.out.println(FormatUtil.toString(account, false, "fullName"));

Which outputs:

Account(id=1, username="Alice", name="Alice Wonder", address=Address(id=1, city="London", country="UK"))

Method FormatUtil.toString() is overloaded, and in the above use a projection is defined in a simplified way by enumerating a variable number of fields or properties to include in the formatting. The boolean parameter set to false is used to specify that the projection is not complete, and all other fields should normally printed should also be printed with this projection. Notice also that the label for the property fullName is name, because it was explicitly defined with @Format(label="name").

It also possible to restrict the set of fields printed in a explicitly way, as follow:

System.out.println(FormatUtil.toString(account, false, "id", "username", "fullName"));

Which outputs:

Account(id=1, username="Alice", name="Alice Wonder")

The name of fields in projections are actually property paths. This allows fine-grained control on what to be printed in an object tree. By default, when printing the value of a complex typed, such as Address, the method toString() for that method is called which is not knowledge about the projection being used. To override this behavior the option @Format(recurse=true) can be used. This allows property paths to be used, as follow:

System.out.println(FormatUtil.toString(account, false, "id", "username", "fullName", "address.city"));

Which outputs:

Account(id=1, username="Alice", name="Alice Wonder", address=Address(city="London"))

Omitting Values

In classes with many fields or properties, outputting the value of all the output of all them can be very verbose. There are tree approaches that can be used to reduce the amount of outputted information:

  • Use @Format(false) to exclude a field/properties at all times, Recommended when the value of this field is not relevant (e.g. support/transient field), produces too much information (a possible very long array), or hide cycles in the object graph (e.g. tree-like parent-child relations or object graphs with reference in both directions).
  • Make a field/property optional @Format(optional=true) and/or use projections to specify that the field should be printed only when explicitly or implicitly asked
  • Specify that a field/property should be omitted when the value provides little information, such as being null, empty (size/length 0), or has a default value (e.g. zero for numeric, and false for boolean)

By default a field/property is ignored if the value is null. This can be changed with.@Format(ignoreNull=true). Empty arrays or collection are still output by default. To change this use @Format(ignoreEmpty=true). To ignore fields/properties with currently a default value use @Format(ignoreDefault=true). When the annotation @Format is applied at the type level, its semantics applies to all field/properties, unless a @Format annotation is also used at the field/property level.

Let's add a int field to the class Account to illustrate how a field can be ignored when it matches the default value, and an empty collection:

import org.einnovator.format.Format;
import org.einnovator.meta.Ref;
import org.einnovator.text.transforms.EllipsisTransform;

public class Account {
	...
	@Format(ignoreDefault=true)
	private int reputation;

	@Format(ignoreEmpty=true)
        List<String> titles = new ArrayList<String>();
	...
}

If we don't initialize field reputation it stays with default value 0, and therefore is not printed. However, if we set the value different then 0 it is printed. Below, the first output line does not show the field reputation, while the second output line show the field value after being set different then 0. Note also that field titles is not printed since its length is zero.

Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"))
Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"), reputation=33)

Logging Levels

Fields can also be defined with an associated logging level. This allow printing of fields to be conditional on specifying a logging level low-enough to capture the field. This is shown below:

import org.einnovator.format.Format;
import org.einnovator.meta.Ref;
import org.einnovator.text.transforms.EllipsisTransform;

@Format(level=Level.INFO)
public class Account {
	...

	@Format(recurse=true, level=Level.DEBUG)	
	private Address address;
	...
}

Note that a default logging level INFO was defined for the fields in the class, while field address is set to be printed only if the logging level is DEBUG or higher. To specify a logging level for printing the overloaded version of FormatUtil.toString() that takes a logging Level as parameter can be used:

System.out.println(FormatUtil.toString(account, Level.INFO));
System.out.println(FormatUtil.toString(account, Level.DEBUG));

Which output the field Account.address in the second call with level DEBUG not to in the first call with level INFO:

Account(id=1, username="Alice")
Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"))

Adding Text Transforms

The value of text (or other) fields is often not adequate for direct printing, and yet some information about the field value is required. For example, a property with a very long descriptive text may be summarized to show only the initial part of the description. Let's add a field Account.about to illustrate this:

import org.einnovator.format.Format;
import org.einnovator.meta.Ref;
import org.einnovator.text.transforms.EllipsisTransform;

public class Account {

	...
	static class AboutTransform extends EllipsisTransform {
		public AboutTransform() {
			super(18);
		}
	}
	
	@Format(transform=@Ref(AboutTransform.class))
	private String about;
	...
}

Notice that the field about specifies a text transform should be used in formatting with @Format(transform=@Ref(..)). A custom transform AboutTransform, extending a built-in transform is used. Let's set the value of the about property of Alice's Account, and print it.

account.setAbout("Something about Alice!");
System.out.println(account);

Which outputs:

Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"), about="Something about...")

The annotation @Ref is the mechanism used to reference an object or bean. Since here we are not using (yet) a bean management infrastructure - also know as dependency-injection framework - the object is simply created by calling the no-args constructor on the specified class. Bean referencing can also be made more sophisticated with additional attributes of annotation @Ref. At the end of this article, I discuss how the auto-formatting services provided by the ObjectPrinter class are integrated with dependency injection frameworks, such as Spring Framework, JEE-CDI, and Innovations Framework DefaultBeanManager.

Rather than using an out-of-the-box text transform, one might prefer to use a custom implementation of interface TextTransform, as shown below:

static class AboutTransform2 implements TextTransform {
	int MAX_LENGTH = 20;
	
	@Override
	public String transform(String text) {
		return text.length()<MAX_LENGTH ? text : text.substring(0, MAX_LENGTH) + "...";
	}		
}

The labels for fields or properties can also be transformed. Below, show how this can be done at for all the fields/properties in the class Account by using the annotation @Format at the type level:

import org.einnovator.format.Format;
import org.einnovator.meta.Ref;
import org.einnovator.text.transforms.CompositeTextTransform;
import org.einnovator.text.transforms.CamelCaseToWordsTextTransform;
import org.einnovator.text.transforms.CapsTextTransform;

@Format(labelTransform=@Ref(Account.MyLabelTransform.class))
public class Account {
	
	static class MyLabelTransform extends CompositeTextTransform {
		public MyLabelTransform() {
			super(new CamelCaseToWordsTextTransform(), new CapsTextTransform(true));
		}
	}
	...
	private Color favoriteColor;
	...
}

I define custom text transform MyLabelTransform as a composite transform that chains two built-in TextTranforms. I also added the camel-cased multi-word field favoriteColor to class Account to better illustrate the effect of the custom label transform above. Let's also initialize the field with a non-null value to see the effect of the type-level label transform:

...
account.setFavoriteColor(new Color(255, 0, 0));
System.out.println(account);

Which outputs (newline added just to formatting in this article, not actually output by the ObjectPrinter):

Account(Id=1, Username="Alice", Address=Address(id=1, city="London", country="UK"),
About="Something about...", Favorite Color=java.awt.Color[r=255,g=0,b=0])

Note that field names are capitalized and the camel-case multi-word field favoriteColor is output as space-separated words.

Tailoring the "Look&Fell"

The mechanisms and configuration options show-cased so far illustrated some of the many ways by which developers can control the way objects are printed by ObjectPrinter. The overall output, though, has about the same flavor to it seams the same separator and marker character are being output all along. The separators and markers can also be customized by using type-level annotation @TypeFormat. Below it is used to produce a more "JSON like" formatting:

import org.einnovator.format.Format;

@TypeFormat(beginMarker="{", endMarker="}", valueSeparator=":", fieldSeparator=", ")
public class Account {
	...
}

@TypeFormat(beginMarker="{", endMarker="}", valueSeparator=":", fieldSeparator=", ")
public class Address {
	...
}

Which outputs: (setting the fields fullName and dateOfBirthday back to null, for brievity)

Account{id:1, username:"Alice", address:Address{id:1, city:"London", country:"UK"}}

The name of the attributes of annotations @TypeFormat are self explanatory. For example, @TypeFormat.valueSeparator() specifies the string separating the field (or property) name/label from the actual value. Notice that although the output is "JSON like" is not really JSON - as JSON does not have an explicit notions of custom domain types, and therefore the prefixes with class names are not used. (For producing JSON and XML data representations from JAVA objects - marshaling and marshaling - while still keeping many of the mechanisms and configuration options available for ObjectPrinter, a different module(s) of Innovations Framework should be used.)

Notice also that here are using the annotation with both domain classes Account and Address. It also possible to change the default configuration of ObjectPrinter so that particular settings are applied to all classes. This is done by initializing a FormatOptions object and passing it as constructor argument to ObjectPrinter.

FormatOptions options = FormatOptions.newInstance().valueSeparator(":").beginMarker("{").endMarker("}");
ObjectPrinter printer = new ObjectPrinter();
System.out.println(printer.print(account));

The static utility method FormatOptions.newInstance() is used create an instance of FormatOptions initialized with sensible defaults. FormatOptions also supports a "fluent API" for easy configuration using chained property setting methods.

A customized ObjectPrinter can also be set as a "system-wide" default by calling method FormatUtil.setPrinter(). In this case, all calls to FormatUtil.toString() will use that customized ObjectPrinter. However, this should be done wisely as FormatUtil class keeps the printer a singleton in a static field. So all the cavets applicable to non-final static fields, apply here as well.

Formatting with ConversionService

ObjectPrinter works by iterating over all the properties in the meta-model build for a class, and applies that iteration recursively for fields which are not of a simple type. The way the value of simple-type properties is printed is configurable by a formatting-conversion service modeled by interface ConversionService.

The method ConversionService.format(Object, Locale) is called by the ObjectPrinter to print values of a simple type. The method ConversionService.supportFormat(Object) is also called to check if a property should be considered a simple type in the first place. If the ConversionService supports the formatting of type, that ObjectPrinter simply calls the ConversionService rather than recursing on the properties of the class. Let's use a customized ConversionService to print the date-typed fields with pattern "dd-MM-yyyy":

DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addFormatter(new DateTimeFormatter("dd-MM-yyyy"));
ObjectPrinter printer = new ObjectPrinter(FormatOptions.newInstance().conversionService(conversionService));

System.out.println(printer.print(account));

Which outputs (setting the field birthday and favoriteColor to non-null):

Account(id=1, username="Alice", address=Address(id=1, city="London", country="UK"), 
birthday=04-12-2013, favoriteColor=#ff0000)

Notice that a formater for type color was automatically added by the DefaultConversionService.

For developers with knowledge of Spring Framework, the concept of ConversionService should be familiar. While the details of the API and implementation are different in Innovations Framework core library and Spring Framework, the abstraction is very similar. Integration adapters are available to use Spring ConversionService with class ObjectPrinter and other components, and vice-verse.

i18n - Internationalization

Projections

I showed that the set of fields/properties of an object that are printed can be controlled by specifying additional projection parameters to the FormatUtil.toString() method. A projection can also be specified in field/properties with annotation @Projection() as value of attribute Format.projection(). This is used to control set of field/properties properties that are printed for an associated object. This is illustrated below in field Account.partner:

import org.einnovator.meta.Projection;
...

public class Account {

	...
	@Format(projection=@Projection({"id", "username"}))
	private Account partner;
	...
}

Note that field Account.partner is annotated with @Format and that the attribute projection is set with another annotation @Projection. In this case, it is specified that only fields Account.id and Account.username should be printed when recursing on the field Account.partner.

Let's initialize the value of this field for the Account of "Alice", to see resulting output:

Account account = new Account(1L, "Alice", new Color(255,0,0));
account.setPartner(new Account(1L, "Bob", new Color(0, 255, 0)));
System.out.println(account);

Which outputs (assuming a ConversionService is setup to format color):

Account(id=1, username="Alice", favoriteColor=#ff0000, partner=Account(id=1, username="Bob"))

Notice that while the field favoriteColor is setup printed for "Alice", for "Bob" only the id and username are printed. This is because of the default projection we setup for field partner.

Annotation-Driven Projections

Rather than specify a projection by specifying the name of the fields/properties to print, it is also possible to use annotations to organize fields/properties in groups and request that only the fields belonging to one or more annotation-defined groups be printed. Any annotation can be used for this purpose, including custom annotations. A pre-defined annotation @Abbrv is also available to cover the common use-case where a generically abbreviated printing of an object is desired.

Let's rewrite class Account to use annotation @Abbrv in fields id and username:

import org.einnovator.format.Format;
import org.einnovator.format.FormatUtil;
import org.einnovator.meta.Abbrv;
import org.einnovator.meta.Projection;

public class Account {

	@Abbrv
	private Long id;

	@Abbrv
	private String username;

	...
	@Format(projection=@Projection(includeOnlyAnnotated=Abbrv.class))
	private Account partner;
	...
}

Note the use of attribute Projection.includeOnlyAnnotated to specify the annotation defining the projection - @Abbrv in this case. The above class definition makes class Account to be printed the same way as before. The advantage of using annotations rather than field names is that the code become more refactor friendly - If the name of the fields are changed the projection does not be changed. It is also less verbose, as many fields can be included in the same annotation group with out explicitly listing them when the projection is selected. The advantage of using the name of fields/properties to specify the projection is that is can be done even if that was not antecipated in the class being referenced. This is specially relevant, when the class being referenced was developed independently.

Formatting Collections

Let's add some collection fields to class Account:

import org.einnovator.format.Format;
...
public class Account {

	...
	@Format(ignoreEmpty=true, projection=@Projection(includeOnlyAnnotated=Abbrv.class))
	private List<Account> friends;
	...
}

Dealing with Cycles

Cyclic reference is an object-graph could potentially lead to an infinite recursion problem during printing. For this reason, cycles are detected by ObjectPrinter (by keeping a list of objects/nodes already visited). Rather then recursively print an object already printed, the cycle is highlighted with a differentiated format. By default, the ellipsis string '...' is used as replacement for an object that has been printed already. The hash code is still printed for the duplicated object (if hash code printing is enabled), for easy identification of the objects in the cycle.

Integration

Implementing the toString() method is such a common thing to do, that is likely that the auto-formatting feature provided by org.einnovator.format.ObjectPrinter be appreciate in several context. The core library where it is included, bundled as ei-core.jar, does not have any dependencies on other libraries other then JAVA JRE. So I can be compiled easily, and used without chances of dependency conflicts. On the other hand, since most applications require considerable middleware support, is natural that ei-core.jar library be used in integration with others framework. While the ObjectPrinter can be used "standalone" as presented in this article, there several points where integration with other frameworks or libraries makes sense:

  • Type conversion in printing simple types can be delegated to third-party frameworks/libraries
  • Object/Bean resolution can be delegated to dependency injection-framework, whenever the @Ref annotation is used (e.g. reference to custom Printers, and to TextTransforms)

Spring Framework

CDI

Innovations

Design and Implementation

Discussion

Conclusion

Resources

Download

Articles and Tutorials by the Same Author

External References


Exercises to the Reader

Exercises in Using Auto-Formatting

Exercises Extending Auto-Formatting


No Comments

Post First Comment

Login (or Register)
Contribute Feedback