Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: Migration of unmigrated content due to installation of a new plugin
Info

Documentation task

Jira
serverMagnolia
keyDOCU-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.

Table of Contents

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.

Code Block
languagejava
titleAnnotated methods on the module class provide apps and dialogs
linenumberstrue
/**
 * 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.

Code Block
languagejava
titleUsing the builders and the config object
linenumberstrue
@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())
                              )
                        )
                   )
      );
}