Search code examples
javajsonswinguser-interfacemetawidget

Dynamically creating Swing GUI from JSON Schema (using Metawidget)


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" }


Solution

  • 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;
       }
    }