Friday, October 22, 2010

Metawidget: Collections Support

Update: this blog entry has been superseded. Metawidget has a Swing CollectionTableModel built-in as of version 3.7

I was recently asked...

"Is there a way to have all [Collection] references rendered as a link that pops up to a list with search/filter/select options?"

This comes up quite a bit. The problem, which is implied in the question, is there's a lot of aesthetic preferences involved in rendering Collections. The poster was asking for a pop-up box with search/filter/select options. But what about those who want to edit the Collection in-place? Or have an edit button on each row that you have to click? Or want their Collection to be paginated? Sortable? Etc etc.

To support this, Metawidget provides WidgetBuilders.

Here's an example of a custom WidgetBuilder that wires up a Swing JTable. This isn't part of the core Metawidget distribution, because there's lots of personal preferences here - not least because Swing doesn't have a standard TableModel for rendering Collections. But the following should give you a good start in writing your own WidgetBuilder to suit your tastes (other examples you can look at include HtmlWidgetBuilder.createDataTableComponent and DisplayTagWidgetBuilder):

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.util.*;

import javax.swing.*;
import javax.swing.table.*;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.java5.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.swing.widgetbuilder.*;
import org.metawidget.util.*;
import org.metawidget.widgetbuilder.composite.*;
import org.metawidget.widgetbuilder.iface.*;
import org.w3c.dom.*;

public class Main {

   @SuppressWarnings( "unchecked" )
   public static void main( String[] args ) {

      // Model

      Person person = new Person();
      person.getAddresses().add( new Address( "Street 1", "City 1", "State 1" ) );
      person.getAddresses().add( new Address( "Street 2", "City 2", "State 2" ) );

      // Metawidget

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector(
            new CompositeInspectorConfig().setInspectors(
                  new PropertyTypeInspector(),
                  new MetawidgetAnnotationInspector(),
                  new Java5Inspector() ) ) );
      metawidget.setWidgetBuilder( new CompositeWidgetBuilder<JComponent, SwingMetawidget>(
            new CompositeWidgetBuilderConfig<JComponent, SwingMetawidget>().setWidgetBuilders(
                  new CollectionWidgetBuilder(),
                  new SwingWidgetBuilder() ) ) );
      metawidget.setToInspect( person );

      // Frame

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

   /**
    * Model
    */

   public static class Person {

      private String         mName;

      private int            mAge;

      private boolean         mRetired;

      private List<Address>   mAddresses   = CollectionUtils.newArrayList();

      private String         mNotes;

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      @UiComesAfter( "name" )
      public int getAge() {

         return mAge;
      }

      public void setAge( int age ) {

         mAge = age;
      }

      @UiComesAfter( "age" )
      public boolean isRetired() {

         return mRetired;
      }

      public void setRetired( boolean retired ) {

         mRetired = retired;
      }

      @UiComesAfter( "retired" )
      public List<Address> getAddresses() {

         return mAddresses;
      }

      public void setAddresses( List<Address> addresses ) {

         mAddresses = addresses;
      }

      @UiLarge
      @UiComesAfter( "addresses" )
      public String getNotes() {

         return mNotes;
      }

      public void setNotes( String notes ) {

         mNotes = notes;
      }
   }

   public static class Address {

      private String   mStreet;

      private String   mCity;

      private String   mState;

      public Address( String street, String city, String state ) {

         mStreet = street;
         mCity = city;
         mState = state;
      }

      public String getStreet() {

         return mStreet;
      }

      public void setStreet( String street ) {

         mStreet = street;
      }

      @UiComesAfter( "street" )
      public String getCity() {

         return mCity;
      }

      public void setCity( String city ) {

         mCity = city;
      }

      @UiComesAfter( "city" )
      public String getState() {

         return mState;
      }

      public void setState( String state ) {

         mState = state;
      }
   }

   static class CollectionWidgetBuilder
      implements WidgetBuilder<JComponent, SwingMetawidget> {

      public JComponent buildWidget( String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

         // Not for us?

         if ( TRUE.equals( attributes.get( HIDDEN ) ) || attributes.containsKey( LOOKUP ) ) {
            return null;
         }

         String type = attributes.get( TYPE );

         if ( type == null || "".equals( type ) ) {
            return null;
         }

         final Class<?> clazz = ClassUtils.niceForName( type );

         if ( clazz == null ) {
            return null;
         }

         if ( !List.class.isAssignableFrom( clazz ) ) {
            return null;
         }

         // Inspect type of List

         String componentType = attributes.get( PARAMETERIZED_TYPE );
         String inspectedType = metawidget.inspect( null, componentType, (String[]) null );

         // Determine columns

         List<String> columns = CollectionUtils.newArrayList();
         Element root = XmlUtils.documentFromString( inspectedType ).getDocumentElement();
         NodeList elements = root.getFirstChild().getChildNodes();

         for ( int loop = 0, length = elements.getLength(); loop < length; loop++ ) {

            Node node = elements.item( loop );
            columns.add( metawidget.getLabelString( XmlUtils.getAttributesAsMap( node ) ) );
         }

         // Fetch the data. This part could be improved to use BeansBinding or similar

         List<?> list = (List<?>) ClassUtils.getProperty( metawidget.getToInspect(), attributes.get( NAME ) );

         // Return the JTable

         @SuppressWarnings( "unchecked" )
         ListTableModel<?> tableModel = new ListTableModel( list, columns );

         return new JScrollPane( new JTable( tableModel ) );
      }
   }

   static class ListTableModel<T>
      extends AbstractTableModel {

      private List<T>         mList;

      private List<String>   mColumns;

      public ListTableModel( List<T> list, List<String> columns ) {

         mList = list;
         mColumns = columns;
      }

      public int getColumnCount() {

         return mColumns.size();
      }

      @Override
      public String getColumnName( int columnIndex ) {

         if ( columnIndex >= getColumnCount() ) {
            return null;
         }

         return mColumns.get( columnIndex );
      }

      public int getRowCount() {

         return mList.size();
      }

      public T getValueAt( int rowIndex ) {

         if ( rowIndex >= getRowCount() ) {
            return null;
         }

         return mList.get( rowIndex );
      }

      public Object getValueAt( int rowIndex, int columnIndex ) {

         if ( columnIndex >= getColumnCount() ) {
            return null;
         }

         T t = getValueAt( rowIndex );

         if ( t == null ) {
            return null;
         }

         return ClassUtils.getProperty( t, getColumnName( columnIndex ) );
      }
   }
}

6 comments:

Anonymous said...

so, assuming an HTML environment, are there some default options that I can achieve using annotation or configuration?

By default, related classes are rendered in context, but in the end there are only so many typical ways to work with ManyToOne, ManyToMany and OneToMany:
1. pulldown (read only)
2. multi select (read only)
3. in context list (select one/many by checkbox, possible add option)
4. popup with in context list
5. in context form (default)

It would be great to be able to have this available and have it as an annotation option.
Something like

@ManyToOne
@UIList(modes={Crud.DELETE,Crud.ADD})
public Address getAddress()

public class Address{
@UIListField
public String getPhone(){
return phone;
}
}

Of course this would assume some kind of scheme to handle the actual interaction with the servlet environment

Marc

Richard said...

Marc,

I think there are too many variations for there to be 'typical ways'. So you'll always need to write a WidgetBuilder for this, setup to your own needs, rather than it being something offered by default.

You could, of course, easily add support for custom annotations like you describe. See this example.

Richard.

Anonymous said...

How can I get an Address object/ Address List coresponding to the selected row/selected rows in the addresses JTable?

Thanks in advance,
Alex

Richard said...

Alexandru,

That's up to you. You're using the standard Swing APIs at that point, so this is not Metawidget-specific.

For one way, take a look at the Swing Address Book example (included in the Metawidget examples pack). It does something like:

((ListTableModel) myTable.getModel()).getValueAt( myTable.getSelectedRow() );

Regards,

Richard.

Anonymous said...

Hi Richard,

Thanks for the pointer, it does work indeed. But more complicated, how would you go in nesting those lists?
Example:
-Parent:
--Child1:
---GreatChild1
---GreatChild2
--Child2:
---GreatChild3

Apparently a ListTableModel like you described in your post would not be enough, or only good enough for the "last" levels of data (containing only simple data, but no list).

Thanks for the help,
Yoann

Richard said...

It's important to remember Metawidget simply aims to automate what you would already have done manually. So there are many questions you must answer manually first, and this is one of them.

I would suggest you take a look at JTree and its corresponding TreeModel. Or maybe something like this or this.

Figure out how to write one of those to suit your purposes, then transplant that code into a WidgetBuilder as demonstrated in this blog.

Please let me know how you go.

Regards,

Richard.