Out of date
This tutorial explains how to develop a module created from scratch using blossom to manage the templates and paragraphs.
It has been developed/tested with JDK6.0, Magnolia v4.1+, Blossom v1.1.1, Spring v3.0.4, Eclipse 3.6 (Helios)
This is an overview of the simple site we are going to create.
The complete project can be downloaded here.
Pre-requisites
- Eclipse installed with the m2eclipse plugin
- Maven installation / configuration
Installation / Configuration
Installation of dependencies
Copy the following dependencies to the WEB-INF/lib/ folders of your Author and Public instances.
- magnolia-module-blossom-1.1.1.jar
- org.springframework.aop-3.0.4.RELEASE
- org.springframework.asm-3.0.4.RELEASE.jar
- org.springframework.beans-3.0.4.RELEASE.jar
- org.springframework.context.support-3.0.4.RELEASE.jar
- org.springframework.context-3.0.4.RELEASE.jar
- org.springframework.core-3.0.4.RELEASE.jar
- org.springframework.expression-3.0.4.RELEASE.jar
- org.springframework.web.servlet-3.0.4.RELEASE.jar
- org.springframework.web-3.0.4.RELEASE.jar
Download Links:
Configuration of web.xml
Edit the WEB-INF/web.xml file of both your Public and Author instances adding the following snippet. Make sure you put it before Magnolias context listener.
<listener> <listener-class>info.magnolia.module.blossom.support.ServletContextExposingContextListener</listener-class> </listener>
New Project Creation
You can download the empty project here or follow the steps below to create it.
Maven Archetype
For more details, check this page
Module Name
In this example, the module name is set to hyro-magnolia-blossom. It can be replaced with another name.
Execute the following command to create the project skeleton:
mvn archetype:generate -DarchetypeCatalog=https://nexus.magnolia-cms.com/content/groups/public/
Select the magnolia-module-archetype archetype, and enter your groupId, artifactId and other properties as prompted.
Import your new project in Eclipse: File > Import... > Maven > Existing Maven Projects
Clean project skeleton
Edit the module descriptor to match this one (src/main/resources/META-INF/magnolia/hyro-magnolia-blossom.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE module SYSTEM "module.dtd" > <module> <name>${project.artifactId}</name> <displayName>${project.name}</displayName> <description>${project.description}</description> <class>com.hyro.magnolia.blossom.core.ModuleCore</class> <versionHandler>com.hyro.magnolia.blossom.core.ModuleVersionHandler</versionHandler> <version>${project.version}</version> <dependencies> <dependency> <name>core</name> <version>4.1/*</version> </dependency> </dependencies> </module>
Maven properties
Thanks to the maven properties plugin, we can reuse here the properties defined in the pom.xml file instead of hardcoding them.
2 - Module Bootstrap:
Rename the folder src/main/resources/mgnl-bootstrap/mymodule to hyro-magnolia-blossom and remove the two xml files located there.
3 - Module Bootstrap Samples:
Rename the folder src/main/resources/mgnl-bootstrap-samples/mymodule to hyro-magnolia-blossom and remove the xml files located there.
4 - Module Resource:
Clean the folder src/main/resources/mgnl-resources removing its content
5 - Templates:
Rename the folder src/main/resources/mgnl-files/templates/mymodule to hyro-magnolia-blossom. Remove the existing folders in there and create two folders templates and paragraphs.
Update project POM
Replace the content of the pom.xml file with this one.
Module Core and Version handler
Add the two following classes to the sources of your project
package com.hyro.magnolia.blossom.core; import info.magnolia.module.ModuleLifecycle; import info.magnolia.module.ModuleLifecycleContext; import info.magnolia.module.blossom.module.BlossomModuleSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Core class of the Magnolia and Blossom Module.<br/> * Starts the Spring Context using Blossom. * * @author bruno.chauvet * */ public class ModuleCore extends BlossomModuleSupport implements ModuleLifecycle { private static final Logger LOGGER = LoggerFactory.getLogger(ModuleCore.class); private static ModuleCore instance; /** * Initialise the instance variable */ public ModuleCore() { ModuleCore.instance = this; } /** * Return the singleton instance * * @return */ public static ModuleCore getInstance() { return ModuleCore.instance; } /** * Starts the Spring/Blossom context. */ public void start(ModuleLifecycleContext moduleLifecycleContext) { ModuleCore.LOGGER.info(this.getClass().getSimpleName() + " is starting"); super.initRootWebApplicationContext("classpath:/applicationContext.xml"); super.initBlossomDispatcherServlet("blossom", "classpath:/blossom-servlet.xml"); } /** * Stops the Spring/Blossom context. */ public void stop(ModuleLifecycleContext moduleLifecycleContext) { ModuleCore.LOGGER.info(this.getClass().getSimpleName() + " is stopping"); super.destroyDispatcherServlets(); super.closeRootWebApplicationContext(); } }
Spring configuration files
The Spring configuration files to use are defined here: applicationContext.xml and blossom-servlet.xml.
The content of these files is detailed below.
package com.hyro.magnolia.blossom.core; import info.magnolia.module.DefaultModuleVersionHandler; import info.magnolia.module.InstallContext; import info.magnolia.module.delta.DeltaBuilder; import info.magnolia.module.delta.ModuleBootstrapTask; import info.magnolia.module.delta.ModuleFilesExtraction; import info.magnolia.module.delta.Task; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class instructs Magnolia on what to do when this module is installed or * updated. * * @author bruno.chauvet * */ public class ModuleVersionHandler extends DefaultModuleVersionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(ModuleVersionHandler.class); private static final String PROPERTY_FILE = "application.properties"; /** * ModuleVersionHandler Constructor. * * Register any tasks in here that need to happen as part of an update. All * though the constructor here will always be invoked, it will only ever * have an effect for updates (not new installs) because the DeltaBuilder is * adding "update" tasks. By default new installs are setup correctly * through inherited behaviour. However, if you need to perform additional * non default tasks as part of a new install the way this needs to be done * is by overriding the getExtraInstallTasks() method and adding additional * non-default tasks in here. * * @see <a * href="[WIKI|Handling module versions]"> * http://wiki.magnolia-cms.com/display/WIKI/Handling+module+versions * </a> * for more info on module versioning * */ public ModuleVersionHandler() { // Get the module version from the properties InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROPERTY_FILE); Properties properties = new Properties(); try { properties.load(is); } catch (IOException e) { ModuleVersionHandler.LOGGER.error("Cannot load property file: " + PROPERTY_FILE, e); } // Register the module using version specified in the properties. super.register(DeltaBuilder.update(properties.getProperty("Application.version"), "Common tasks").addTasks( getCommonTasks())); } /** * Overwrites the list of tasks to perform during module installation. */ @Override protected List<Task> getExtraInstallTasks(InstallContext installContext) { return this.getCommonTasks(); } /** * Return the common task for every module update. * * @return */ private List<Task> getCommonTasks() { final List<Task> commonTasks = new ArrayList<Task>(); // Installs all files from // /src/main/resources/mgnl-bootstrap commonTasks.add(new ModuleBootstrapTask()); // Extracts all files from // /src/main/resources/mgnl-files commonTasks.add(new ModuleFilesExtraction()); return commonTasks; } }
Application Version
The module version is dynamically retrieved from the application properties files
Properties file and Context configuration
Copy the following file to src/main/resources
Application.name=${project.artifactId} Application.version=${project.version}
This file is used by Maven to copy the project properties during building phase and set the properties during installation by the VersionHandler class.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:blossom="http://www.magnolia-cms.com/schema/blossom" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.magnolia-cms.com/schema/blossom http://www.magnolia-cms.com/schema/blossom-1.1.xsd"> <blossom:configuration /> </beans>
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:annotation-config /> <context:component-scan base-package="com.hyro.magnolia.blossom.controller" /> <bean class="info.magnolia.module.blossom.preexecution.BlossomHandlerMapping"> <property name="targetHandlerMappings"> <list> <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"> <property name="useDefaultSuffixPattern" value="false" /> </bean> <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" /> </list> </property> </bean> <bean class="info.magnolia.module.blossom.view.TemplateViewResolver"> <property name="prefix" value="/templates/${project.artifactId}/templates/" /> <property name="suffix" value=".jsp" /> <property name="viewRenderer"> <bean class="info.magnolia.module.blossom.view.JspTemplateViewRenderer" /> </property> </bean> <bean class="info.magnolia.module.blossom.view.ParagraphViewResolver"> <property name="prefix" value="/templates/${project.artifactId}/paragraphs/" /> <property name="suffix" value=".jsp" /> <property name="viewRenderer"> <bean class="info.magnolia.module.blossom.view.JspParagraphViewRenderer" /> </property> </bean> </beans>
JSP Paths
The Templates and Paragraphs JSP paths are defined here in Spring Configuration view resolvers.
The project structure should look like this:
At this stage, the project can be built using maven and deployed in your magnolia instance.
Styling resources
Copy the static resources to the folder src/main/resources/mgnl-files. This contains the CSS and images used to display nicely the web site.
Template Creation using Blossom
JSP Template
Copy the templates contained in this archive in the folder src/main/resources/mgnl-files/templates/hyro-magnolia-blossom/templates.
Basically, the main template jsp named mainTemplate.jsp includes a header, content and footer and some content managed properties:
- Header: Content managed list of header paragraphs
- Content: Content managed list of simple and nested paragraphs
- Footer: Static footer
Blossom Template class
Copy the following class to the sources of the project
package com.hyro.magnolia.blossom.controller.template; import info.magnolia.module.blossom.annotation.DialogFactory; import info.magnolia.module.blossom.annotation.Template; import info.magnolia.module.blossom.dialog.DialogBuilder; import info.magnolia.module.blossom.dialog.TabBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * Controller defining the Main Template. * * @author bruno.chauvet * */ @Template(name = "main-template", value = "Main Template", description = "Main Template example with Blossom", visible = true) @Controller public class MainTemplate { /** * Process the requests on URL /mainTemplate * * @return */ @RequestMapping("/mainTemplate") public ModelAndView homeTemplate() { return new ModelAndView(); } /** * Dialog factory associated to the Main page settings. * * @param dialog */ @DialogFactory("main-dialog") public void homeDialog(DialogBuilder dialog) { TabBuilder settings = dialog.addTab("Main page settings"); settings.addEdit("title", "Title", "The HTML page title"); settings.addEdit("metaDescription", "Meta Description", "HTML Meta Description of the web site"); settings.addEdit("metaKeywords", "Meta Keywords", "HTML Meta Keywords of the web site"); } }
Template Details
- @Controller: Spring annotation to automatically deploy the class as a Controller
- @Template: Blossom annotation to automatically register the template in Magnolia
- @RequestMapping("/mainTemplate"): Spring annotation to map the URL /mainTemplate to src/main/resources/mgnl-files/templates/hyro-magnolia-blossom/templates/mainTemplate.jsp
@DialogFactory("main-dialog"): Blossom annotation to associate a dialog box to manage the properties of the template. Refered in the main template with:
<cms:mainBar dialog="main-dialog" label="Main page properties" adminButtonVisible="true" />
Paragraph Creation using Blossom
JSP Paragraphs
Copy the templates containes in this archive in the folder src/main/resources/mgnl-files/templates/hyro-magnolia-blossom/paragraphs.
These are the JSP pages used to display the paragraphs that we are going to create using Blossom
Blossom Paragraph classes
Header Paragraph
Copy the following class to your sources:
package com.hyro.magnolia.blossom.controller.paragraph; import info.magnolia.module.blossom.annotation.DialogFactory; import info.magnolia.module.blossom.annotation.Paragraph; import info.magnolia.module.blossom.annotation.ParagraphDescription; import info.magnolia.module.blossom.annotation.TabFactory; import info.magnolia.module.blossom.dialog.TabBuilder; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * Controller defining the Header paragraph and dialog. * * @author bruno.chauvet * */ @Paragraph(name = "headerParagraph", value = "Header Paragraph", dialog = "header-dialog") @ParagraphDescription("Header Paragraph") @DialogFactory("header-dialog") @Controller public class HeaderParagraph { /** * Process the requests on URL /headerParagraph * * @return */ @RequestMapping("/headerParagraph") public ModelAndView handleRequest() { return new ModelAndView(); } /** * Tabs of the Header Paragraph. * * @param tab */ @TabFactory("Header") public void content(TabBuilder tab) { tab.addStatic("Header paragraph"); // Logo image tab.addFile("logo", "Logo", "Logo Image (about 120 x 100 px)"); // Header text tab.addEdit("text", "Header Text", "Header Text (about 60 characters)"); } }
Paragraph Details
- @Controller: Spring annotation to automatically deploy the class as a Controller
- @Paragraph: Blossom annotation to automatically register the paragraph in Magnolia
- name = "headerParagraph": Reference used in the <cms:newBar/> tags
- dialog = "header-dialog": Reference to link the dialog box to use when creating/editing a paragraph
- @RequestMapping("/headerParagraph"): Spring annotation to map the URL /headerParagraph to src/main/resources/mgnl-files/templates/hyro-magnolia-blossom/paragraphs/headerParagraph.jsp
- @DialogFactory("header-dialog"): Blossom annotation to associate a dialog box to manage the content of the paragraph.
In the Header Template (/templates/common/header.jsp):
<!-- Display Headers paragraphs --> <cms:contentNodeIterator contentNodeCollectionName="HeaderParagraphs"> <cms:includeTemplate /> </cms:contentNodeIterator> <!-- Admin bar to add a new Header paragraph --> <cms:newBar contentNodeCollectionName="HeaderParagraphs" paragraph="headerParagraph" newLabel="New header paragraph" />
In the Header Paragraph (/paragraphs/headerParagraph.jsp):
<!-- Edit Header paragraph --> <cms:editBar editLabel="Edit Header" /> <!-- Header paragraph content --> <div class="header"> <div class="headerLeft"> <img id="logo" width="120px" height="100px" src="${pageContext.request.contextPath}${content.logo}" /> </div> <div class="headerCenter"> <span class="header">${content.text}</span> </div> </div>
Simple Content Paragraph
Copy the following class to your sources:
package com.hyro.magnolia.blossom.controller.paragraph; import info.magnolia.cms.gui.dialog.DialogEdit; import info.magnolia.cms.gui.dialog.DialogTab; import info.magnolia.cms.util.AlertUtil; import info.magnolia.module.blossom.annotation.DialogFactory; import info.magnolia.module.blossom.annotation.Paragraph; import info.magnolia.module.blossom.annotation.ParagraphDescription; import info.magnolia.module.blossom.annotation.TabFactory; import info.magnolia.module.blossom.annotation.TabOrder; import info.magnolia.module.blossom.annotation.TabValidator; import info.magnolia.module.blossom.dialog.TabBuilder; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * Controller defining a Simple Paragraph. * * @author bruno.chauvet * */ @Paragraph(name = "simpleParagraph", value = "Simple Paragraph", dialog = "simple-dialog") @ParagraphDescription("Simple Paragraph with title, text, description, image and link") @DialogFactory("simple-dialog") @TabOrder({ "Content", "Description" }) @Controller public class SimpleParagraph { /** * Process the requests on URL /simpleParagraph * * @return */ @RequestMapping("/simpleParagraph") public ModelAndView handleRequest() { return new ModelAndView(); } /** * First Dialog Tab. * * @param tab */ @TabFactory("Content") public void content(TabBuilder tab) { tab.addStatic("Title, image and link of the paragraph"); tab.addEdit("title", "Title", ""); tab.addFile("image", "Image", "Image 100 x 70 px"); tab.addEdit("linkText", "Link Text", ""); tab.addLink("link", "Link", ""); } /** * Second Dialog Tab. * * @param tab */ @TabFactory("Description") public void description(TabBuilder tab) { tab.addStatic("Description of the paragraph"); tab.addFckEditor("description", "Description", "Rich text editor"); } /** * First Tab Validator. * * @param dialogTab */ @TabValidator("Content") public void validate(DialogTab dialogTab) { DialogEdit title = (DialogEdit) dialogTab.getSub("title"); if (StringUtils.isEmpty(title.getValue())) { AlertUtil.setMessage("You need to enter a title !"); } } }
Paragraph Details
- @Controller: Spring annotation to automatically deploy the class as a Controller
- @Paragraph: Blossom annotation to automatically register the paragraph in Magnolia
- name = "simpleParagraph": Reference used in the <cms:newBar/> tags
- dialog = "simple-dialog": Reference to link the dialog box to use when creating/editing a paragraph
- @RequestMapping("/simpleParagraph"): Spring annotation to map the URL /simpleParagraph to src/main/resources/mgnl-files/templates/hyro-magnolia-blossom/paragraphs/simpleParagraph.jsp
- @DialogFactory(simple-dialog"): Blossom annotation to associate a dialog box to manage the content of the paragraph.
- @TabOrder({"Content", "Description"}): Blossom annotation to define the order of the dialog box tabs
- @TabFactory("Content") and @TabFactory("Description"): Blossom annotation to create a tab and its associated properties in a dialog box
In the Content Template (/templates/common/content.jsp):
<!-- Content Paragraphs--> <cms:contentNodeIterator contentNodeCollectionName="ContentParagraphs"> <cms:includeTemplate /> </cms:contentNodeIterator> <cms:newBar contentNodeCollectionName="ContentParagraphs" paragraph="simpleParagraph" newLabel="New Content Paragraph" />
In the Simple Content Paragraph (/paragraphs/simpleParagraph.jsp):
<div class="simple"> <div style="float:right;"> <img width="76px" src="${pageContext.request.contextPath}${content.image}" /> </div> <h2>${content.title}</h2> <span class="description">${content.description}</span> <a href="${content.link}">${content.linkText}</a> <cms:editBar editLabel="Edit Paragraph" /> </div>
Nested Content Paragraph
Copy the following classes to your sources:
package com.hyro.magnolia.blossom.controller.paragraph; import info.magnolia.cms.gui.dialog.DialogEdit; import info.magnolia.cms.gui.dialog.DialogTab; import info.magnolia.cms.util.AlertUtil; import info.magnolia.module.blossom.annotation.DialogFactory; import info.magnolia.module.blossom.annotation.Paragraph; import info.magnolia.module.blossom.annotation.ParagraphDescription; import info.magnolia.module.blossom.annotation.TabFactory; import info.magnolia.module.blossom.annotation.TabValidator; import info.magnolia.module.blossom.dialog.TabBuilder; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * Controller defining a Nested Paragraph. * * @author bruno.chauvet * */ @Paragraph(name = "nestedParagraph", value = "Nested Paragraph", dialog = "nested-dialog") @ParagraphDescription("Nested Paragraph with a title and a list of links") @DialogFactory("nested-dialog") @Controller public class NestedParagraph { @RequestMapping("/nestedParagraph") public ModelAndView handleRequest() { return new ModelAndView(); } @TabFactory("Content") public void content(TabBuilder tab) { tab.addStatic("Title, Title of the paragraph"); tab.addEdit("title", "Title", "Main title"); } @TabValidator("Content") public void validate(DialogTab dialogTab) { DialogEdit title = (DialogEdit) dialogTab.getSub("title"); if (StringUtils.isEmpty(title.getValue())) { AlertUtil.setMessage("You need to enter a title !"); } } }
package com.hyro.magnolia.blossom.controller.paragraph; import info.magnolia.cms.gui.dialog.DialogEdit; import info.magnolia.cms.gui.dialog.DialogTab; import info.magnolia.cms.util.AlertUtil; import info.magnolia.module.blossom.annotation.DialogFactory; import info.magnolia.module.blossom.annotation.Paragraph; import info.magnolia.module.blossom.annotation.ParagraphDescription; import info.magnolia.module.blossom.annotation.TabFactory; import info.magnolia.module.blossom.annotation.TabValidator; import info.magnolia.module.blossom.dialog.TabBuilder; import org.apache.commons.lang.StringUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; /** * Controller defining a Nested SubParagraph. * * @author bruno.chauvet * */ @Paragraph(name = "nestedSubParagraph", value = "Nested SubParagraph", dialog = "nested-sub-dialog") @ParagraphDescription("Nested SubParagraph with title and links") @DialogFactory("nested-sub-dialog") @Controller public class NestedSubParagraph { @RequestMapping("/nestedSubParagraph") public ModelAndView handleRequest() { return new ModelAndView(); } @TabFactory("Content") public void content(TabBuilder tab) { tab.addStatic("Title, Title of the paragraph"); tab.addEdit("text", "Text", "Text"); tab.addLink("link", "Link", "Link"); } @TabValidator("Content") public void validate(DialogTab dialogTab) { DialogEdit text = (DialogEdit) dialogTab.getSub("text"); if (StringUtils.isEmpty(text.getValue())) { AlertUtil.setMessage("You need to enter a text !"); } } }
You can find here the usual Spring and Blossom annotations.
Regarding the paragraphs JSP:
<div class="nested"> <h2>${content.title}</h2> <!-- Counter to display subparagraphs on two columns --> <c:set var="index" value="1" scope="request" /> <ul class="nested"> <cms:contentNodeIterator contentNodeCollectionName="NestedSubParagraphs"> <cms:includeTemplate /> </cms:contentNodeIterator> </ul> <cms:newBar contentNodeCollectionName="NestedSubParagraphs" paragraph="nestedSubParagraph" newLabel="New Link" /> <cms:editBar editLabel="Edit Paragraph" /> </div>
<!-- Start a new list when half of the subparagraphs have been displayed --> <c:if test="${index > (size/2)}"> <c:set var="index" value="1" scope="request" /> </ul> <ul class="nested"> </c:if> <li><a href="${content.link}">${content.text}</a> <cms:editBar editLabel="Edit Link" /></li> <c:set var="index" value="${index + 1}" scope="request" />
At this point, you can package and deploy your module in magnolia. You will then be able to create a new page using the Main Template we created and start adding content in it.
6 Comments
Bruno Chauvet
Any comments are welcome
Tobias Mattsson
Thanks for this contribution, nice work!
I have a few notes and suggestions,
Again, nice work!
Grégory Joseph
The archetype has now been updated - see Module QuickStart for instructions
Pete Ryland
Please use https to access nexus from now on since http access will shortly be disabled.
Grégory Joseph
How about we create a blossom-specific archetype ? Would remove a lot of manual work that seems to be needed when following this article. Ping me if you need help to do this !
Jean-Francois Nadeau
That would be awesome if you can post an updated fully working example with blossom 2.0 and magnolia 4.5.x.