Documentation JIRA ticket DOCU#375

Configuration by code is an alternative approach to configure Magnolia. It includes apps, dialogs etc. The idea is to do the configuration by writing code instead of configuring in the config workspace and using bootstrap XML files.

Benefits

Configuration by code was introduced as we believe it can be quicker. Modern IDEs autocomplete the code you type. It is also easier to see what settings are available and less likely that small subtle mistakes go unnoticed.

  • Faster development
  • Less error-prone as IDEs provide code completion and syntax checking
  • Easier to keep definitions in sync when refactoring
  • Less verbose and easier to maintain than XML files

Limitations

  • Reuse. We use the extends feature extensively in Node2Bean. We reuse parts of definitions in multiple places, such as using one tab in many dialogs. This will not work with configuration by code. At least not as-is. Although, if the tab to be reused is also created by code, it is trivial to include it in a dialog by calling the piece of code that creates the tab. It can be either in a base class, a static util class or a dedicated class for this purpose. Or if the reuse is within the same module then just a method in that class thats called from all methods that use it is enough.
  • Migration in future versions of Magnolia will not be able to automatically process definitions and convert them to a newer style. They will need to be updated should changes be made to the API.

Solution

We're realising it as a fluent builder style API using the return-self pattern where all methods on the builders are chainable. This allows for a very compact and highly readable style of code.

Each configured entity be it an app, dialog etc is provided by a method on the module instance of the module that brings it in. These methods are annotated to declare them as providing a specific entity with a specific id.

Annotated methods on the module class provide apps and dialogs
/**
 * Module class for the contacts module.
 */
public class ContactsModule implements ModuleLifecycle {

    @App("contacts")
    public void contactsApp(ContentAppBuilder app, UiConfig cfg) {
        ...
    }

    @Dialog("ui-contacts-app:folder")
    public DialogDefinition folderDialog() {
        ...
    }

    @Dialog("ui-contacts-app:contact")
    public void contactDialog(DialogBuilder dialog, UiConfig cfg) {
        ...
    }
}

The annotated methods can choose to either return a definition that describes the entity or to use a builder. Methods using a builder simply adds a the builder as an argument to the method and one will be provided when then method is called. The method can also declare that it wants a config object. The methods in the above example does this with the UiConfig class. The config class provides an easy way to create builders for various things used by the entity.

Using the builders and the config object
@App("contacts")
public void contactsApp(ContentAppBuilder app, UiConfig cfg) {
   app.label("Contacts").icon("icon-people").appClass(ContactsApp.class)
        .subApps(
              app.subApp("main").subAppClass(ContactsMainSubApp.class).defaultSubApp()
                   .workbench(cfg.workbenches.workbench().workspace("contacts").root("/").defaultOrder("jcrName")
                        .groupingItemType(cfg.workbenches.itemType("mgnl:folder").icon("icon-node-folder"))
                        .mainItemType(cfg.workbenches.itemType("mgnl:contact").icon("icon-node-content"))
                        .imageProvider(cipd)
                        .columns(
                              cfg.columns.column(new ContactNameColumnDefinition()).name("name").label("Name").sortable(true).propertyName("jcrName").formatterClass(ContactNameColumnFormatter.class).expandRatio(2),
                              cfg.columns.property("email", "Email").sortable(true).displayInDialog(false).expandRatio(1),
                              cfg.columns.column(new StatusColumnDefinition()).name("status").label("Status").displayInDialog(false).formatterClass(StatusColumnFormatter.class).width(46),
                              cfg.columns.column(new MetaDataColumnDefinition()).name("moddate").label("Modification Date").sortable(true).propertyName(NodeTypes.LastModified.LAST_MODIFIED).displayInDialog(false).formatterClass(DateColumnFormatter.class).width(160)
                        )
                        .actionbar(cfg.actionbars.actionbar().defaultAction("edit")
                              .sections(
                                   cfg.actionbars.section("contactsActions").label("Contacts")
                                        .groups(
                                              cfg.actionbars.group("addActions").items(
                                                   cfg.actionbars.item("addContact").label("New contact").icon("icon-add-item").action(addContactAction),
                                                   cfg.actionbars.item("addFolder").label("New folder").icon("icon-add-item").action(new AddFolderActionDefinition())),
                                              cfg.actionbars.group("editActions").items(
                                                   cfg.actionbars.item("edit").label("Edit contact").icon("icon-edit").action(editContactAction),
                                                   cfg.actionbars.item("editindialog").label("Edit contact in Dialog").icon("icon-edit").action(editContactActionInDialog),
                                                   cfg.actionbars.item("delete").label("Delete contact").icon("icon-delete").action(new DeleteItemActionDefinition()))
                                        ),
                                   cfg.actionbars.section("folderActions").label("Folder")
                                        .groups(
                                              cfg.actionbars.group("addActions").items(
                                                   cfg.actionbars.item("addContact").label("New contact").icon("icon-add-item").action(addContactAction),
                                                   cfg.actionbars.item("addFolder").label("New folder").icon("icon-add-item").action(new AddFolderActionDefinition())),
                                              cfg.actionbars.group("editActions").items(
                                                   cfg.actionbars.item("edit").label("Edit folder").icon("icon-edit").action(editFolderAction),
                                                   cfg.actionbars.item("delete").label("Delete folder").icon("icon-delete").action(new DeleteItemActionDefinition()))
                                        )
                              )
                        )
                   ),
              app.subApp("item").subAppClass(ContactsItemSubApp.class)
                   .workbench(cfg.workbenches.workbench().workspace("contacts").root("/").defaultOrder("jcrName")
                        .form(cfg.forms.form().description("Define the contact information")
                              .tabs(
                                   cfg.forms.tab("Personal").label("Personal tab")
                                        .fields(
                                              cfg.fields.text("salutation").label("Salutation").description("Define salutation"),
                                              cfg.fields.text("firstName").label("First name").description("Please enter the contact first name. Field is mandatory").required(),
                                              cfg.fields.text("lastName").label("Last name").description("Please enter the contact last name. Field is mandatory").required(),
                                              cfg.fields.fileUpload("fileUpload").label("Image").preview().imageNodeName("photo"),
                                              cfg.fields.text("photoCaption").label("Image caption").description("Please define an image caption"),
                                              cfg.fields.text("photoAltText").label("Image alt text").description("Please define an image alt text")
                                        ),
                                   cfg.forms.tab("Company").label("Company tab")
                                        .fields(
                                              cfg.fields.text("organizationName").label("Organization name").description("Enter the organization name").required(),
                                              cfg.fields.text("organizationUnitName").label("Organization unit name").description("Enter the organization unit name"),
                                              cfg.fields.text("streetAddress").label("Street address").description("Please enter the company street address").rows(2),
                                              cfg.fields.text("zipCode").label("ZIP code").description("Please enter the zip code (only digits)").validator(cfg.validators.digitsOnly().errorMessage("validation.message.only.digits")),
                                              cfg.fields.text("city").label("City").description("Please enter the company city  "),
                                              cfg.fields.text("country").label("Country").description("Please enter the company country")
                                        ),
                                   cfg.forms.tab("Contacts").label("Contact tab")
                                        .fields(
                                              cfg.fields.text("officePhoneNr").label("Office phone").description("Please enter the office phone number"),
                                              cfg.fields.text("officeFaxNr").label("Office fax nr.").description("Please enter the office fax number"),
                                              cfg.fields.text("mobilePhoneNr").label("Mobile phone").description("Please enter the mobile phone number"),
                                              cfg.fields.text("email").label("E-Mail address").description("Please enter the email address").required().validator(cfg.validators.email().errorMessage("validation.message.non.valid.email")),
                                              cfg.fields.text("website").label("Website").description("Please enter the Website")
                                        )
                              )
                              .actions(
                                   cfg.forms.action("commit").label("save changes").action(new SaveContactFormActionDefinition()),
                                   cfg.forms.action("cancel").label("cancel").action(new CancelFormActionDefinition())
                              )
                        )
                   )
      );
}

5 Comments

  1. Is there a way how to add - by code - a new actions to the existing action bar? Let's say I have a new module that adds an "Export to PDF" feature to the Pages app, and the action bar in Pages app is already defined in JCR. Do I have to create XML files and put them into mgnl-bootstrap folder of my module's JAR file, or is there a way to add this action there by code?

  2. It seems to be outdated (didn't find anything in the code about the builder..). Is still a live code?

    1. It's a feature we still want to add, its planned but we don't have a schedule for it at the moment.

      Half of it is for dialogs and that's in the product right now, used by blossom for dialogs. The other half is for apps and that hasn't been shipped.

      The code was removed as part of MGNLUI-1592 and commit 0cea092b358338b8472453e94d4e3dd8d402b511.

      1. Thanks Tobias. Anyway, a nice feature…

  3. Why not expose an API (fluent builders), that generate the configuration as normal JCR nodes? This would overcome the extension limitation and would allow a mix of "classical" and code based configuration.

    I think, the builders could be generated out of the classes, that the node2bean process is instantiating...