6 Developer Guide - Reference Documentation
Authors: Stephan Albers, Mark Palmer, July Antonicheva
Version: 1.4-SNAPSHOT
Table of Contents
6 Developer Guide
This section covers the internal details that can be used to customize Weceem within your application, create custom content types and other more technical subjects.6.1 Hooking into the Admin interface
Hooking into the Admin interface of WeceemApplications can add items to the Weceem "Administration" page by using navigations from Platform-core plugin, to add items to the navigation group "weceem_admin". The Weceem Application does this :- Prepare AppNavigation.groovy file in "grails-app/conf/"
- Put some more configuration there
navigation = { weceem_admin { users(controller:'CMSUser', action:'list', order:5) { } }}
6.2 Create custom content types
Weceem provides many content types as standard. Each content type has a specific purpose, with different fields in the editor.Some content types are directly rendered when requested, others are used by other content nodes to compose the page seen by the user.The standard content types available in Weceem are :- Action - A server-side action that will execute the code in a Groovy Script node, for example to create new content nodes or send an email
- Blog - The root node of a Blog, which has Blog Entry nodes as children
- Blog Entry - An entry (child of) a Blog node
- Comment - A comment on a content node. Any nodes can have comments attached or created.
- External Link - A URL of another website. When this URI is rendered, it redirects to the external site
- Folder - A folder for grouping together content nodes and creating URI hierarchy
- Groovy Script - A piece of Groovy code to execute on the server, used by Action nodes
- HTML - A piece of HTML which may contain GSP (Grails Server Page) tags
- JavaScript - A piece of JavaScript source
- Server File - A file uploaded to the Weceem repository and stored on the server filesystem, e.g. an image file
- Server Directory - A directory on the server filesystem which contains Server File nodes
- Stylesheet - A CSS style sheet
- Template - A GSP template used to layout and decorate the current content node
- Widget - A reusable GSP fragment for rendering sections of a page, e.g. a header/footer or a remote news feed.
Creating custom content types
You can add new content types that will be editable by your users and displayed in the repository along with the standard content types.These custom types can have full control over how a request is handled (e.g. issue a redirect, like WcmExternalLink does) or delegate to established rendering mechanisms within Weceem. You might have it call out to a PDF generator for example, or render some data using an alternative presentation technology e.g. wiki rendering or Freemarker. They can also specify how their properties should be edited in the repository, and hook into events.To create a new content type, you just need to create a new Grails domain class that extends the WcmContent class and follow a few simple conventions. You can create a basic template for a new content type using the create-content-class script :grails create-content-class com.mycompany.Product
import org.weceem.util.ContentUtils import org.weceem.content.WcmContentclass PressRelease extends WcmContent { // Set to false if this content node should never actually be rendered // i.e. Groovy Script nodes cannot be rendered directly. static standaloneContent = false String getMimeType() { "text/html" } String content /** * Must be overriden by content types that can represent their content as text. * Used for search results and versioning */ public String getContentAsText() { ContentUtils.htmlToText(content) } /** * Should be overriden by content types that can represent their content as HTML. * Used for wcm:content tag (content rendering) */ public String getContentAsHTML() { content } static constraints = { content(nullable: false, maxSize: 65536) } static editors = { content(editor:'HtmlCode') } static transients = WcmContent.transients }
- the Grails "transients" property must be populated with the sum of the superclass transients and any new transient properties you add (should not be necessary for newer Grails versions)
- the "searchable" property convention must be defined as per Grails Searchable plugin, to add any fields that should be indexed for searching
Controlling the editing of your content type
To customize behaviour for the properties of the domain class, the "editors" closure property is used, like Grails constraints closure - but in this case how they are treated in the Weceem editor.For each field of the content domain class you can define the order in which they appear in the editor - based on the order in which they are listed in the editors closure.You can also set the editor used for each property. These have names like HtmlCode, ReadOnly, CssCode, GroovyCode etc. You can also define your own editors if the supplied editors are not suitable. See the section on customizing content editors.Some properties are for internal use only, for those you can set hidden:true.Finally, you can push your properties into the "Extras" editor group that is displayed separately by setting "group" to "extra":static editors = {
isbn(editor:'ISBNEditor')
genre(editor:'GenrePopup', group:'extra')
Displaying content as text or HTML
For various reasons such as search results and versioning, Weceem needs to be able to get some kind of representation of your custom content as both plain text and HTML. If your needs are different from the defaults, you can override the methods for getContentAsText and getContentAsHTML.The default behaviour is:/** * Must be overriden by content types that can represent their content as text. * Used for search results and versioning */ String getContentAsText() { "" } /** * Should be overriden by content types that can represent their content as HTML. * Used for wcm:content tag (content rendering) */ String getContentAsHTML() { contentAsText ? contentAsText.encodeAsHTML() : '' }
Defining mime type
Weceem's content controller will ask the content for its MIME type when building the response. In many content types this is a constant value and is not persisted, but in types such as WcmContentFile this is persisted so that the type can be overridden by the user.To return the correct MIME type, just implement the getter:String getMimeType() { "text/html" }
Customizing rendering of your content
Weceem delegates rendering of content to the content node itself. If there is no rendering implementation provided, it will simply render the node as text to the client. This mechanism is used by the built in content types to render themselves appropriately. Some types render HTML decorated by a template, and others issue redirects.There are two conventions that control the custom rendering of content:- Static "standaloneContent" property is set to false if content type is not intended for direct rendering. This prevents node types used by other types from being rendered when their URI is requested - e.g. is the built-in template and widget types, which make no sense being rendered on their own.
- Static "handleRequest" Closure property provides custom implementation of rendering if you require it.
static handleRequest = { content -> if (content.allowGSP) { renderGSPContent(content) } else { renderContent(content) } }
static handleRequest = { content ->
redirect(url:content.url)
}
Interacting with content events
Content nodes can hook into common events, such as deletion and updates. To do this your content class needs to implement methods for any events it wants to support. None of these are mandatory, and you can implement only those you need. The available event methods are defined in WeceemDomainEvents.groovyThe event methods are:boolean contentShouldBeCreated(WcmContent parentNodeOrNull)
void contentDidGetCreated()
boolean contentShouldBeDeleted()
void contentWillBeDeleted()
void contentDidGetDeleted()
void contentDidChangeTitle(String previousTitle)
void contentDidGetUpdated()
boolean contentShouldMove(WcmContent targetParent)
void contentDidMove()
boolean contentShouldAcceptChildren()
boolean contentShouldAcceptChild(WcmContent newChild)
Customizing the icon shown in the repository
You can supply an icon for your custom content types, that will be used in the admin UI of Weceem, e.g. in the repository view. To do this, define a static "icon" property which contains the arguments you would pass to Grails g:resource tag to create a link to the resource :static icon = [ plugin: "weceem", dir: "_weceem/images/weceem/content-icons", file: "widget-32.png" ]
Content versioning
In order to participate in Weceem's content versioning system, you need to add any properties you wish to have serialized to the getVersioningProperties() implementation of your class. This function returns a map of values that are stored in the content revision.These are simple values that must be expressible as strings. By necessity you may refer to objects not available at the time the user views your revision, so all complex object references that need to be versioned should be coerced to some kind of text representation.For a real example, here's the code from WcmHTMLContent :Map getVersioningProperties() { def r = super.getVersioningProperties() + [ menuTitle:menuTitle, htmlTitle:htmlTitle, keywords:keywords, template:template?.ident() // A string representation of template object ] return r }
6.3 Custom content editors
As mentioned in the create custom content type section, you can specify what kind of editor is used for each property of content types. By following some simple procedure, you can also create your own new editors.Weceem will locate the appropriate editor using the name of the type of the property, if a custom property editor name is not given.Content Editors
Weceem has quite a few editors that are available to edit all the predefined content type properties.Editor Field Name | Description |
---|---|
String | for single-line strings |
LongString | for multi-line strings |
Integer | for whole numbers |
Boolean | for boolean fields |
Date | for dates, includes datepicker and HH and MM fields |
Title | uses a larger font for editing the title |
Tags | UI for displaying and add/remove tags via ajax |
ContentFileUpload | renders a file upload field or file information if already uploaded |
RichHTML | HTML editing with rich editor |
HTMLContent | HTML content editor that checks allowGSP property on content and switches between RichHTML and HtmlCode as appropriate |
HtmlCode | Code editor with highlighting for HTML |
JSCode | Code editor with highlighting for JavaScript |
GroovyCode | Code editor with highlighting for Groovy code |
CSSCode | Code editor with highlighting for CSS code |
LanguageList | a language selection drop-down list |
WcmScript | For selecting any WcmScript node in the current space |
WcmStatus | For selecting any WcmStatus instance |
WcmTemplate | For selecting any WcmTemplate instance in the current space |
WcmSpace | For selecting a Space |
ReadOnly | a text-only rendering of the value with no editing capability |
ReadOnlyURI | a text-only URI value, using URL encoding of restricted chars |
ReadOnlyDate | a read-only display of a date |
Writing your own content editor
Creating a custom editor is simply a case of defining one or two Grails tags in the "wcm" namespace.Editors are resolved by looking for a tag with the target editor name appended to "wcm:editorField". So for example a new editor for choosing a colour might be called Colour and the tag would be called "editorFieldColour" :class MyWeceemTagLib { static namespace = "wcm"def editorFieldColour = { attrs -> out << someFancyColourPicker(pageScope.content[attrs.property]) } }
- property - property name of the content object for which the tag should render a field
pageScope.content[attrs.property]
Providing resources needed by custom editors
Some editors - e.g. the FCK and code editors used by Weceem, require JavaScript and CSS resources to be included in the <head> section of the editor page. To achieve this, Weceem looks for a tag with a name like "wcm:editorResourcesColour" where the type name is appended to "editorResources". This tag can output anything you like into the <head> section of the page:def editorResourcesTags = { attrs -> out << g.render(template:'/editors/tags_resources', plugin:'weceem', model:[name:attrs.property, value:pageScope.content[attrs.property]]) }
6.4 Custom request/form submission with groovy scripts
Capture data from forms and perform an action, such as sending mail after submission of a contact form by writing some Groovy code that you store in the content repository. This happens dynamically and you don't need to redeploy your Weceem application or restart it.Two content types work together in Weceem to enable you to write arbitrary server-side code. The Scripted Action node type is used to receive requests, and delegates the work to a Groovy Script node.For example, to create a Contact form that sends mail to a known address, you could implement it like this :- Create a Groovy Script node in your repository, and paste into it the following code :
def args = [ recipients:'admin@yourserver.com', subject:'Message from website visitor' ] try { sendMail { to args.recipients if (args.ccRecipients) { cc args.ccRecipients } subject args.subject from params.senderAddress body params.message } redirect(uri:params.success) } catch (Exception e) { log.error "Can't send mail", e redirect(uri:params.error) }
6.5 Embedding content
In some custom applications you may want to embed some Weceem content within your own Grails GSP views. This is easily achieved for most situations using the new wcm:render tag as of release 1.1.However, if you need to render content with more control over the process - e.g. using an artificial request/response to render content for use in emails, you can use the wcmRenderEngine bean.This bean offers methods for rendering a specific content node.You must typically pass in a request and response to use - given that most of Weceem functionality uses Grails tags and these require features of requests and responses - although you can usually also pass in your own controller instance if you have one.See the source code for RenderEngine on github for more details.6.6 Exporting and importing content
Each Weceem space (repository) can be individually exported and imported individually and is useful for backups or migrating content between servers or spaces.For this :- Go to the Administration section
- Select option - "Spaces"
- Click the Export or Import buttons
- Select space when prompted.
6.7 Hooking into content events
In custom Grails applications that use Weceem plugin you may need to perform actions when certain events occur on content.To do this, register a listener with the WcmEventService and implement any of the event notification methods you require.First, you inject the WcmEventService using Grails dependency injection, or obtain it via the Spring ApplicationContext, and then call the "addListener" method :class MyService implements InitializingBean { def wcmEventService void afterPropertiesSet() { wcmEventService.addListener(this) } void contentDidGetUpdated(WcmContent node) { log.info "Look! Content ${node.title} was updated" } }
package org.weceem.eventimport org.weceem.content.WcmContentclass WeceemEvents { static events = { /* Called just before saving the node */ contentWillBeCreated { WcmContent contentNode, WcmContent parentNode -> } /* Called after a new node has been created */ contentDidGetCreated { WcmContent node -> } /* Called before a node is deleted */ contentWillBeDeleted { WcmContent node -> } /* Called after a node is deleted */ contentDidGetDeleted { WcmContent node -> } /* Called after a new node is updated */ contentDidGetUpdated { WcmContent node -> } /* Called after a new node is moved to a new parent */ contentDidMove { WcmContent node, String previousURL, WcmContent previousParent -> } } static { EventManager.define(WeceemEvents) } }
package org.weceem.eventimport org.weceem.content.WcmContentclass WeceemDomainEvents { static events = { /* Called before a new node is created. Return true to allow the creation */ contentShouldBeCreated { WcmContent parentConent -> } /* Called just before saving the node */ contentWillBeCreated { WcmContent parentConent -> } /* Called after a node has been created, so that it can intialize anything that depends on it */ contentDidGetCreated() /* Called to see if a node can be deleted. Return true to permit the deletion */ contentShouldBeDeleted() /* Called before a node is deleted. */ contentWillBeDeleted() /* Called after a node is deleted. */ contentDidGetDeleted() /* Called after a node's title has changed (edited). */ contentDidChangeTitle { String previousTitle -> } /* Called after a node is updated (edited). */ contentDidGetUpdated() /* Called before a node is moved to a new parent. Return true to permit the move. Parent may be null */ contentShouldMove { WcmContent newParent -> } /* Called after a node is moved to a new parent. */ contentDidMove { String previousURL, WcmContent previousParent -> } /* Called to establish whether this node can have children. Return true to permit addition of children */ contentShouldAcceptChildren() /* Called before a node is moved to become a child of this node. Return true to permit addition of the new child */ contentShouldAcceptChild { WcmContent possibleChild -> } } static { EventManager.define(WeceemDomainEvents) } }
6.8 Page rendering
When a request is made to Weceem, it goes through a number of steps to produce the final content.- Weceem first locates the content at the URI path specified in the request
- Once located, it checks if content can be rendered. Content which is not "standalone" and used by other content cannot be rendered directly (e.g. Templates cannot be rendered directly).
- Next, it checks to see if the content implements rendering itself. If it does, it will call the content's handleRequest method to perform the rendering of the response.
- If there is no handleRequest method, Weceem will look for a "template" property on the content object and if it finds one, will ask that template to render the content.
Default document handling
Weceem supports httpd-style request URLs that end in /, it will automatically check for a child node of the URI, with one of the default names ("index.html" or "index"), much like a regular webserver.6.9 Integrating the authentication and authorisation system
The Weceem plugin neither provides an authentication mechanism nor depends on authentication plugins. Just wire-in whichever authentication system you want to use - e.g. the Weceem CMS Application wires up the Weceem plugin to the Spring Security plugin for authentication and provides a simple user management UI as part of the Weceem Admin.Customizing AuthenticationThe WcmSecurityService class has simple methods on it that the CMS uses to get information about the current logged-in user. To customize this, just replace or modify the delegate property on the service - e.g. to plug in Acegi (note that Spring Security replaces Acegi - see below for an off the shelf solution), you would place code like this in the BootStrap of your application :def authenticateService = context.authenticateServicecontext.wcmSecurityService.securityDelegate = [ getUserName : { -> def princ = authenticateService.principal() if (princ instanceof String) { return null } else { return authenticateService.principal()?.username } }, getUserEmail : { -> def princ = authenticateService.principal() if (princ instanceof String) { return null } else { return authenticateService.principal()?.email } } getUserRoles : { -> authenticateService.userDomain()?.roleAuthorities ?: ['ROLE_GUEST'] }, getUserPrincipal : { -> authenticateService.principal() } ]
Integration with Spring Security
If you create your own application using Weceem plugin and wish to use the Grails Spring Security plugin, install the weceem-spring-security plugin to provide the bridge between Weceem and your application's Spring Security authentication/authorization scheme.When you use this plugin there is no need to write your own UserDetailsService for the Spring Security plugin.To do this, after installing the plugin you simply have to specify some configuration to tell the plugin how to map from your Spring Security domain classes, and the plugin will make this information available to Weceem :weceem.springsecurity.details.mapper = { -> [ // Stuff required by weceem spring sec username: username, password: password, enabled: enabled, authorities: authorities, // optional stuff we add email: email, description: description, firstName: firstName, lastName: lastName, id: id ] }
6.10 Styling With Templates
Templates are content nodes used to set page layouts on your site. Certain content types such as HTML and Blog, support setting a template when creating or editing nodes. If a template is not set, it is inherited from the content's ancestors.Typically the template will include the head and body tags, pull in the CSS and other resources required, and provide some common layout elements.They use standard Grails tags and Weceem tags to render content within the body of the page, as well as meta data about the current page e.g. dynamic titles.Here's a simple example that renders a dynamic title and renders the content of the current node.<html>
<head>
<title>${page.title.encodeAsHTML()}</title>
</head>
<body>
<div id="content">
<wcm:content/>
</div>
</body>
</html>
<wcm:content/>
tag is used to render the content of the page the user requested. Its important to remember that your Template is not actually requested by the user by URI - it is simply used to decorate the content the user requested.You might add a news panel to this layout, by using the <wcm:widget>
tag to render a reusable block that iterates over children of the "News" node to list the titles of articles and links to them.