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.
There are three ways to implement a dynamic builder with Groovy:
- Generate builder methods and properties with interceptors
propertyMissing
andmethodMissing
- (+) 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.
The other snippet shows the usage of Groovy's with
operator which allows for writing the builder code in a flatter way.
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.
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.
// 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
// 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
- http://docs.codehaus.org/display/GROOVY/DSL+Descriptors+for+Groovy-Eclipse#DSLDescriptorsforGroovy-Eclipse-delegatesTochanges
- Similar architecture (scope/contributors)
- Not tested yet - TBD
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
UiConfi
g etc) to the script viaBinding
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:
@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:
@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).
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.