Dynamically creating Swing GUI from JSON Schema (using Metawidget)

1.7k Views Asked by At

as the title suggest, I would like to create a Swing GUI based on a JSON Schema (that I fetch in real time) and use it to populate a JSONObject (Google SimpleJSON). I was thinking of using the Metawidget framework for this, but have been so far unsuccessful. I found various references online but none seem to be working for this particular case. There's always some classes or methods missing that are used in the example, and the documentation of Metawidget is not great (at least I wasn't able to find a set of examples for version 4.2). The JSON Schema I get describes Java classes that were described using Jackson's JSONSchema on the server side, but aren't available locally or can be known beforehand, so this should be handled as well.

Does anyone have any suggestions about a different approach, or some examples/references I can use? Of course, concrete code that compiles with Metawidget 4.2 is more that welcome as well.

--- EDIT --- (Due to the response of Richard Kennard)

Using the code blocks provided, I managed to generate a GUI. However, I needed to modify the value of the 'json' and 'jsonSchema' string and insert additional values, and switch the order of the inspectors passed to CompositeInspector. Here's the code and the generated GUI:

final JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

String json = "{\"person\": { \"firstname\": \"Richard\", \"surname\": \"Kennard\", \"notes\": \"Software developer\" }}";
String jsonSchema = "{ \"name\": \"person\", \"type\": \"person\", properties: { \"firstname\": { \"required\": true }, \"surname\": { \"required\": true }, \"notes\": { \"large\": true }}}";

final SwingMetawidget metawidget = new SwingMetawidget();
metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
        new JsonSchemaInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( jsonSchema.getBytes() ) ) ),
        new JsonInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( json.getBytes() ) ) )
)));

metawidget.setToInspect( json );
frame.add( metawidget, BorderLayout.CENTER );
frame.setSize(500, 500);
frame.setVisible(true);

enter image description here

This is without the usage of MapWidgetProcessor because (I suppose) it needs to be modified to support String to JSONObject conversion. (Also, the 'NAME' variable in that code block is undefined and supposedly needs to be replaced with 'elementName'?)

However, all of this begs a couple of new questions:

1) Why aren't the values from the 'json' mapped to the components?

2) What should the setup be if I didn't have the 'json' value, but only the 'jsonShema'?

3) Why doesn't the code work when explicitly specifying the property type in the schema like:

"firstname": { "required": true, "type": "string" }

2

There are 2 best solutions below

1
On BEST ANSWER

A core principle of Metawidget is allowing you to mix-and-match various approaches to suit your architecture. So I can answer this question in pieces.

A basic SwingMetawidget:

// UI

final JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

// Metawidget

final SwingMetawidget metawidget = new SwingMetawidget();
...configure Metawidget by setting inspectors, inspection result processors, widget builders, etc...
metawidget.setToInspect( myData );
frame.add( metawidget, BorderLayout.CENTER );

To read JSON type data, and JSON schemas, use a CompositeInspector:

String json = "{ \"firstname\": \"Richard\", \"surname\": \"Kennard\", \"notes\": \"Software developer\" }";
String jsonSchema = "{ properties: { \"firstname\": { \"required\": true }, \"notes\": { \"large\": true }}}";

...
metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
      new JsonInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( json.getBytes() ) ) ),
      new JsonSchemaInspector( new JsonInspectorConfig().setInputStream( new ByteArrayInputStream( jsonSchema.getBytes() ) ) ) )

To map types, consider adding in a TypeMappingInspectionResultProcessor:

metawidget.addInspectionResultProcessor(
    new TypeMappingInspectionResultProcessor<SwingMetawidget>(
        new TypeMappingInspectionResultProcessorConfig()
            .setTypeMapping( "foo", "bar" )
            .setTypeMapping( "abc", "def" )));

Or, possibly a better approach, add in a custom WidgetBuilder to handle widgets for your unknown types:

metawidget.setWidgetBuilder( new CompositeWidetBuilder( new ompositeWidgetBuilderConfig()
    .setWidgetBuilders(
        new OverriddenWidgetBuilder(), new ReadOnlyWidgetBuilder(),
        new MyWidgetBuilder(), new SwingWidgetBuilder()
    )));

where MyWidgetBuilder does something like

class MyWidgetBuilder
    implements WidgetBuilder<JComponent, SwingMetawidget> {

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

        if ( "my.special.type".equals( attributes.get( TYPE ) ) )

            return new JSuperWidget();
        }

        // Fall through to other WidgetBuilder

        return null;
    }

By default, JComponents will not save their data anywhere. You need to add something like BeansBindingProcessor for that. Of course BeansBinding only binds to JavaBeans. If you want to bind to something else (like a JSON Map) you can add your own MapWidgetProcessor:

/**
* MapWidgetProcessor uses the Metawidget's <code>toInspect</code> to retrieve/store values.
*/

public class MapWidgetProcessor
   implements AdvancedWidgetProcessor<JComponent, SwingMetawidget> {

   //
   // Public methods
   //

   @Override
   public void onStartBuild( SwingMetawidget metawidget ) {

      getWrittenComponents( metawidget ).clear();
   }

   /**
    * Retrieve the values from the Map and put them in the Components.
    */

   @Override
   public JComponent processWidget( JComponent component, String elementName, Map<String, String> attributes, SwingMetawidget metawidget ) {

      String attributeName = attributes.get( NAME );
      getWrittenComponents( metawidget ).put( attributeName, component );

      // Fetch the value...

      Map<String, Object> toInspect = metawidget.getToInspect();
      Object value = toInspect.get( attributeName );

      if ( value == null ) {
         return component;
      }

      // ...and apply it to the component. For simplicity, we won't worry about converters

      String componentProperty = metawidget.getValueProperty( component );
      ClassUtils.setProperty( component, componentProperty, value );

      return component;
   }

   @Override
   public void onEndBuild( SwingMetawidget metawidget ) {

      // Do nothing
   }

   /**
    * Store the values from the Components back into the Map.
    */

   public void save( SwingMetawidget metawidget ) {

      Map<String, Object> toInspect = metawidget.getToInspect();

      for ( Map.Entry<String,JComponent> entry : getWrittenComponents( metawidget ).entrySet() ) {

         JComponent component = entry.getValue();
         String componentProperty = metawidget.getValueProperty( component );
         Object value = ClassUtils.getProperty( component, componentProperty );

         toInspect.put( entry.getKey(), value );
      }
   }

   //
   // Private methods
   //

   /**
    * During load-time we keep track of all the components. At save-time we write them all back
    * again.
    */

   private Map<String,JComponent> getWrittenComponents( SwingMetawidget metawidget ) {

      @SuppressWarnings( "unchecked" )
      Map<String,JComponent> writtenComponents = (Map<String,JComponent>) metawidget.getClientProperty( MapWidgetProcessor.class );

      if ( writtenComponents == null ) {
         writtenComponents = CollectionUtils.newHashMap();
         metawidget.putClientProperty( MapWidgetProcessor.class, writtenComponents );
      }

      return writtenComponents;
   }
}
0
On

In answer to the new questions:

2) Then you'd have to supply the complete schema. At the moment the jsonSchema only has attributes like 'required'. Attributes like 'type' are being inferred from the json object values. And CompositeInspector is merging them together for you. But if you want to just have a JsonSchemaInspector (no JsonInspector, no CompositeInspector) then your jsonSchema would have to have all the attributes

3) Because 'string' is a JavaScript type. The Java equivalent is 'java.lang.String'. So you can use TypeMappingInspectionResultProcessor (or its subclass JsonSchemaMappingInspectionResultProcessor). This may seem onerous, but remember what you're doing is quite unusual (rendering JSON in Java). Luckily Metawidget is pluggable for all sorts of combinations.

Finally: "support String to JSONObject conversion" - I wouldn't think so. JSONObject is the top-level concept. It's basically a Map. The individual fields you need to bind to are still primitives (Strings, numbers etc). So MapWidgetProcessor is probably a good fit.