Light Development with Groovy

Published on August 16, 2016 by Rich Gange



Since the release of Magnolia 5.4, light development has been all the rage. Configuration-by-file, a revamped resources loading mechanism and light modules have spawned a new development technique that allows the so-called “front-end” developer be more productive with Magnolia. Of course this is music to the ears of the “back-end” dev who never really wanted to do template development in the first place. However, what happens when the front-end dev needs a model class? Some might point to the introduction of more templating functions classes alleviating the need for as many model classes in a project. This may be true in some cases, but I don’t think we can wave bye-bye to the model just yet. Don’t forget custom definition classes as well. Perhaps at some point in the project you’ll need one of those. Using Groovy we can add some model classes and custom definition classes to our project without getting a back-end dev or IDE involved.

 

What is Groovy?

Apache Groovy is an object-oriented programming language for the Java platform. It is a dynamic programming language similar to Python, Ruby, or Perl. The Magnolia Groovy module adds Groovy capabilities to Magnolia. This feature allows you to virtually replace every Magnolia Java class with a Groovy one. The module provides a web based Unix-like console where you can access contents in Magnolia repositories. Plus a scripts workspace where you can store your scripts and Groovy classes. This scripts workspace provides the ability to plugin Groovy classes into Magnolia at runtime, without the need for restarting the servlet container. This makes for a more agile approach to coding and maintaining Magnolia based websites. I can change existing classes or add new ones and never experience a second of downtime.

To demonstrate this technique let’s take the example of the page navigation list component. If you ever took our standard developer training then you may remember doing this exercise. Except here we will implement the the component using YAML instead of JCR and Groovy instead of Java. Also we will use the Atom text editor rather than the Eclipse IDE. So let’s get started.

 

Initial setup

First thing you’re going to need is a bundle. For this I will download our latest-and-greatest at the time of this writing, Magnolia 5.4.8. Specifically I will download the “Enterprise Pro MTE demo Tomcat bundle” from http://download.magnolia-cms.com/5.4.8/. However, before I start it up, since I’m going to be working with Groovy classes, I’d prefer that I have the WebDAV module installed so that I may edit the files, from the scripts workspace, outside Magnolia. This is much more convenient in my opinion, but not required. If you choose to install WebDAV then you can download a bundle which contains all the necessary jars here.

Since this isn’t a blog on light modules, I will assume you are up-to-speed there. I have gone ahead and created my light module which I will call groovy-examples. Inside there I have a dialog, template definition, and a FreeMarker script. For those who may not have had the standard developer training the idea here is that we have a component that creates a navigation list from a page node, which will be the "target" node, selected by the editor.

So in the dialog I have a single link field configured for the Pages app.

/groovy-examples/dialogs/components/pageNavigation.yaml

form:
  tabs:
    - name: tabMain
      fields:
        - name: target
          identifierToPathConverter:
            class: info.magnolia.ui.form.field.converter.BaseIdentifierToPathConverter
          appName: pages
          description: Choose a page
          class: info.magnolia.ui.form.field.definition.LinkFieldDefinition
          label: Pages
          targetWorkspace: website
actions:
  commit:
    class: info.magnolia.ui.admincentral.dialog.action.SaveDialogActionDefinition
  cancel:
    class: info.magnolia.ui.admincentral.dialog.action.CancelDialogActionDefinition

 

For the template definition I have the following.

/groovy-examples/templates/components/pageNavigation.yaml

dialog: groovy-examples:components/pageNavigation
templateScript: /groovy-examples/templates/components/navigationList.ftl
title: Page Navigation Link List
renderType: freemarker

 

For the time being, my FreeMarker script looks like this.

/groovy-examples/templates/components/navigationList.ftl

<p>${def.title}<p>

 

So at this point I have a light module which contains the three files above. Both author and public are now running and will continue to run through the rest of the example.

Now let's test out what we have so far just to make sure we have everything in place. To keep it simple, I will edit the basic page template provided by MTK to allow for my new page navigation component in the main area. Then I will create a new page which uses the basic template and attempt to add my new component. When adding the new component make sure to select a page that has children, such as travel.

If your page looks like this then you are doing great. Now let's move on to adding the groovy model class to our component.

 

Declaring a Groovy class

From the app launcher you will find the Groovy app under the DEV menu. When you open the app you will be looking at the scripts workspace. First let's create a package structure for our model class. This is nothing more than a series of nested folders. I will use info.magnolia.groovy.component.models. Once the package is declared add a new script inside the models folder. Here is where we declare our groovy model class. At minimum a template model class needs to extend from RenderingModelImpl and declare a constructor which takes in the current content node, the current definition, and the parent model, as parameters.

info.magnolia.groovy.component.model.NavigationListModel

package info.magnolia.groovy.component.models;
 
import javax.jcr.Node;
 
import info.magnolia.rendering.model.RenderingModel;
import info.magnolia.rendering.model.RenderingModelImpl;
import info.magnolia.rendering.template.configured.ConfiguredTemplateDefinition;
 
public class NavigationListModel extends RenderingModelImpl {
   
  public NavigationListModel(Node content, ConfiguredTemplateDefinition definition, RenderingModel<?> parent) {
    super(content, definition, parent);
  }
}

 

There is one little issue I need to mention here about creating Groovy model classes and that is MGNLGROOVY-148. Not a huge deal and very easy to workaround by checking the "Is a script?" box before the save. Once saved initially, open the script back up and uncheck the box. This will signal to the system that this file is a Groovy class. Finally, don't forget to enable it by checking the "Enabled" box. Save changes to close the script.

 

Setting up the development environment

I will use the Atom text editor because I think it works nicely for this kind of development. Feel free to use whatever text editor you like best. However, before I open the editor I want to use the WebDAV module I installed earlier and map my scripts workspace as a local drive on my machine. Since the groovy class must be declared inside the scripts workspace it would be helpful to have a way to edit it outside of Magnolia. This way I can keep everything inside Atom. See the connecting to a workspace section of the WebDAV module documentation. 

Underneath the File menu is Add Project Folder. I added the parent directory for my light module, which is called light-modules, as a project folder. Inside there you can see groovy-examples module with the dialog, template definition, and script for the navigation component. You can also see I have my scripts workspace mapped as a project folder as well. Down at the bottom of the screenshot you can see the Groovy model class created previously. Now I have one centralized place for my development without the overhead of having to learn some new tool like Eclipse. Btw, Ctrl+Shft+l for the syntax highlighting selection dialog.

 

Finish the navigation model

The role of the navigation model class is to retrieve the uuid of the target, locate the node with that uuid in the website workspace, and provide it to the script as a content map. The approach here will be to create a private member to hold the target node. Along with it we'll create a getter function the script can use to get the target node from the model object. For the logic portion of finding the node we'll override the execute method of RenderingModelImpl. The final class will look something like this.

info.magnolia.groovy.component.model.NavigationListModel

package info.magnolia.groovy.component.models;
 
import javax.jcr.Node;
 
import org.apache.commons.lang3.StringUtils;
 
import info.magnolia.jcr.util.ContentMap;
import info.magnolia.jcr.util.NodeUtil;
import info.magnolia.jcr.util.PropertyUtil;
import info.magnolia.rendering.model.RenderingModel;
import info.magnolia.rendering.model.RenderingModelImpl;
import info.magnolia.rendering.template.configured.ConfiguredTemplateDefinition;
 
public class NavigationListModel extends RenderingModelImpl {
 
  public NavigationListModel(Node content, ConfiguredTemplateDefinition definition, RenderingModel<?> parent) {
    super(content, definition, parent);
  }
 
  private Node targetNode = null;
 
  public ContentMap getTargetNode() {
    return (targetNode != null) ? new ContentMap(targetNode) : null;
  }
 
  public String execute() {
    String uuid = PropertyUtil.getString(content.getJCRNode(), "target");
    if (StringUtils.isNotEmpty(uuid)) {
      try {
        this.targetNode = NodeUtil.getNodeByIdentifier("website", uuid);
      } catch (RepositoryException e) { e.printStackTrace(); return "repex"; }
      return "found";
    }
    else return "not found";
  }
}

 

Finally, add the model class property to the definition.

/groovy-examples/templates/components/pageNavigation.yaml

dialog: groovy-examples:components/pageNavigation
templateScript: /groovy-examples/templates/components/navigationList.ftl
title: Page Navigation Link List
renderType: freemarker
modelClass: info.magnolia.groovy.component.models.NavigationListModel

 

At this point it might be a good idea to check the log just be sure everything looks good. You should see a confirmation that looks something like this.

2016-08-03 20:02:27,629 INFO  agnolia.config.source.yaml.YamlConfigurationSource: Registered DefinitionMetadataBuilder.DefinitionMetadataImpl(type=TEMPLATE, referenceId=groovy-examples:components/pageNavigation, name=pageNavigation, module=groovy-examples, location=/groovy-examples/templates/components/pageNavigation.yaml, relativeLocation=components/pageNavigation) from LayeredResource{path='/groovy-examples/templates/components/pageNavigation.yaml', layeredResources=[FileSystemResource{origin=filesystem,path=/groovy-examples/templates/components/pageNavigation.yaml,file}]}

If you see any class not found exceptions then you might want to double check that your class is enabled. You'll have to go into Magnolia for that, back to the Groovy app, open the Groovy class, and check the "Enabled" box.

 

Finish the script

Right now the script doesn't do anything other than output the title of the component. At least we know the wiring is in place and we can concentrate on the logic. Here we want to first try and retrieve the target node from the model, check if we get something, and take the appropriate action. If we do get a node returned then we will create a link to the target and a link to each child page node. The final script will look something like this.

/groovy-examples/templates/components/navigationList.ftl

<p>${def.title}</p>
[#assign chosenTarget = model.targetNode!]
 
[#if chosenTarget?has_content]
   <div>
      <a href="${cmsfn.link(chosenTarget)!}">${chosenTarget.title!chosenTarget.@name}</a>
      <ul>
      [#list cmsfn.children(chosenTarget, "mgnl:page") as child]
         <li><a href="${cmsfn.link(child)!}">${child.title!child.@name}</a></li>
      [/#list]
      </ul>
   </div>
[#else]
   <div>Target node could not be found.</div>
[/#if]

 

Test it out

Now go back to the page and refresh it. If you selected a target node that had children, like the travel home page, then you would see this.

 

Wait, there's more

What if we could use this model and script for more than one workspace? For example, the DAM workspace where the assets are stored. Let's create a second component which uses the same model and script, but creates a list of assets links. To do this we will need to create a custom definition class that allows for us to configure two new properties, workspace and node type. Even if this component had already been used in the website it will be no problem at to add in a new definition class and change its model class without a second of downtime.

 

Create a Groovy definition class

Go back into the Groovy app and add a new folder under info.magnolia.groovy.component called definitions. Add a new Groovy class using the same procedure as before. The new Groovy class will introduce two new configuration options to our template definition. The class will look something like this.

info.magnolia.groovy.component.definitions.NavigationListDefinition

package info.magnolia.groovy.component.definitions;
 
import info.magnolia.rendering.template.configured.ConfiguredTemplateDefinition;
 
public class NavigationListDefinition extends ConfiguredTemplateDefinition {
 
  private String nodeType;
  private String workspace;
 
  public String getNodeType() { return nodeType; }
 
  public void setNodeType(String nodeType) { this.nodeType = nodeType; }
 
  public String getWorkspace() { return workspace; }
 
  public void setWorkspace(String workspace) { this.workspace = workspace; }
}

 

Now that the definition class is registered, we can go ahead and create the new document navigation component template.

/groovy-examples/templates/components/documentNavigation.yaml

dialog: groovy-examples:components/documentNavigation
templateScript: /groovy-examples/templates/components/navigationList.ftl
title: Document Navigation Link List
renderType: freemarker
modelClass: info.magnolia.groovy.component.models.NavigationListModel
class: info.magnolia.groovy.component.definitions.NavigationListDefinition
nodetype: mgnl:asset
workspace: dam

 

By simply adding a class property to our template definition we can point to the location of the new definition class. Now the system will be aware of the two new properties and we can use them in our model and script. For the page navigation component we will add the same three properties, but instead node type will be set to a value of mgnl:page and workspace will be set to a value of website. These are the same two values we previously hard coded into the script and model respectively.

 

Update the model class and script

Even though the model class has already been through a rendering cycle it's no problem at all to change it. However, here is where things might start getting a little more complicated. If you have understood everything so far then great. You can now start using Groovy models in your designs or at least you have this knowledge in your back pocket. However, in order for our model class to know about our new definition class we must update the signature of the constructor. Currently our model expects an object of type ConfiguredTemplateDefinition, which our new definition class extends. A perfect fit!

info.magnolia.groovy.component.model.NavigationListModel

package info.magnolia.groovy.component.models;
 
import javax.jcr.Node;
 
import org.apache.commons.lang3.StringUtils;
 
import info.magnolia.jcr.util.ContentMap;
import info.magnolia.jcr.util.NodeUtil;
import info.magnolia.jcr.util.PropertyUtil;
import info.magnolia.rendering.model.RenderingModel;
import info.magnolia.rendering.model.RenderingModelImpl;
import info.magnolia.groovy.component.definitions.NavigationListDefinition;
 
public class NavigationListModel extends RenderingModelImpl {
 
  public NavigationListModel(Node content, NavigationListDefinition definition, RenderingModel<?> parent) {
    super(content, definition, parent);
  }
 
  private Node targetNode = null;
 
  public ContentMap getTargetNode() {
    return (targetNode != null) ? new ContentMap(targetNode) : null;
  }
 
  public String execute() {
    String uuid = PropertyUtil.getString(content.getJCRNode(), "target");
    if (StringUtils.isNotEmpty(uuid)) {
      try {
        this.targetNode = NodeUtil.getNodeByIdentifier(definition.getWorkspace(), uuid);
      } catch (RepositoryException e) { e.printStackTrace(); return "repex"; }
      return "found";
    }
    else return "not found";
  }
}

 

With the new definition object we can also remove the hard coded "website" when trying to get the node by uuid. Instead the code will read the configured workspace in our template definition. Same with the script but instead there we will use the configured node type to filter the children. The final step would be to create a dialog for the new document navigation component. A quick way to do this, use the duplicate file inside Atom. Reconfigure of four values and you're done. Two components sharing a single model and definition class.

 

Final thoughts

Ok, I get it, the example was a little silly. I'll admit it, I really didn't need the model class. I could have done this all script based. However, I think it's a good learning example. One that we use in our current trainings, but much different version of course. And let me quickly give credit to the inventor, Christian Ringele. But anyway, the idea is to show how to add Groovy based classes to your design. Perhaps you have some parts of the site that change often. This Groovy based approach can provide all the flexibility you need. We saw that I was able to create a new component, use it, then change its model and definition classes without ever having to shut down. Even if you haven't used Groovy classes to start with it doesn't matter. You can always introduce a new class at any time and then reconfigure your template to use it. This creates a nice way to patch something without needing access to the underlying server.



Comments



{{item.userId}}   {{item.timestamp | timestampToDate}}

About the author Rich Gange

Richard Gange is a Senior Developer Trainer with Magnolia International Ltd. Rich has been with the company for 5 years and previously worked as a full-stack developer on the Magnolia platform for Manatee County. He began developing on Magnolia at version 4.3.


See all posts on Rich Gange

Demo site Contact us Free trial