Thursday, June 19, 2008

Metawidget and Name/Value Pairs

I was recently asked whether Metawidget 'covered the scenario where my object contains a list of properties (name/value pairs) stored in a collection'?

The answer is yes! Metawidget can read properties from almost any source. It can even combine properties from multiple sources (using CompositeInspector). With respect to name/value pairs in a collection:

Step 1: Write an Inspector

Write an Inspector suited to your particular architecture. Here's an example:

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;
import java.util.Map;
import org.metawidget.inspector.iface.*
import org.metawidget.util.*;
import org.w3c.dom.*;

public class MapInspector implements Inspector {
   private Map<String, Map<String, Class<?>>> mObjectProperties = CollectionUtils.newHashMap();

   public MapInspector() {
      // Some dummy data
      Map<String, Class<?>> personProperties = CollectionUtils.newLinkedHashMap();
      personProperties.put( "name", String.class );
      personProperties.put( "age", int.class );
      personProperties.put( "retired", boolean.class );

      mObjectProperties.put( "person", personProperties );
   }

   public String inspect( Object toInspect, String type, String... names )
      throws InspectorException {
      Map<String, Class<?>> properties = mObjectProperties.get( type );

      if ( properties == null )
         return null;

      try {
         Document document = XmlUtils.newDocumentBuilder().newDocument();
         Element elementRoot = document.createElementNS( NAMESPACE, ROOT );
         document.appendChild( elementRoot );

         Element elementEntity = document.createElementNS( NAMESPACE, ENTITY );
         elementEntity.setAttribute( TYPE, type );
         elementRoot.appendChild( elementEntity );

         for ( Map.Entry<String, Class<?>> entry : properties.entrySet() ) {
            Element element = document.createElementNS( NAMESPACE, PROPERTY );
            element.setAttribute( NAME, entry.getKey() );
            element.setAttribute( TYPE, entry.getValue().getName() );
            elementEntity.appendChild( element );
         }

         return XmlUtils.documentToString( document );
      }
      catch ( Exception e ) {
         throw InspectorException.newException( e );
      }
   }
}

Step 2: Use the Inspector

You can try MapInspector by updating the code from the Metawidget tutorial:

package com.myapp;

import javax.swing.*;
import org.metawidget.swing.SwingMetawidget;

public class Main {

   public static void main( String[] args ) {
      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new MapInspector() );
      metawidget.setPath( "person" );


      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 210 );
      frame.setVisible( true );
   }
}

Running this will give you a UI driven by name/value pairs in a collection. And because Metawidget decouples its Inspectors from its Metawidgets, it can be equally applied to Spring, Struts, JSF, JSP, GWT, Android etc. for free!

Step 3: Change your Inspector

Of course, your particular take on name/value pairs may be different.

For example, you may not be storing the class in the 'value', you might be storing an Object instead. In that case you'll need to either use Object.getClass() as the type, or just return no type (in which case Metawidget will choose a textbox widget). Equally, you may be storing more than just the value - you may be storing whether the field is required, its maximum length etc. You'll need to add these as extra attributes to the XML elements you create.

Finally, you may want to think about how you're going from a Map to a piece of XML, and whether you can optimize that process. For example, where is the Map coming from? Wherever it is, can you have the Inspector get it from there directly?

0 comments: