Goal

Aiming for the configuration by file support in Magnolia 5 Groovy scripts are considered as a somewhat hybrid solution with the features of config by code. Our goal is to research the options to design a DSL (Domain Specific Language) that would have lean syntax and at the same time would benefit from the possibility to write Groovy code within a definition (loops, ifs, business logic etc). Another important aspect is the code completion support in IDE's.

Dynamic builders

Dynamic builder stands for a universal tool that can describe any definition object in a convenient way. Such builders utilize Groovy's introspection and dynamic class nature goodness. 

// Domain classes 
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SomeBean {
    private String a;
    private String b;
}
 
@Data
public class DummyThing {
    private String name;
    private String title;
    private Boolean enabled;
    private int level;
    private Class<? extends DummyModel> modelClass;
    private SomeBean someBean;
    private List<SomeBean> someBeans;
    private List<Float> someNumbers;
    private Map<String, SomeBean> someOtherBeans;
    private Map<String, String> someOtherStrings;

    // Simulate the situation in renderable definition
    private Map<String, Object> parameters;
}
 
// Builder code
 
 
// One builder for any object 
builder.dummyThing('myDummyWithListAndMap'/*Some properties like name can be passed as default args*/) {
    
    modelClass = info.magnolia.config.dummy.DummyModel
    enabled =  true
    level = 4
    title = 'Some title'
    someNumbers = [1.0, 2.1, 3.2]
	// Collection properties
    someBeans {
        // Properties can be set both via c-tor...
		someBean (a: 'third', b: 'fourth')
 
		// and closures
        someBean {
            a = 'first'
            b = 'second'
        }           
    }

	// Map properties
    someOtherBeans {
        key1 = someBean {
            a = 'key1-a'
            b = 'key1-b'
        }
        
        key2 = someBean(a: 'key2-a', b: 'key2-b')
    }
}

There are three ways to implement a dynamic builder with Groovy: 

  • Generate builder methods and properties with interceptors propertyMissing and methodMissing
    • (+) Full control over DSL structure and syntax.
    • (-) Slightly tedious task since all the corner cases and patterns have to be handled by hand.
  • By means of groovy.util.BuilderSupport
    • (+) Provides factory methods for creation of DSL nodes.
    • (+) Convenient for deep hierarchical structures.
    • (-) However, does not suit cases when the node types vary.
  • groovy.util.ObjectGraphBuilder(Groovy FactoryBuilderSupport)
    • (+) Improves and complements the above mentioned approaches.
    • (+) Provides a flexible strategy for object creation, method/property invocation, relation modelling etc.
    • (+) A lot of default strategy parts make sense.

The main benefit of the such builders is obvious - a single builder can handle almost all the definitions. However, the dynamic nature makes them rather obscure and complicates the auto-completion support.

Concrete builders

UI Framework already contains some definition builders written in Java (e.g. info.magnolia.ui.dialog.config.DialogBuilder) used for instance in Blossom module. It is possible to bind them to the Groovy script.

Due various syntax sugar provide by Groovy the way those builders are used may vary. The following example shows more Java-like way of builder pattern usage.

Dialog definition via dotted notation
// Variables
def cancelActionDescription = "description"

// Breaking down the parts of the builder
def commitAction = cfg.actions.action("commit");

dialog
  .actions(
        commitAction,
        cfg.actions.action("cancel").description(cancelActionDescription)
        )
  .form()
    .description('desc')
    .tab("tab")
       .i18nBasename('test')
       .label('label')
       .fields(
          field.text("test").defaultValue("text").label("l1").i18n(),
          field.select("select").options([1, 2, 3, 4, 5]))

The other snippet shows the usage of Groovy's with operator which allows for writing the builder code in a flatter way.

Dialog definition with 'with' operator
// Optionally enclose with configuration object, so that we would not have to reference it later
// In this case cfg - UIConfig object provided via Script variables
cfg.with {
    
	// dialog - builder bound by the Script
    dialog.with {
        
		// Accessing UIConfig#actions without a need to refer to 'cfg.'
        actions.with {
            actions(
                    action("commit").with {
                        implementation AddNodeAction.class

                        description "save and add"
                    },

                    action("cancel").with {
                        implementation DeleteAction.class

                        description "save and delete"
                    }
            )
        }

        form().with {

            description 'desc'
			
			// In dotted notation it is hard to use methods like this as tab() returns TabBuilder and thus breaks the FormBuilder() chain
            tab("tab2").with {
                i18nBasename("test")
            }

            tab("tab").with {

                i18nBasename 'test'

                label('label')

                fields.with {
                    fields text("test"){

                    }, select("select")
                }
            }
        }
    }    
}
Builder extension via Groovy categories

During the previous attempts of config-by-code design one of the main problems was the extensibility of the builders. As an example we could take a look at the TabBuilder. In order to add fields to it we could have methods like TabBuilder#text("name") or TabBuilder#select("name") etc. However, since the amount of field types is virtually infinite, such API is not possible. In order to work that around the so-called config objects were introduced.

FieldConfig provides different types of field builders
public class FieldConfig {
    public DateFieldBuilder date(String name) {
        return new DateFieldBuilder(name);
    }
    public TextFieldBuilder text(String name) {
        return new TextFieldBuilder(name);
    }
    ...
}
 
// Sample of Groovy builder script where we have FieldConfig object (fieldCfg)
dialog.with {

    form().with {

        tab("tab2").with {
			
            i18nBasename("test")
 			
			// We obtain instances of field builders via 'fieldCfg' object and pass them to the 'fields()' method.
			fields(fieldCfg.text("text1"), fieldCfg.date("name2"))
        }
	}
}


However, with Groovy categories feature it is possible to add methods to builders as mix-ins.

Extending TabBuilder with FieldCategory
// The category groovy class can be constructed in two ways.
// First (like here) - usage of @Category annotation ('value' parameter indicates which type the category will be mixed-in to). In the example 'this' refers // to TabBuilder class.
//
// Second way - no annotation, but all the methods of category must be "public static"
@Category(value = TabBuilder.class)
public class FieldCategory {

    public DateFieldBuilder date(String name) {
        return addField(this, new DateFieldBuilder(name));
    }

    public TextFieldBuilder text(String name) {
        return addField(this, new TextFieldBuilder(name));
    }
 
	private static <T extends AbstractFieldBuilder> T addField(TabBuilder tabBuilder, T fieldBuilder) {
    	tabBuilder.definition().addField(fieldBuilder.definition());
    	return fieldBuilder;
	}
}
 
 
//Later the category can be applied with 'use' keyword
 
// We declare that in the following scope FieldCategory is used and hence TabBuilder gets new methods
use (FieldCategory) {
  dialog.with {
    form().with {
       tab("tab").with {
		  // Here TabBuilder already has a text() method.
          text("text")
		}
	....
}

 

Perks of existing builders
  • Control over the syntax and methods of the builders
  • Code completion is fully enabled apart from Script-bound variables, for the latter - there are workarounds in at least IntelliJ and Eclipse.
  • Less magic - clear and sane logic
Drawbacks
  • We have write and maintain the builders ourselves

Code completion tools

Since the builders are bound to the script from the outside - an IDE will have a hard time guessing which builder is used in a current script. IntelliJ IDEA as well as Eclipse try to approach the problem of IDE DSL awareness. Both do it in a similar way but not exactly the same.

IntelliJ - GroovyDSL framework 
  • https://confluence.jetbrains.com/display/GRVY/Scripting+IDE+for+DSL+awareness
  • Allows for defining suggestion rules for scripts and classes with Groovy (partial wrappers of IntelliJ code completion foundation).
  • Based on simple ideas (scopes/contributors)
  • Powerful but has limitations 
    • worst I experienced - hard to determine the delegate type in closure, but that probably comes from dynamic nature of groovy
Sample gdsl file
// Such file can be saved along with the sources and IntelliJ will automatically use it when the library is in the classpath
// Define a whole-script scope (possible to set the name pattern not to pollute other scripts)
def ctx = context(scope: scriptScope())

// Contributor clause
contributor(ctx) {
	// Hint Groovy scripts that there is a 'dialog' variable available 
    property name: "dialog",type: "info.magnolia.ui.dialog.config.DialogBuilder"
    property name: "field", type: "info.magnolia.ui.form.config.FieldConfig"
}

 

Eclipse - DSLD

How does it work (obsolete schema...)

Relations between modules, definition managers and registries.

 

GroovyDialogDefinitionProvider principle

 

 

 

Proposal: Annotated Groovy scripts

The previously observed approaches of configuration via Groovy scripts suffer from some pain points:

  • Obscurity: The binding between the script and the configuration provider is implicit and not very flexible: the configuration provider would have to bind the builder and all other necessary components (config objects like UiConfig etc) to the script via Binding object. That means that the script writer (and IDE) would not be aware of what is available in the script without additional support (like code completion scripts GDSL and DSLD).
  • Complexity: GDSL and DSLD would require thorough documentation and in case third party developer would want to expose their own component to the script - they'd have to write their own script to have code completion and that process has a certain learning curve.
  • Abstraction issue: Definition provider must know the definition type coming from the script and hence we would need definition providers for all the configurable components (dialogs, apps, templates etc).
  • Lack of IoC: Not possible to inject the components into the script via ComponentProvider.
  • Builder type restriction: Relatively hard to substitute the builder implementation used in the script: since one definition provider takes care of all the dialog scripts - they all would have to share the same builder.

In order to overcome the listed drawbacks it is proposed to make the configuration Groovy scripts more explicit and powerful. Instead of treating the script as a black box that takes the bound arguments and produces some definition the type of which we can only guess - we can introduce special builder script methods:

Annotated builder method in Groovy script
@Dialog("testDialogWithADot")
def sample(DialogBuilder dialog, FieldConfig field) {
	dialog
		.form()
		.actions(...)
	
	return dialog.definition
}

Here @Dialog is a special annotation that points to the fact that the following method provides a dialog definition. It's declaration looks like this:

@Dialog annotation declaration
@Target(ElementType.METHOD)
@Retention(RUNTIME)
// @Definition annotation marks @Dialog annotation as definition-related and points to the 
// registration operation.
@Definition(type = DialogDefinitionProviderRegistration.class)
public @interface Dialog {
    String value();
}

In such case when the script is executed the annotated  methods are resolved. The method is considered to be relevant to configuration if it declares annotation that in turn is annotated with @Definition, which also points how the resulting definition should be registered. These methods later can be invoked with all the arguments injected by the ComponentProvider. Such an approach leads to the following perks:

  • Script clearly states which objects it needs for definition construction.
    • Easier code completion in IDE's
    • Easier for developer to know what is available within a script
    • Developer is free to chose what builder to use for definition creation.
  • Definition manager is aware of:
    • how many definitions are provided by the script, 
    • what kind of definitions they are,
    • what arguments the script needs to construct each of the definitions.
  • Decoupling the definition resolution process and the process of their registration:
    • definitions are resolved from the script,
    • registration is delegated to an object declared in @Definition annotation,
    • hence we can have one definition manager that reads the scripts and automatically dispatches incoming definitions to the corresponding registries.

Source code references

Currently the annotation-based approach as a most promising is implemented as a proof of concept in the magnolia-ui project on the feature/config-sprint-1 branch (https://git.magnolia-cms.com/gitweb/?p=magnolia_ui.git;a=shortlog;h=refs/heads/feature/config-sprint-1).

(warning) On 2014-12-23, this stuff was moved into https://git.magnolia-cms.com/internal/sandbox/config-by-groovy

The previous attempt that utilises GroovyDSL code completion scripts and binding of concrete builders to Groovy is stored on the https://git.magnolia-cms.com/gitweb/?p=magnolia_ui.git;a=shortlog;h=refs/heads/feature/config-sprint-1-obsolete.

 

 

 

 

  • No labels