0
0
0
7,355

Automating Generation of Property Editors from Meta-Data

jorge.simao
Posted 10/1/10

Automating Generation of Property Editors from Meta-Data

Abstract

I show how to use recursive probes to implement generic Gui Factory in Java. The approach uses the Java reflection API to transverse class and object structures. It works by mapping field or property types to specific property editor widgets or visualization layouts. Multiple back-ends may be support, as we will illustrate with Swing, HTML, and JSP.

Introduction

GUI design and implementation is a costly and often tedious affair. This is true for standalone applications, and for web application. For example, if the application domain defines N object classes to store and manage application information, than one typically needs on the order of additional $N$ container widgets to perform basic visualization and editing of application object using a GUI. If each class has a mean number of M editable fields, than one needs about N*M field editors. Thus, giving a total number of (N+1)\times M widgets. Often, collections are treated specially using tabular views, and multiple views may be needed to display different sub-sets of data-fields. Thus the actual number of widgets involved may be much higher.

The use of widget toolkits highly simplifies the problem of GUI design and implementation, but it often a burden even for applications with limited number of domain classes and operations. Usually a larger number of classes are required, either to extend toolkit classes, configure existing widget classes, and/or to implement event listeners to realize the user interaction semantics. As many GUI designer and implementer will point out, it is not uncommon for the part of a program dedicated to the GUI to be as large or larger than the actual application logic.

Gui factories are object that can produce individual widgets or containers of widgets given some input data. In this article, we show how recursive probes can be used as GUI factories given as input the meta-data of the objects to inspect Probes are basically graphical property editors or information visualization panels and layouts that may be used in most kinds of application (as opposed to be restricted, for example, to a RAID GUI application). For standalone applications, probes widgets are keep in-sync with application state during the lifetime of user interaction.

Probes use meta-data information to automatically build widget components structures given the set or fields or properties from a class. For example, probes can be made recursive such that if a class A reference other class B than widgets for fields from the later class are automatically included in the panels of the formal class (e.g. using a tabular layout, or inline framed panels).

Sometimes we just want to automatically visualize the state of objects and application state and not editing the content. This is the case when generating separated show and edit panels or pages in a web application. It also the case when when the underlying media does support editing, such as a static file (e.g. PDF, PS). Additional applications of probes, not typically the domain of property editors, include: automatic generation of reports or reports fragments based on data, automatic listing of data stored in database, even automatic display of graphical plots.

Since the process of GUI generation is mostly automated, one can also make the probe infrastructure to be widget toolkit and environment independent. For example, multiple GUI toolkits for standalone applications can be supported (e.g. Swing and SWT in Java), and multiple view technologies for webapps can be supported (e.g. HTML for mostly static content, and JSP for dynamic content). Thus, if we are going to develop a (sub-)system that automatically to generate object property visualizes and editors (probes) from meta-data, we want it to work with several output widget toolkit and media formats.

In this article, I show how to use recursive probes to implement generic Gui Factory in Java. It uses Java reflection API to transverse class and object structures. It works by mapping field or property types to specific property editor widgets or visualization layouts. Multiple back-ends may be support, as we will illustrate with Swing, HTML, and JSP.

Architecture for Recursive Probes

For designing and implementing the generic probe system, we rely heavily on the factory design-pattern so that we can factor code out to work with multiple widget toolkit and output media. Diagram below shown the basic architecture the probe system.

Class painted (in yellow) represent arbitrary application classes that we want to generate probe for. It highlights two classes A and B with the former holding a reference to the later. Two instance objects a and b are also shown. Two Probe objects are used inspect and/or edit the state of each object, by reflecting of the classes meta-data. A ProbeFactory object is used to produce the actual widgets (graphical components) to insert in some widget container. EditorData is an auxiliary structure that references a specific field or property and widget, and is created and managed by a Probe. To have the probe be toolkit or media generic, we only need to provide a ProbeFactory implementation for a specific system.

Architecture for Recursive Probe Architecture.

A Probe is usually created for each top-level Java class that is probed. If a top-level class references two different instances of the same class B then a different probe is created for each field or property. In our design, a Probe instance reflects only on a single class and inspects objects of that class only. To support multiple classes we introduce a second-level factory class ProbeFactory. Instances of this class maintain a mapping between classes and probes and produce new probes or return existing probes when requested. This allows for example, to have an application that maintains a tree of objects of different types, and can inspect these object in a panel managed by a ProbeFactory.

Probe Services

Below, we show the interface specification for a probe. Method build(.,.) asks the probe to build the widgets required to inspect the specified class. It should be called only once. An object instance of that class may also be provided, so it is inspected and used to configure the widgets initial state. Method inspect(.) is used to inspect a specific instance. This method can be invoked many times with different objects.


public interface Probe  {
	//build probe for class and inspect object
	void build(Class<?> cl, Object obj);
	
	//inspect object
	void inspect(Object obj);
	
	//create new Probe instance
	Probe newInstance(Class<?> cl, Object obj,  EditorData ped);

	//get top-level widget (container) 
	Object getWidget();

	//add widget to container
	void add(EditorData ed, Object wg);

	 //create an instance of a EditorData
	EditorData createData();

//get inspect class
	public Class<?> getProbedClass();
		
	//get expression to access probed variable
	public String getExpr();		
}

Method newInstance(..) to produce a new probe of the same type. It is used to reflect on referenced object by recursive creation of probes. Method add(.,.) is used to add new widgets to the probe during construction.

Method createData() is factory method for instances of EditorData. This is a auxiliary class that is used to encapsulate the state necessary to build the widget, including: a java.lang.reflect.Member meta-object such as a Field or setter Method for the property, and a reference to the object being inspected. Providing a factory method allows specific Probe implementation to tailor to specific needs, an in particular to have it implement event listeners methods so that the state of the inspected object and the state of the editor widget can be kept in synch. Remaining methods are auxiliary and at appropriated points below.

A Probe implementation iterates on class properties to build a set of widget or visualization components. The property value for a specific object instance provide the state to the widgets.

EditorFactory Services

A EditorFactory is used by a Probe to map specific field types to widget types. The service of the EditorFactory interface are shown below. Method create(.) is used to create a widget for a specific property. EditorFactory implementations should check the type of the property when selecting a widget to build. The value returned is of type Object so that a common widget hierarchy is not forced to all implementation (e.g. a Swing JComponent widget, or a HTML/XML Element may be returned).

Method createLabel(.) is used as a factory method for decorative labels to put next or around to the widget. Likewise, method createPanel(.) is used to created the panel widget that holds the pairs of widget for the label and for the value widget.


public interface EditorFactory {
	//Create widget to edit object.
	Object create(EditorData data);

	//Create panel to aggregate label and editor widget.
	public Object createPanel(EditorData data);
	
	//Create label widget to decorated edited object.
	Object createLabel(EditorData data);

	//(Notify editor widget about new object value.
	void setEditorValue(EditorData data);

	//Update object with probe value.
	Object getEditorValue(EditorData data);

}

Since we want a probe to be reused for possibly many object of the same type, we define a method EditorFactory.setEditorValue(..) to set individual widget state.

The implementation of method Probe.inspect(.), iterates object properties and stored FieldData objects to updata the state of widgets by calling EditorFactory.setEditorValue(..). Method EditorFactory.getEditorValue(.) is used to get the state from the widget typically to update the state of the probed object in response to an event from the widget.

EditorFactory Abstract Class

It useful to introduce a class implementation of EditorFactory as a support to implement specific factories. This is show below as class GenericEditorFactory with the implementation of method create(.). This method checks the type of the field to edit or visualize, as returned by argument EditorData ed, and invokes specific methods for each type category.


public abstract class GenericEditorFactory implements EditorFactory {

	public Object create(EditorData ed) {
		Class<?> ty = ed.getType();
		
		if (TypeUtil.isCollection(ty)) { return createForCollection(ed); </b>
		
		if (TypeUtil.isBool(ty)) { return createForBoolean(ed); </b>
	
		if (ty==String.class) { return createForText(ed); </b>
		if (TypeUtil.isChar(ty)) { return createForChar(ed); </b>	
		
		if (TypeUtil.isNumeric(ty)) { return createForNumeric(ed); </b>

		if (ty==Date.class) { return createForDate(ed); </b>
		if (ty==Color.class) { return createForColor(ed); </b> 
		if (ty==File.class) { return createForFile(ed); </b> 
			
		return null;
	}	
	...
}

A custom utility class named TypeUtil is used to check type match for properties.

For example TypeUtil.isBool(Class<?>) checks if a type is either a primitive or wrapper for boolean values. Its implementation is show below. Other method TypeUtil follow a similar pattern thus its implementation should be straightforward (left as exercise).

	
class TypeUtil {
	public static isBool(Class<?> ty) {
		return ty==Boolean.TYPE || ty==Boolean.class;
	}
	...
}

The actual methods to create the widgets are shown below. For example, method createForBoolean(.) is called to created a widget that can model boolean values (e.g. a checkbox), while method createForNumeric(.) is called to create widgets to edit numeric values (e.g. a spinner or slider, if available, or a text field otherwise). Since we are not bound to any specific widget toolkit or output media bodies in this class, these methods are mostly empty in function (return null). Method createForText(.) is somewhat special in that it makes further checks using the input EditorData. Namely, it selected one between three additional method: one specific to edit password, another for normal single text line, and an additional one for multi-line text area (to edit properties with long text content).

	
	protected Object createForBoolean(EditorData ed) { return null;	}

	protected Object createForChar(EditorData ed) { return null; }

	protected Object createForText(EditorData ed) {
		if (ed.isPasswd()) { return createForPasswd(ed); }
		if (ed.isXLong()){ return createForTextArea(ed); }
		return createForTextLine(ed);
	}

	protected Object createForPasswd(EditorData ed) { return null; }
	protected Object createForTextLine(EditorData ed) { return null; }
	protected Object createForTextArea(EditorData ed) { return null; }
	
	protected Object createForNumeric(EditorData ed) { return null; }
	protected Object createForDate(EditorData ed) { return null; }

	protected Object createForCollection(EditorData ed) { return null; }

	protected Object createForColor(EditorData ed) { return null; }
	protected Object createForFile(EditorData ed) { return null; }
	
	protected Object createForOther(EditorData ed) { return null; }	

The implementation for setEditorValue(EditorData) and getEditorValue(EditorData) follows the same pattern as create(EditorData), except that other method are created. For example, for boolean properties methods setForBoolean(.) and getForBoolean(.) are invoked (not shown; left as exercise).

Generic Probe Implementation

Below, we show a generic probe implementation. Probes for specific widget toolkits or media types may derive this class. Field wf is the EditorFactory that is used to build the individual property editing widgets. Fields cl and obj are, respectively, the type probed and the currently inspected object (if any). Field wg is the top-level widget for the probe as a panel or other kind of widget container.

Field EditorData ped is a reference for the parent's probe property that created this Probe by recursion. Field PropeOptions options is an object that contains optional data to control and configure the widgets generation process. Finally, field led is a map from property meta-object and support EditorData. In the presented implementation public Field are always inspected directly for properties. (Using accessors and modifier property methods is left as exercise .)


abstract public class GenericProbe implements Probe {

	public static final String ATTRS = "Attributes";

	protected EditorFactory wf; //widget factory

	protected Class<?> cl; //reflect class
	protected Object obj; //inspected object
	protected Object wg; //top-level generated widget container

	protected EditorData ped; //property in parent Probe
	protected ProbeOptions options; //configuration
	
	java.util.Map<Field, EditorData> led =
		new java.util.HashMap<Field, EditorData>();
	
...	
}

The GenericProbe constructor initializes the fields from the arguments. Additionally, it check if a class is provided. If not, the object is checked and if provided it get the class of the object. It is an semantic error to build a probe with a probe with supplying neither a class nor an object instance of that class.


public GenericProbe(Class<?> cl, Object obj,  EditorData ped, ProbeOptions options) {
	this.obj = obj;
	this.ped = ped;		
	this.cl = cl;	
	this.options = options;		
	if (cl==null && obj!=null) this.cl = obj.getClass();
}

Utility method build() build the probe widgets for the class set on the constructor. It also check if an object is provided, in which can it inspects that object to set the state of created widget from the state of the application object.


protected void build() {
	build(cl);
	if (obj!=null) inspect(obj);
}

public void build(Class<?> cl) { build(cl, obj); }

Building Widgets by Iterating and Recursing on Meta-Data

Code below show how the probed widget actually created and aggregated. Method buildForFields(..) iterates over public fields as definition of properties, creates and stores EditorData object as memorized tokens for properties (calling method getEditorData(..)), and calls an additional build(.) method to construct and get the widget for that property. If a non-null widget object is returned virtual method add() is called to add the widget to the top-level widget container. The widget is also memorized in the EditorData object for future reference.


public void build(Class<?> cl, Object obj) {
	this.cl = cl;
	buildForFields(cl, obj);
}

private void buildForFields(Class<?> cl, Object obj) {
	if (cl==null) return;
	buildForFields(cl.getSuperclass(), obj);
	Field[] fds = cl.getDeclaredFields();
	for (Field fd: fds) {
		int mod = fd.getModifiers();
		if (!Modifier.isPublic(mod)) continue;
		EditorData ed = getEditorData(fd, obj);
		Object wg = build(ed);
		if (wg!=null) {
			ed.setEditor(wg);
			add(ed, wg); //virtual
		}
	}		
}

Note that the relative layout order of declared class properties is controlled in relation to the properties declared in the parent class. Namely, we make parent class properties to be reflected upon and appear first in the generated layouts. This comes from the intuition that parent classes contain more generic or abstract domain data, and thus many times its more natural to make widgets for these properties appear first. An alternative, is to use a ProbeOptions flag to specify this relative ordering. (Left as exercise.)

The details of creating and integrating a new property widget is done in method build(EditorData), shown below. Method getType() is invoked on the EditorData to get the type of the property. Then a few utility method are used to group the type and give different treatments to each case. If the object is a collection or it a primitive or simple type then the EditorFactory is invoked directly to get the new widget. A type is defined as primitive or simple if it assumed that the EditorFactory implementation is responsible to produce a widget for it. This includes all numeric types, java.lang.String, java.awt.Color, java.util.Date, and java.io.File. Other design assumption are possible, but these constitutes usually a good working set. Collections are also assumed to be handled by the EditorFactory, although its actual implementation is a bit more subtle (as we shall see below).

	
Object build(EditorData ed) {
	Class<?> cl = ed.getType();

	if (TypeUtil.isCollection(cl)) {
		return buildCollection(ed);			
	}
	else if (!TypeUtil.isPrimitiveOrSimple(cl)) { //recurse (tab or in-line)
		try {
			Object obj_ = ed.getValue();
			Probe prb = newInstance(cl, obj_, this);
			ed.setFactory(prb);
			Object edf = ed.getFactory();
			if (prb!=null) return prb.getWidget();
		} catch (IllegalAccessException ex) { ex.printStackTrace(); </b>
	} else {
		return buildPrimitive(ed);
	}

	return null;
}

Object buildPrimitive(EditorData ed) {
	return wf.create(ed);
}

Object buildCollection(EditorData ed) {
	return wf.create(ed);
}

abstract public void add(EditorData ed, Object wg);	

When the property type is not primitive or simple, nor a collection, things get more interesting as we recursivelly build a new Probe of the same kind and use the top-level widget container as the property widget. This is done by calling method newInstance(..). Note the parameters passed: the class is the type of the property, the object is the field value for the object instance of the parent probe (obtained with method EditorData. getValue(), and the parent probe is the current probe. This apparently simple step allows arbitrary class and object structures to be recursively probed. Thus the returned widget structure will have the same level of complexity as the reflected application domain class structure.

Some caveats are in order thought. A basic difficulty is that if a class A reference an instances of the same type, then an infinite recursion of widget construction occurs. One approach to dealt with this case is to ignore such properties. If application objects of are stored in some high-level container structure (e.g. an object tree, a scene graph, or a data-base table), then this is not a serious consideration. Referenced objects can be inspected as high-level objects selected from the high-level container.

Another issue, is controlling the details of the generated layout which are indeed important given the visual character of the output. Here aesthetics, style, and navigation issue are of true importance. Below, we discuss one such layout options: selecting to organize recursed objects either in an inline layout or using tabbing widgets (also know as notebooks).

Inspecting Objects

Once a probe builds a widget tree structure one want is to be inspect the state of object of the class reflected on the build-up. This is done in method inspect(.), shown below for GenericProbe. It first check that the class of the object passed as argument matches the class used in th build-up. As a sanity measure it returns immediately if there is a mismatch. Then, it iterates over memorized EditData objects and updates it to the new identity of the inspected object.


	public void inspect(Object obj) {
		//sanity test: check type match
		if (obj.getClass()!=cl) return;
		this.obj = obj;
		for (EditorData ed: led.values()) {
			ed.setObject(obj);
			ed.setValue(ed.getValue());
			Object edf = ed.getFactory();
			if (edf!=null && edf instanceof Probe) {
				((Probe)edf).inspect(val);
			}
			wf.setEditorValue(ed);
		}
	}

A special case is when property recursion has been applied. This has been set with method setFactory() in the build-up. Method getFactory() is used to access back the object. If it non-null and of type Probe then recursion has been applied. Then inspect() method is invoked on the child Probe passing the appropriate field value as object to inspect.

Accessors and Modifiers

Accessors and modifiers may also be defined to make our GenericProbe object comply with JavaBeans conventions. These are shown below.


	public ProbeOptions getOptions() { return options; }
	public void setOptions(ProbeOptions options) { this.options = options; }
	
	public Probe getParentProbe() { return ped!=null ? ped.getProbe() : null; }
		
	protected void setWidget(Object wg) { this.wg = wg; }
	public Object getWidget() { return wg; }

	public EditorFactory getFactory() { return wf; }
	public void setFactory(EditorFactory wf) { this.wf = wf; }
	
	public Class<?> getProbedClass() { return cl; }	

Tabbed Layouts and View Details

It is useful to control the details of layout structure for style, aesthetics, and easy navigation. This is particularly true when the inspected objects have many fields, recurse very deep, or editor widget use much space on the layout. Organizing editor in notebook or tabbed widgets are a common solutions to this problem. Other widget solutions include collapsible panes, tabular representation of data-views, and other custom solutions.

Below, we show the code required to implement an optional tabbed solution. Method isTabed(Field) check field annotation to instruct the details of view layout. The custom annotation View is used to provide hints about viewing. Attribute tab is used to specify that a property should be rendered in a dedicate tab of a notebook. Methods isTabed() and allTabed check condition on the property set, and may be used in setting up the top-level widget container. For example, if isTabed() returns true then a Notebook widget should be created as top-level widget. This is further explored below in implementing Swing probes.


protected boolean isTabed(Class<?> cl) {
	Field[] fds = cl.getFields();
	for (Field fd: fds) {
		if (isTabed(fd)) return true;
	}
	return false;
}

protected boolean allTabed(Class<?> cl) {
	Field[] fds = cl.getFields();
	for (Field fd: fds) {
		if (!isTabed(fd)) return false;
	}
	return true;
}

protected boolean isTabed(EditorData ed) {
	return isTabed(ed.getField());
}

protected boolean isTabed(Field fd) {
	Annotation[] ans = fd.getAnnotations();
	for (Annotation an : ans) {
	    if (an instanceof View) {
	    	View aprb = (View) an;
	    	if (aprb.tab()) return true;
	    </b>
	}
	return false;
}

A possible specification for annotation View is shown below. Attributes may be sent to influence the graphical layout of probe views. For example, boolean attribute passwd may be use to specify that a String property holds a password and a widget for password should be used. Likewise, slider may be used in numeric fields and/or method to specify that a property should be edited using a Slider widget (e.g. as opposed or in additional to a Spinner widget).


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD</b>)
public @interface View {
String widget() default "";
boolean tab() default false;
boolean passwd() default false;
boolean slider() default false;
boolean xlong() default false;
boolean rich() default false;    
}

Making Text Label

Another layout concern is selection of text label for a property or a class. Here we use the simple approach of taking type names without prefix (package or class) information. For properties we also capitalize the first letter of the field name. This is shown below. Possible extensions to this approach is to use an attribute of the View annotation for labels, and/or use external property files for internationalization and easy configuration.


static public String getLabel(Class<?> cl) {
	return getLabel(cl.getName());
}

static public String getField(Field fd) {
	return getLabel(fd.getName());
}

static public String getLabel(String s) {
	int i = s.lastIndexOf(".");
	if (i>0) return s.substring(i+1);
	return s;
}

EditorData

We made extensive use of auxiliary EditorData without showing the specification or the way they initialized from a GenericProbe. This is shown below in method GenericProbe.getEditorData().


protected EditorData getEditorData(Field fd, Object obj) {
	EditorData ed = led.get(fd); //check map cache
	if (ed!=null) {
		ed.setObject(obj);
		return ed;
	}
	ed = createData();
	ed.setMetaObject(fd);
	ed.setObject(obj);
	ed.setFactory(wf);
	ed.setProbe(this);
	led.put(fd, ed); //save in cache
	return ed;
}

public EditorData createData() {
	return new EditorData();
}

The map led is searched for a cached EditorData is one is found, the it is returned. It also assigned the new inspected object identity. If not found, it is created and put in the map for later access. Method setFactory(.) and setProbe(.) are also used to specify factory and owner objects.

The specification for EditorData class is shown below (accessors and modifiers methods omitted). Field are as follows: Member mb is a reference to the inspected Field, Object obj is the inspected object, Probe prb and Object edf are the probe and factory that created the EditorData instance. Object ed is the created widget.


public class EditorData  {
	Field fd;
	Object obj;
	Object edf;
	Object ed;
	Probe prb;

	public EditorData() {</b>
	...
	
	public Object getValue() {
		if (obj==null) return null;
		try {
			return fd.get(obj);
		} catch (IllegalAccessException ex) {</b>	
		return null;
	}

	public void setValue(Object val) {
		if (obj==null) return null;
		try {
			fd.set(obj, val);
		} catch (Exception ex) {</b>	
	}
	
}

Method getValue() and setValue() are use utility methods to access and set the field value in the inspected object.

Property Value Constraints and Layout Hints

We often want to limit the set or range of values that can be set for a property. For example, numeric values may be limited to a specific $[min,max]$ range. This in turn, may be used configure editor widgets to operate with the same constraints. Furthermore, some configuration options refer only to the style of the layout or data input mode. For example, one can specify the kind of widget to use to edit numeric values (e.g. spinner, slider, or both). To give control to application developers of these configurations, we can use Java annotations to specify them accordingly.

ProbeOptions

Additional control of probe generated layout can be done use a ProbeOption class. Useful properties to define include: if editing is allowed or widget should be created in read-only mode, configuration of optional decorations, creation of hyper links, and so forth. For fixed media outputs or web content, a standalone attribute may be used to specify if the probe top-level widget is to be used alone or as part of a large document (e.g. a template layout). A possible sketch of this class is shown below (accessors and modifier method not shown):


public class ProbeOptions {
	 public boolean standalone, readonly, links, dynamic;

	...
}

Figure below show an UML diagram of the probe system presented in this article:

UML diagram of probe system.

Demo Application Domain

To test and showcase the features of your probe system, we devise a simple demo application domain with three class: a User, a ShoppingCart, and a Item. A User is a generic representation for person in a computer system. A ShoppingCart is a common entity in enterprise applications such as on-line stores to manage the set of item a user purchases or orders. An Item is a individual or a number of products purchased.

The definitions for these classes are shown below. Class User highlights the use of annotations to control the probe generated widgets. Property String passwd uses annotation @View to specify that a password text field should be used (with actual text usually obscured with some fixed character). Numeric attribute type is configured with annotation @View to be represented and set as a slider widget. Annotation @Range is used to enforce a constraint for the value to lie within a set $[min,max]$ range. Property about is annotated with @View(xlong=true), so that a multi-line text-area editor widget is used. Remaning properties, Color favoriteColor, String[] emails, and ShoppingCart cart are request to be layout in a tabular manner, because its expected that that visual space used by each of these components is large.


import java.awt.Color;
import java.util.Date;
import jprobe.annotation.View;
import types.Range;

public class User {
	public String name;

	@View(passwd=true)
	public String passwd;	

	public Date born;

	public float pay;

	@Range(min=0, max=10) 
	@View(slider=true)
	public int type;

	@View(xlong=true)
	public String about;

	@View(tab=true)
	public Color favoriteColor;	

	@View(tab=true)
	public String[] emails;

	@View(tab=true)
	public ShoppingCart cart;
}

The property emails is a collection, so a list of values should be rendered and possibly edited. Property cart is non primitive or simple, so under the hoods an additional probe should be created recursively.

Class ShoppingCart is shown below with a single collection property for items. Each Item specifies a product name, a quantity, and a total price. More realistic application are likely to include more properties in the ShoppingCart and Item definitions.


public class ShoppingCart {
	public Item[] items;
}

public class Item {
	public String product;
	public int quantity;	
	public float price;
	
	public Item(String product, int quantity, float price) {
		this.product = product;
		this.quantity = quantity;
		this.price = price;
	}
}

Swing Probes

At this point, we will see you the GenericProbe implementation of Probe can be specialized to Swing, likely the most widely used JAVA widget toolkit. Below, you find the definition of SwingProbe that extends GenericProbe. Its constructor invokes the super class constructor, and call the initialization method init(). This method sets widget factory as instance of SwingEditorFactory (described further below), calls initLayout() to setup top-level Swing widget containers, and invokes the support class to build the widget tree structure.


public class SwingProbe extends GenericProbe implements ComponentListener, ChangeListener {

	public SwingProbe(Class<?> cl, Object obj,  EditorData ped, ProbeOptions options) {
		super(cl, obj, ped, options);
		init();
	}	

	protected void init() {
		setFactory(new SwingEditorFactory());
		initLayout();
		build();
	}
	
	protected void initLayout() { ...
</b>
}

Probe Layout for Swing

There are several possible solutions for top-level widget container to layout property editors. Here, we use a Panel with a GridBagLayout layout manager. Since we want to support tabbing of widgets, we also use a JTabbedPane widget if at least one field is annotated with a @View(tab=true). When tabs are used, a specially dedicated tab labeled GenericProbe.ATTRS is created to layout all widgets that are are not specifically requested to be put on a separate tab.


JPanel pn0, pn;
JTabbedPane tabs;

protected void initLayout() {
	pn0 = new JPanel();
	setWidget(pn0);

	if (isTabed(cl)) {
		tabs = new JTabbedPane();
		add(tabs);
		if (allTabed(cl)) {
			pn = pn0;
			return;
		}
		pn = new JPanel();
		tabs.add(GenericProbe.ATTRS, pn);
	} else {
		pn = pn0;
	}
	
GridBagLayout ly = new GridBagLayout();
	pn.setLayout(ly);		
}

To add individual property editor widget inline we use method add() for JPanel passing in the approprite layout options. For tabbed field we use method add() on JTabbedPanel. When adding to the JPanel we setup a GridBagConstraints object to properly place the widgets. In the implementation show below, individual label and widgets for a property are layout as an horizontal pair, widget different pair being layout vertically.


static final int XPAD = 2, YPAD = 2, MARGIN = 2;

public void add(EditorData ed, Object _wg) {
	JComponent wg = (JComponent) _wg;

	String lb = ed.getLabel();
	
	//add as tab
	if (tabs!=null && isTabed(ed)) {
		tabs.add(lb, wg);
		return;
	}

	//add inline
	int i = pn.getComponentCount()/2;

	//add label
	GridBagConstraints c = new GridBagConstraints();
	c.gridx = 0;
	c.gridy = i;
	c.anchor = GridBagConstraints.LINE_END;
	JComponent jlb = (JComponent) getFactory().createLabel(ed);
	pn.add(jlb, c);

	c.ipadx = XPAD; c.ipady = YPAD;
	c.insets = new Insets(MARGIN, MARGIN, MARGIN, MARGIN);

	//add widget
	c = new GridBagConstraints();
	c.fill = GridBagConstraints.HORIZONTAL;
	c.gridx = 1; c.gridy = i;
	c.weightx = 1.0;
	c.ipadx = XPAD; c.ipady = YPAD;
	c.insets = new Insets(MARGIN, MARGIN, MARGIN, MARGIN);
	c.anchor = GridBagConstraints.LINE_START;		

	pn.add(wg, c);
}

void add(JComponent wg) {
	GridBagConstraints c = new GridBagConstraints();
	pn0.add(wg, c);
}

Factory for Swing Widgets

A factory for Swing editor widgets should extend GenericEditorFactory to create Swing widgets capable of editing the values of the property type. This is shown below as class SwingEditorFactory. Since the set of widgets to create is somewhat extensive we introduce them in parts.


class SwingEditorFactory extends GenericEditorFactory  {

	public EditorData createData() {
		return new SwingEditorData();
	}
...

}

Method createData() is overriden to produce EditorData instances adapted to Swing event handling mechanisms (this is discusses further below).

Property Labels

Labels for properties are mapped to JLabel instance in a staightfoward manner:

public Object createLabel(EditorData ed) {
	JLabel lb = new JLabel(ed.getLabel());
	return lb;
}

Boolean Properties

Boolean properties are mapped to JCheckBox. Since a label is already placed near the widget, the intrinsic label for the JCheckBox is set to empty. Note that a listener is connected to the JCheckBox to get notification about state modifications and propagate the changes from the widget state to the object state.


protected Object createForBoolean(EditorData ed) {
	JCheckBox cb = new JCheckBox("");
	cb.addChangeListener((SwingEditorData)ed);
	return cb;		
}

Text Properties


protected Object createForTextLine(EditorData ed) {
	StringViewHint vh = (StringViewHint) getViewHint(ed);
	JTextField tf = (vh!=null && vh.getLen()>0) ?
			new JTextField(vh.getLen()) :
			new JTextField();
	tf.addActionListener((SwingEditorData)ed);
	return tf;
}

protected Object createForPasswd(EditorData ed) {
	StringViewHint vh = (StringViewHint) getViewHint(ed);
	JPasswordField pf = 
		(vh.getLen()>0) ? new JPasswordField(vh.getLen()) :
			new JPasswordField();
	pf.addActionListener((SwingEditorData)ed);
	return pf;
}

protected Object createForTextArea(EditorData ed) {
	StringViewHint vh = (StringViewHint) getViewHint(ed);
	if (vh!=null && vh.isRich()){ 
	}
	JTextArea ta = new JTextArea();
	ta.addPropertyChangeListener((SwingEditorData)ed);
	return ta;
}

Numeric Properties


protected Object createForNumeric(EditorData ed) {
	NumericRangeViewHint vh = (NumericRangeViewHint) getViewHint(ed);
	Object val = ed.getValue();
	
	Class<?> ty = ed.getType();
	if (vh==null) vh = new NumericRangeViewHint().configFor(ty);
	if (!vh.isSlider()) {
		Double min = (Double)TypeUtil.cast(vh.getMin(), Double.class);
		Double max = (Double)TypeUtil.cast(vh.getMax(), Double.class);
		Double d = min;
		if (val!=null) {
			d = (Double)TypeUtil.cast(val, Double.class);
			if (d.compareTo(min)<0) min = d;
			if (d.compareTo(max)>0) max = d;				
		}
		SpinnerModel mdl = new SpinnerNumberModel(d, min, max,
			(Double)TypeUtil.cast(vh.getStep(), Double.class));
		JSpinner spn = new JSpinner(mdl);
		spn.addChangeListener((SwingEditorData)ed);			
		return spn;
	} else {
		JSlider sld = new JSlider(JSlider.HORIZONTAL,
				(Integer)TypeUtil.cast(vh.getMin(), Integer.class),
				(Integer)TypeUtil.cast(vh.getMax(), Integer.class),
				(Integer)TypeUtil.cast(vh.getStep(), Integer.class));
		sld.addChangeListener((SwingEditorData)ed);
		return sld;
	}
}

Date and Color Properties


protected Object createForDate(EditorData ed) {
	Calendar tyd  = new GregorianCalendar();
	Date dc = tyd.getTime();
	tyd.add(Calendar.YEAR, -100);
	Date d0 = tyd.getTime();
	tyd.add(Calendar.YEAR, 200);
	Date d1 = tyd.getTime();
	SpinnerModel mdl = new SpinnerDateModel(dc, d0, d1, Calendar.YEAR);
	JSpinner spn = new JSpinner(mdl);
	spn.addChangeListener(ed);
	return spn;	
}

protected Object createForColor(EditorData ed) {
	JColorChooser cc = new JColorChooser();
	cc.getSelectionModel().addChangeListener(this);
	return cc;	
}

Handling Events

To be able to keep the state of the inspect object and the widget state in sync, the probe system needs to be notified when widget state is changed and make the changes to be propagated back to the application object.


public class SwingEditorData extends EditorData
implements ChangeListener, ActionListener, PropertyChangeListener {

	public SwingEditorData() {</b>
	
	public void propertyChange(PropertyChangeEvent ev) {
		updateValue();
	}

	public void stateChanged(ChangeEvent e) {
		updateValue();
	}
	public void actionPerformed(ActionEvent e) {
		updateValue();
	}	
}

Collections Properties


protected Object createForCollection(EditorData ed) {
	Class<?> ty = ed.getType();
	Object val = ed.getValue();
	Probe pp = ed.getProbe();
	ProbeOptions options = pp!=null ? ((GenericProbe)pp).getOptions() : null;		
	DataView tp =  new SwingDataView(ty, val, ed.getComponentType(), pp, options);
	ed.setFactory(tp);
	ed.setEditor(tp.getWidget());
	return tp.getWidget();
}

Swing Probes for Demo Application Domain

To test our Swing implementation of probes we use the demo application domain, with a User object initializes as shown below:


public static User demoUser1() { 
	User u = new User();
	u.name = "Jack Grip";
	u.passwd = "ping2001";
	u.born = new Date(10,10,10);
	u.about = "A nice user,\nlonging for pretty probes...";
	u.pay = 123.5f;
	u.type = 4;
	u.emails = new String[]{ "abc@xyz.net", "abc@zoo.biz" </b>;
	u.cart = new ShoppingCart();
	u.cart.items = new Item[] {
		new Item("Product 1", 1, 110.5f),
		new Item("Product 2", 2, 35.0f)
	};
	return u;
}

Figure belows show the generated Swing widgets with a different tab in each image. The tab for attributes not specifically tabbed is named Attributes. Note that for collections and lists, we have implemented only the viewing component not the editing component. (This is left as exercise.)

Swing probe for demo application domain.

HTML Probes

To support web applications, we should extend the probing system to generate HTML and JSP web pages or fragments of it. The goal it to have probe that auto-generates HTML forms to edit data, or HTML pages with other visualization elements when working in read-only mode.

Below, we show class HtmlProbe extending GenericProbe. It follow a similar design pattern as SwingProbe, except that widget are represented as elements and element sub-trees of a XML/(X)HTML document. Field Document doc is the XML document where the top-level element is attached. An instance of HtmlEditorFactory is created to produce the XML/(X)HTML elements to edit or visualize the properties.


public class HtmlProbe extends GenericProbe {
	Document doc;
	Element e1;
	
	public HtmlProbe(Class<?> cl, Object obj,
		EditorData ped, Document doc, ProbeOptions options) {
		super(cl, obj, ped, options);		
		init(doc);
	}
	
	protected void init(Document doc) {
		if (doc==null) {
			this.doc = XmlUtil.createDocument();
		} else {
			this.doc = doc;
		}
		setFactory(new HtmlEditorFactory(this.doc));
		initLayout(cl);
		build();
	}
	
	protected void initLayout(Class<?> cl) { ...
</b>	
	...
}

XML Utils

In the code above, class XmlUtil is used to hide the details of build the XML document and elements. In our implementation, it use JDOM library. XML DOM library shipped with XJAP in JDK, has some limitation in dealing with XML namespaces which can be an issue when generating JSP pages. For this reason, we converged to use JDOM.

Method XmlUtil.createDocument() is used create a XML Document. Other methods, used below, include: method createElement() to create a XML element with the specified tag, attributes and body text value; method append() is used to append a child node element to a parent XML node; method getRoot() and setRoot() get and set the XML document root element. Some methods are invoked directly on a XML Element, such as setAttribute() to set additional attributes.

(X)HTML Forms and Layout

The layout of (X)HTML property editors is tuned depending on whether we are generated a standalone (X)HTML file with <html>, <head>, <body> top-level tags, or is intended to be used as part of a large document (e.g. using layout composition with view templates).

Additionally, a <form> element should only be used if editing properties (i.e. not in read-only mode), and only once in editing a object tree. Thus if probe is built recursively and as a parent probe, as when a class A reference some other class B, then properties for the referenced class should be layout with tag <fieldset> and not <form>.

The implementation of these design strategy is shown below. Method initLayout() is the method controlling initial layout of the (X)HTML pages. Data-field Element e1 is used to save the element where individual widgets for properties are to be inserted. In editing mode, this is either a <form> or <fieldset> element.


protected void initLayout(Class<?> cl) {
	if (isStandalone() && getParentProbe()==null) {
		Element pe = XmlUtil.getRoot(doc);
		Element e = addHTML(pe);
		e = addHeader(e);	
		e1 = addForm(e);
		//keep widget null so is not added again
	} else {
		if (getParentProbe()!=null)  {
			Element pe = ((HtmlProbe)getParentProbe()).e1;
			e1 = addFieldSet(pe);
			if (pe==null) XmlUtil.setRoot(doc, e1);
		} else {
			Element pe = XmlUtil.getRoot(doc);
			e1 = addForm(pe);
		}
		//keep widget null so is not added again
	}
}		

Element addFieldSet(Element pe) {
	if (isReadonly()) {
		Element e = XmlUtil.createElement(doc, pe, "div");
		XmlUtil.createElement(doc, e, "h3", getLabel(cl));
		return e;
	}
	Element e = XmlUtil.createElement(doc, pe, "fieldset");		
	XmlUtil.createElement(doc, e, "legend", getLabel(cl));		
	return e;				
}

Element addForm(Element pe) {
	Element e = XmlUtil.createElement(doc, pe, "div", "class:fieldset", null);
	if (pe==null) XmlUtil.setRoot(doc, e);
	
	if (isReadonly()) {
		String id = getParentProbe()==null ? "id:domain-class" : "";
		XmlUtil.createElement(doc, e, "h2",
			"class:domain-class;" + id, getLabel(cl));
		return e;
	}

	e = XmlUtil.createElement(doc, e, "form");

	e.setAttribute("method", "POST");
	e.setAttribute("action", "url");

	return e;				
}

Element addHTML(Element pe) {
	Element e = XmlUtil.createElement(doc, pe, "html");
	if (!XmlUtil.hasRoot(doc)) XmlUtil.setRoot(doc, pe!=null ? pe : e);
	XmlUtil.createElement(doc, e, "head");		
	return XmlUtil.createElement(doc, e, "body");		
}

To allow specific widget to be added to the layout, we override method GenericProbe.add() and map it to the method XmlUtil.append() to append child node elements to a parent XML node. Pairs of label elements and widgets are arranged using a grouping panel.


public void add(EditorData ed, Object wg) {
	Element pn = (Element) wf.createPanel(ed);
	Element elb = (Element) wf.createLabel(ed);
	add(pn, elb);
	add(pn, (Element)wg);
	add(pn);		
}
void add(Element e) { add(e1, e); }
void add(Element pe, Element e) { XmlUtil.append(pe, e); }

HTMLEditorFactory

Class HTMLEditorFactory implements (X)HTML based widgets to edit or visualize properties. For example, to edit properies HTML forms widget tags such as <input type="checkbox|text|password|.."> are used.


public class HtmlEditorFactory extends GenericEditorFactory {	
	Document doc;
	
	public HtmlEditorFactory(Document doc) {
		this.doc = doc;
	}

...	
}

Boolean Properties

Method createForBoolean() show the implementation of the (X)HTML property editor/visualizer. Auxiliary method isReadonly() is called to check whether edit or read-only mode is configured. For read-only mode an element with tag span is used. For editing of value, form widget <input type="checkbox"> is used. Attribute name is set with the field name so that edited values can be easily recovered from the form action page (set in the ACTION attribute).


protected Object createForBoolean(EditorData ed) {
	if (isReadonly(ed)) {
		return XmlUtil.createElement(doc, null,
			"span", "class=field-value", formatValue(getValue(ed)));
	}
	Element e = XmlUtil.createElement(doc, null,
		"input", "type:checkbox;class=field-value", null);
	e.setAttribute("name", ed.getName());
	Object val = getValue(ed);
	if (val!=null) { e.setAttribute("value", formatValue(val)); </b>
	else { e.setAttribute("value", "false"); </b>
	return e;
}

Method getValue() is used to fill-up the value for the element. In the basic implementation it simple forwards the request to the EditorData instance. Later, we show how to modify the implementation to support dynamic view with EL expressions such as JSP.


	protected Object getValue(EditorData ed) {
		return ed.getValue();
	}

Text Properties

Textual elements follow the same coding pattern as boolean elements, except that the type of widget is different. This is shown below:


protected Object createForPasswd(EditorData ed) {
	if (isReadonly(ed)) {
		return XmlUtil.createElement(doc, null, 
			"span", "class=field-value", formatValue(getValue(ed)));
	}
	Element e = XmlUtil.createElement(doc, null,
		"input", "class=field-value;type:password", null);
	e.setAttribute("name", ed.getName());
	Object val = getValue(ed);
	if (val!=null) e.setAttribute("value", formatValue(val));		
	return e;
}

protected Object createForTextLine(EditorData ed) {
	if (isReadonly(ed)) {
		return XmlUtil.createElement(doc, null,
			"span", "class=field-value", formatValue(getValue(ed)));
	}
	Element e = XmlUtil.createElement(doc, null,
		"input", "class=field-value;type:text", null);
	e.setAttribute("name", ed.getName());
	Object val = getValue(ed);
	if (val!=null) e.setAttribute("value", formatValue(val));		
	return e;
}

protected Object createForTextArea(EditorData ed) {
	if (isReadonly(ed)) {
		return XmlUtil.createElement(doc, null,
			"span", "class=field-value", formatValue(getValue(ed)));
	}
	Element e = XmlUtil.createElement(doc, null,
		"textarea", "class=field-value", null);
	e.setAttribute("name", ed.getName());
	Object val = getValue(ed);
	if (val!=null) e.setAttribute("value", formatValue(val));		
	return e;
}

Numeric and Other Properties

(X)HTML does not have a specific form widget type for numerics. The usual approach is to use a text line input widget, and perform validation of the widget value either on the client side using Javascript (e.g. with custom event handlers, validation and error functions, or using a Javascript framework), or on the server side (e.g. using a MVC framework to simplify and/or automated this task). In our implementation, we use text input widgets without performing client side validation. Thus, the implementation is basically the same as createForTextLine(). Client side validation is left as exercise, while server side validation is mostly out of the scope of the probing system described in this article.

Other types lacking direct (X)HTML from widgets include Date and Color. To support these property type either custom or out-of-the-shell HTML/CSS/Javascript widget toolkits and framework should be used. File uploading is already well supported in (X)HTML with form input widget <input type="file">.

Styling

Following modern principles of web-design, elements style should be not included in the content body but made separated in Cascading Style Sheets (CSS) resource files. Thus, your (X)HTML probes should follow these principles and avoid setting the style property directly on generated (X)HTML. In stead, the class property is set of meaningful names that can be tapped with from CSS files to define the look-and-feel of the widgets. This was already done in code shown above.

HTML Probes for Demo Application Domain

Below, we show a screen-shots of the generated HTML layout as seen in Firefox web browser. Left-hand side image is a form where editing of fields can be performed. Right-hand side image is a read-only visualization. Note several aspects are still missing in our implementation, such as: form submit button, editing of lists and data-views and so forth, hide password in visualization only pages, etc. This is left as future development. Using a Javascript/CSS widget framework might be particularly useful in this respect.

HTML probe for demo application domain: left): Form with edit widgets; right): Read-only visualization.

Clearly, there is not much style in the generated views. But this was purposefully left out to be the work of CSS cascading style sheets and web page templating framework. The generate HTML files are available in the links below. Fell free to use a CSS file to make the generated views more fancy (left as exercise).

[User Edit Probe] [User Show Probe]

JSP Probes and Dynamic Web Views

Dynamic web application using view technology such as Java Server Pages (JSP), can be easily supported by extending or modifying the HtmlProbe implementation. Namely, field values should be set as EL expressions with syntax ${varExpr}, where varExpr is a naming expression to access a variable. For example, property a1 of an instance a of class A requires a EL expression as follows: ${a.a1}. If an instance of class B is reference as field b, then a property of B named b is accessed with EL expression: ${a.b.b1}.

To implement this we need to add some utility methods to GenericProbe and/or FieldEditor to return the EL expression to be set as value of (X)HTML form or other elements. Since probes are arrange in a tree like structure to mirror class structure, one needs to recurse down on field or property names to build the complete EL expression. Code below, shows an implementation.


public String getVarName() {
	return getLabel(cl).toLowerCase();
}
static public String getVarName(Field fd) {
	return getLabel(fd.getName()).toLowerCase();
}

public String getExpr() {
		return pp!=null ? pp.getExpr() + "." + getVarName() : getVarName();
	}
public String getExprEL() {	
	return makeExprEL(getExpr());		
}
public String getExpr(Field fd) {
		return getExpr() + "." + getVarName(fd);
	}
public String getExprEL(JField fd) {	
	return makeExprEL(getExpr(fd));		
}

static public String makeExprEL(String expr) {	
	return "${" + expr + "</b>";
}

To add the generation of EL expression we modify the method that provides the values for widgets as follows:


	protected Object getValue(EditorData ed) {
		return isDynamic(ed) ? ed.getExprEL() : ed.getValue();
	}

Editing Multiple Types using ProbeFactory and Layered Panels

For standalone applications, is useful to inspect and edit many kind of objects from a single GUI location. To implement this, we should create many probes and top-level widgets that share the same visual location. This is done, by hiding all probes except the one under current use.

For this purpose, we introduce the interface ProbeFactory for a factory of probes that share the same visual space in the GUI. A different implementation of ProbeFactory is used for each widget toolkit.


public interface ProbeFactory {
	Probe buildProbe(Class<?> cl, Object obj, ProbeOptions options);

	Object newPanel();

	void addToPanel(Object pn, Object wg);	

	void setVisible(Object wg, boolean b);
}

Method buildProbe() builds a probe for a specific class; method newPanel() creates a root container widget for all probes' top-level widget; method addToPanel() adds a newly created probe to this root widget; and method setVisible() is used to hide/show individual probe widgets.

The common code is factored in class ProbePanel that maintains a cache map of probes and ensures that only a single probe is visible at any time. This is the class the the application developer will create directly and interact with (e.g. to insert the root widget on the global application GUI). The implementation of ProbePanel is shown below:


public class ProbePanel  {
	Map<Class<?>, Probe> prbs = new HashMap<Class<?>, Probe>();
	Object wg;
	Probe prb;
	ProbeFactory pf;
	ProbeOptions options;
	
	public ProbePanel(ProbeFactory pf, ProbeOptions options) {
		this.pf = pf;
		this.options = options;
		wg = pf.newPanel();
	}
	public ProbePanel(ProbeFactory pf) {
		this(pf, null);
	}
	protected void setWidget(Object wg) { this.wg = wg; </b>
	public Object getWidget() { return wg; </b>
	
	protected void hideProbe() { showProbe(prb, false); </b>
	protected void showProbe(Probe prb, boolean b) {
		if (prb==null) return;
		Object cw = prb.getWidget();
		if (cw==null) return;
		pf.setVisible(cw, b);
	}
	protected void showProbe(Probe prb) {
		showProbe(prb, true);
		this.prb = prb;
	}
	
	public void inspect(Object obj) {
		Class<?> cl = obj.getClass();
		if (this.prb!=null) {
			Class<?> _cl = this.prb.getProbedClass();
			if (_cl==cl)  {
				prb.inspect(obj);
				return;
			}
		}
		hideProbe();
		if (obj==null) return;
		Probe prb = prbs.get(cl);
		if (prb!=null) {
			prb.inspect(obj);
			showProbe(prb);
		}
		else {
			prb = pf.buildProbe(cl, obj, options);
			if (prb!=null) {
				pf.addToPanel(wg, prb.getWidget());
				showProbe(prb);
			}
		}
	}
}

Method inspect(.) is used to inspect an object of any type. This is implemented by first checking if the currently visible probe is suitable for the object type passed as argument. If this is the case, the current probe is asked to inspect the object. If the current probe does not match the object type, it is hidden and a probe for the object type is searched on the cache map. If one is found, it is made visible and requested to inspect the object. If no probe is found, one is created first using the configured ProbeFactory and saved on the cache map for future reference. Additionally, the probe is added to the common panel by invoking ProbeFactory.addToPanel(..) and made visible. From the use perspective, this operation give the illusion that a single probe panel is being used as the visual location on the GUI will be the same.

SwingProbeFactory

For Swing-based application we define class SwingProbeFactory as an implementation of ProbeFactory. This class creates SwingProbe instances and adds the probes' top-level widget a common JPanel configured with the simple FlowLayout as layout manager. The implementation of addToPanel() and setVisible() map directly to methods of JComponent, the most abstract class in Swing' hierarchy (except AWT classes).


public class SwingProbeFactory implements ProbeFactory {
	public SwingProbeFactory() {</b>
	
	public Probe buildProbe(Class<?> cl, Object obj, ProbeOptions options) {
		return new SwingProbe(cl, obj, null, options);
	}
	
	public Object newPanel() {
		LayoutManager ly =  new FlowLayout();
		return new JPanel(ly);
	}
	public void addToPanel(Object pn, Object wg) {
		((JPanel)pn).add((JComponent)wg);
	}
	
	public void setVisible(Object wg, boolean b) {
		((JComponent)wg).setVisible(b);
	}
	
}

Summary and Conclusions

I have shown how to implement a system for automatic generation of GUI panels and web pages to edit or visualize the properties of Java classes. The approach is to use probe objects that reflect on properties meta-data (public fields), and compose generate widgets. Widget types are selected from type of the property of the reflected class. To make the implementation generic, we use factory objects to create the widgets. A different factory is used for each widget toolkit or output media type (e.g. Swing, HTML, JSP, etc.). Field or property annotations can be used to control the visual layout and set constraints on property values. These constraints are used in initializing the editor widgets. Probe widgets for different object types can share the same location on the GUI layout by hidden all probes widgets except the one currently in use. This allow the application developer to use the probe system by creating and interacting with a single class that we called ProbePanel.

Exercises

If you want to make sure that you fully understand the topics presented in this article you may perform the exercises described below.

  • Copy&Paste the code fragments presented in this article, complete the missing parts, and setup a project to actually run and test the Probe system. Implement and test first the Swing implementation. Then, implement the HTML implementation. Test the output on your browser. If you feel like deploying a webapp in a servlet/JSP container, implement the JSP probe, deploy it, and test it. To simplify you may ignore initially property constraints specified as annotations.
  • The GenericProbe implementation uses java.lang.reflect.Field to get and set property values. This limits the approach to public field. A more general approach is to use property getter and setter methods (accessors and modifiers). Upgrade GenericProbe to use java.lang.reflect.Method to access and modify object values. If no accessor or modifier method are available than it falls-back to using the Field (if is public).
  • Add an option to the class ProbeOptions to control the relative ordering of inspected declared properties of a class in relation to its parent class.
  • Modify the SwingEditorFactory to use a JList widget for collections of primitive or simple types (e.g. String, Date), and other class types with a single field of these types.
  • Modify GenericEditorFactory to support Enumerated types. Modify SwingEditorFactory to use a JComboBox to visualize and edit such type value.
  • Modify the SwingEditorFactory to support editing of collections as list and tabular data. For example, include buttons to ADD, DELETE, MODIFY components. For array implementation of collections
  • Perform the above modification for Swing probes apply also to Html probes.
  • Extend the HtmlProbe and HtmlEditorFactory to support you favorite javascript widget toolkit. (If you don't know any or don't have a favorite, consider Dojo).

Related Work and External References

  • Repast - A typical simulation framework for agent-based modeling that uses reflection and Swing to auto-generated property editor panels to edit model parameters.
  • Grails - A web-framework for the Groovy language that generates Groovy Server Pages (GSP) to implement a CRUD applications. It takes inspiration form Ruby or Rails web-framework. It uses Spring Framework and Spring MVC as underlying infrastructure.
  • Spring Roo - Another CRUD webapp generation tool for Spring Framework. Inspired by Grails, but generates JSP views and Java code instead of GSP and Groovy code. (Spring Source company also acquired and owns Grails).
  • Seam - Red Hat web framework for JSF that generates Facelets views and CRUD applications.

No Comments

Post First Comment

Login (or Register)
Contribute Feedback