Template Development

At this page you find a hands on guide for Sophora-Template development.

Table of Contents

Composition of the Web Project

The empty web project provides only a default skeleton of the following files and directories:

PathDescription
src/main/webappDirectory for web content
src/main/javaJava source directory. Models an functions should go here
src/main/webapp/META-INF/context.xmlContains the reference to the local configuration directory of the delivery. This directory should be located apart from the web project and must contain the sophora.properties file. It holds required information about the server connection, caching, mode of operation and further delivery settings.
src/main/webapp/WEB-INF/web.xmlThe actual deployment descriptor for configuring the filters, filter mappings and listeners.
src/main/webapp/WEB-INF/libContains the required libraries (com.subshell.sophora.delivery and its dependencies).
src/main/webapp/WEB-INF/sitesContains the global sophora.properties file and the site specific configurations. The global settings are overwritten by the information within the local sophora.properties file (see above). For each site that should be dealt with there has to be an according .properties file in this very directory.
src/main/webapp/WEB-INF/tagsContains project specific tags in form of .tag files. New tags can be stored here.
src/main/webapp/WEB-INF/templatesContains the used templates grouped by document types. For each handled site there has to be a subdirectory containing a templates.xml file each. It is also possible to put a root templates.xml directly into the templates folder. All other templates.xml files inherit from the root templates.xml.
src/main/webapp/For each site that should be dealt with a directory with corresponding name is required. It will later contain the actual (JSP) templates.

Delivery with JSP

To deliver a document two things are required:

  1. A JSP template which generates the view that is finally presented to the visitor, commonly an HTML page.
  2. A central template configuration: the templates.xml file.

Exemplary Behaviour of the Delivery

The following pattern describes the composition of a URL within Sophora:

[/<Name of Website>][/<Path of structure node>/]<sophoraId of document>[-<Template type>]
[_<urlParameterName1>-<urlParameterValue1>].<File ending>

Accordingly, a typical URL looks like this:

http://www.sophora-delivery.com/training/sophora/development/firststeps100.html

When a such URL is called, the Sophora server first checks, whether the requested address points to a real file within the web project. If there is no such file, it is assumed that the given URL relates to a Sophora document. Thereby, the string "training/sophora/development" is interpreted as a structure path where the requested document is assumed to be located. When a structure node exists that matches this path, the repository is searched for the Sophora ID "firststeps100". If a corresponding document is available, the delivery uses the template that is associated within the templates.xml file, based on the document type of the found document. Since no template type is specified in the above URL, the default type is taken. Before the template is actually invoked, several page context variables are set.

Sophora URL Parameters

Sophora URL Parameters are optional parameters for the template which are, in contrary to query parameters, respected by the caching mechanism, thus different URL parameters will provide different cache entries. These parameters are inserted into the URL inbetween the Sophora ID and the file extension in the following pattern: _<ParameterName>-<ParameterValue>.
The appearance of an overall URL including parameters depends on the used URL-Codec.

Example URL for NoNumbersUrlCodec:

http://www.sophora-delivery.com/training/sophora/development/firststeps100_ParameterName-ParameterValue.html

Example for DefaultSophoraUrlCodec

http://www.sophora-delivery.com/training/sophora/development/firststeps100~_ParameterName-ParameterValue.html

For more information about UrlCodecs, see the according section on URL schemas in the templates.xml documentation.

For accessing a parameters value from the template, use either

${urlParam.Parametername}

or

${urlParam['Parametername']}

There are certain limitations what those parameters and theirs values may be composed of:

  1. The following characters are forbidden generally: '_' (underscore), '.' (dot), '/' (slash), '\' (backslash), '#' (hash), '?' (question mark).
  2. The name of the URL parameter must not contain '-' (dash). On the contrary, a parameter's value may contain such a character.

In addition it is not recommended to use the tilde character ('~') within parameters or values. In general, symbols that should be used within URL parameters or values, need to be valid characters for file names of the operating system at hand since these parameters are also used to create according cache entries.

Additional Attributes within the urlParam Object

There are additional attributes within the urlParam map that you may access, but which are not part of the URL. These attributes are set by an internal filter.

NameDescription
sophora:idSophora ID of the requested document
templatetypeType of the used template as given in the templates.xml file
templatepathThe path, where the document was requested
suffixSuffix of the requested page; e.g., "html"

Creating a JSP Template

  • Scenario: JSP to display documents of type "story"
  • Template name & directory: src/web/<sitename>/story.jsp

The Sophora Tag Library

First of all, you need to include the Sophora Tag Library as shown subsequently:

<%@ taglib uri="http://www.subshell.com/sophora/jsp" prefix="sophora" %>

Sophora's tag library is used to develop JSP templates that display the managed content. JSP helps to separate business logic from presentation by the usage of tag libraries. For an optimal separation custom libraries are developed constantly. The usage of tag libraries has the advantage that developers may easily build new JSP templates without any knowledge about the underlying data model in Sophora. By using (multiple) tag libraries this separation can be optimised so that templates can be written without using any Java code but use only well-specified tag libraries. The concept of a tag library is defined in the JSP specification. Two types of tag libraries exist:

  1. Tag libraries that consist of a collection of Java classes and a tag library descriptor (TLD).
  2. Tag libraries that consist of a collection of tag files. These tag files contain JSP code which itself may use other tag libraries. This kind of a tag library doesn't need a TLD since the corresponding description is located in the tag files.

The tag classes are responsible for reading from and writing to the repository. The tag libraries are designed so that the basic functions are reusable across multiple websites.

Documentation

The documentation of the Sophora JSP Tags can be found here-

Page Context Variables

Within a template, you may access the following request scoped variables.

Don't overwrite these variables.
Variable NameContent
thisDeprecated: Use current instead
The default content map: A map that contains the available properties and childnodes of the current document.
Note when using Tomcat 7, "this" is a reserved keyword and my not be used as an identifier in JSP templates. See Configuration > Notes on Tomcat 7 for details
contentDeprecated: Use current instead
Same as "this", but you can change the variable name by setting the property
sophora.delivery.render.defaultContentMapName
currentUse this content map/model to get the features of functions and models. By setting the property
sophora.delivery.render.contentMapName, you can change the variable name of the map/model. Beside many new features,
this variable enables automatic access to referenced documents, without using an additional tag.
currentStructureNodeUuidThe UUID of the structure node the requested document is located at.
urlParamA map that contains the URL parameters that have been committed with this request.
sophoraDeliveryChannelNameThe name of the channel that is accessed. This is not set, when the default channel is used.
currentStructureNodeUuid is not the UUID of the structure node where the delivered document is located at. It is the UUID of the structure node at which the document has been requested (labelling)

Accessing Documents

For every requested page, there is a corresponding document in Sophora which is available within the template as the page context variable "current". The access to its properties, childnodes or structure info happens with the JSTL Expression Language and with JSTL core Tags.

For develop new templates the IContentMap implementation should be used. The old LazyContentMap is deprecated, and there can be problems in the template development.
You can change the default name of the context variable by overwriting the property sophora.delivery.render.contentMapName in the configuration.

Accessing a Document's Properties

Default Notation
Since the document is available in form of a map, you can access it with the JSTL Syntax. For instance:

${documentMap['propertyName']}

The keys are full qualified names of the properties of the document types as given in their CNDs (for details see page Configuration of Document Types). Thus, the title of some text document might be accessed by

${documentMap['sophora-content:title']}

Abbreviated Notation
There is also the possiblity to declare namespaces to use an abbreviated notation to access a document's properties. Hereby the namespace can be omitted within the actual statements and you can retrieve the title of a text document in a shorter way:

${documentMap['title']}

or even shorter:

${documentMap.title}

These namespaces have to be provided within the sophora.properties files as comma separated list for the property sophora.delivery.defaultNamespaces. For the previous example this might look like this:

sophora.delivery.defaultNamespaces=sophora-content

If multiple namespaces are used:

sophora.delivery.defaultNamespaces=sophora-content,sophora-extension

Property Types
Analogous to the property types of document types, the types of the properties from the document map (the variable current) are either String, Boolean, Long, Double or Date objects. Multiple properties are available as List objects.

Accessing Richtext Fields

A special kind of String property are the richtext fields, which contain some elements which are custom to Sophora.

Markups for Richtext

Richtext which is written in the Deskclient is saved in a special format within the repository. Some elements like bold text or abbreviations are stored in the repository as XML, whereas structural components like paragraphs are saved as documents with a special type. The information about how the individual elements of the richtext are saved is relevant, when the text is rendered on the website. An introductory example for converting richtext to HTML is the "formatTextMap"-Tag of the Sophora Demosite.

The following table gives an overview of all available richtext elements and their XML / HTML markups.

Richtext ElementXMLAttributes
Bold<strong></strong>
Italic<em></em>
Link<a href="" externalId="6c5cc980-73b2-11e3-981f-0800200c9a66"></a>externalId: The external id of the linked document. For external links the referenced document will be a link document.
href: The anchor name of a anchor link
name: Defines an anchor target
List<ul>
<li></li>
</ul>
Line Break within a paragraph<br/>
Paragraphno markup: Childnode of type sophora-extension:paragraph with property sophora-extension:style = "paragraph"
Paragraphboxno markup: Childnode of type sophora-extension:paragraphbox
Headlineno markup: Childnode of type sophora-extension:paragraph with property sophora-extension:style = "headline"
Table<table summary="">
<caption></caption>
<tbody>
<tr>
<th></th>
</tr>
</tbody>
</table>

The table cells can contain markup, e.g. bold, italic, links etc. Subtables and lists within table cells are not possible.
summary: This attribute is equivalent to the HTML summary attribute
caption: The headline of the table.
HTMLno markup: Childnode of type sophora-extension:paragraph with property sophora-extension:style = "html". Text entered in the DeskClient is saved as is, no sophora markup is used.
QuoteInternal representation:
<markup type="cite">A quotation</markup>

Translates to HTML:
<cite>A quotation</cite>
type="cite": Defines this markup as a quote.
LanguageInternal representation:
<markup type="language.*">Text in a language</markup>

Translates to HTML:
<span lang="*">Text in a languange</span>
type="language": Defines this markup as a language. The wildcard '' is replaced by the actual region code (e.g. 'de' or 'en')
AbbreviationInternal representation:
<markup type="abbr" abbreviation="Abkürzung" correspondent="">Abk.</markup>

Translates to HTML:
<abbr title="Abkürzung">Abk.</abbr>
type="abbr": Defines this markup as an abbreviation.
abbreviation: The abbreviation described by this markup.
correspondent: The actual meaning of the abbreviation.

Element Hierarchy

All XML elements of the table above occurr in a defined nesting hierarchy. The following list shows the elements ordered according to their precedence:

  1. List
  2. Link
  3. Markup: Quote, Language, Abbreviation
  4. Bold and Italic

This precedence list determines which XML elements are nested in other elements. For example 'links' have a higher precedence than 'markups'. So when a text is a link as well as a quote, the markup element will be nested within the link element.

Accessing a Document's Childnodes

Childnodes exist as List objects within the document map. Usually, there are two different type

1. Reference Lists
The first type is a list of document references. For instance, a teaser image (node type sophora-content:image) is an list of document references. Even though the teaser image has only one element, it is available as list with the actual image reference at position 0. Written in JSTL syntax, this childnode can be accessed by

${documentMap['sophora-content:image'][0]}

or by

${documentMap.image[0]}

or access to first ChildNode without specifying the index

${documentMap.image.alttext}

If the namespace has been declared for the abreviated notation (see above). When its likely that a reference list contains more than just one element, its recommmended to use the forEach tag of the core tag lib:

<c:forEach items="${documentMap.image}" var="imageDocument">
    ${imageDocument}
</c:forEach>

Please note, that forEach uses the Iterator-Implementation of documentMap.image. This means if you want to assign a variable to the list of childnodes and use them multiple times, you have to use the DefaultFunction asList:

<c:set var="images" value="${documentMap.image.asList}" />
<c:forEach items="${images}" var="imageDocument">
	${imageDocument}
</c:forEach>
...
<c:forEach items="${images}" var="imageDocument">
	${imageDocument}
</c:forEach>

2. Group Lists
The second variant is a list of groups. Here, each group contains a list of document references, available under the name sophora-content:teaser. Due to this second 'layer' an additional forEach statement is required to access the actual document. Thus, the outer forEach iterates over the individual groups and the inner one iterates through the document of the current group:

<c:forEach items="${documentMap.teasergroup}" var="group">
    <c:forEach items="${group.teaser}" var="teaserDocument">
        <%-- Accessing the teaser document --%>
        ...
    </c:forEach>
</c:forEach>

Loading a referenced Document

The deprecated content map "this"

In the old content map, you have to be aware that childnodes never contain an entire document but only a reference. This reference node type is comprised of the UUID of the referenced document and the overwritable properties (see section "Component Details" within the user guide).

To get the whole document, including the overwritten properties, use the sophora:getDocument tag, providing the reference node as value of the referenceNode parameter:
...
<c:forEach items="${group.teaser}" var="teaserRef">
<sophora:getDocument referenceNode="${teaserRef}" var="teaserDoc"/>
<p>${teaserDoc.title}<br/>${teaserDoc.shorttext}</p>
</c:forEach>
...

An example how to get a referencing document, including the overwritten properties using the new variable current.

...
<c:forEach items="${group.teaser}" var="teaserDocument">
    <p>${teaserDocument.title}<br/>${teaserDocument.shorttext}</p>
</c:forEach>
...

Detecting Empty Lists and Null Properties
The if tag of the core tag lib in combination with the JSTL operator empty offers a possibility to prevent access to empty properties or lists. For example, only show the subtitle (property) if present:

<h1>${documentMap.title}<h1><br/>
<c:if test="${!empty documentMap.subtitle}">
    <h2>${documentMap.subtitle}<h2><br/>
</c:if>

In exactly the same way you can use this in combination with childnodes: Only show a teaser image, if the corresponding childnode is non-empty:

<c:if test="${!empty documentMap.image}">
    <%-- Image logic goes here --%>
    <img href="...
</c:if>

Default functions of the Sophora Content Map

On content maps of documents, there are properties with default functions. Also, there are some functions that can be used in combination with existing property-/childnodenames.

Node function for the document map:

NameFunctionExample
isArchiveReturns true, if the document is archived. False otherwise${documentMap.isArchive}
hierarchyDocumentBy using this function, you can get a property or a child node from the hierarchy document of the structure node where the document is located. If the given value is not found, the function tries to get the value from a hierarchy document in higher structure level.${documentMap.hierarchyDocument.PROPERTY}
${documentMap.hierarchyDocument.CHILDNODE}
structureNodeDocumentBy using this function, you can get a property or a child node from the structure node document of the structure node where the document is located. If the given value is not found, the function tries to get the value from a structure node document in higher structure level.${documentMap.structureNodeDocument.PROPERTY}

${documentMap.structureNodeDocument.CHILDNODE}
uuidSimple code for getting the UUID of a document${documentMap.uuid}
primaryTypeSimple code for getting the documentType of a document${documentMap.primaryType}
toModelReturn a new content map/model based on current content map${documentMap.toModel['modelName']}
isImageVariantDisabledReturn true/false if a image variant is disabled in image document${documentMap.isImageVariantDisabled['imageVariantName']}
isDefaultDocumentReturns true, if the document is referencing by a structure node as default document${documentMap.isDefaultDocument}
referencingStructureNodeUuidReturns the structure node uuid of referencing structure node, if the document is the default document${documentMap.referencingStructureNodeUuid}
inheritedComponentsReturn a new content map which contains all inherited components. The deactivated inherited components are not included in the resulting content map.${documentMap.inheritedComponents}
yellowDataReturns the Yellow Data of the given type for a document.${documentMap.yellowData['typeName']}

Childnode functions of the document map:

NameFunctionExample
includeInvalidWhen accessing childnodes, per default only references to valid documents are returned. References to documents which are offline or deleted will be omitted. To override this behaviour and access all childnodes, including invalid ones, use the function includeInvalid in combination with the desired childnode name.<%-- Iterates over all paragraphlink references, including references to invalid documents --%>
...
asListIn case a property and a childnode share the same name, this function forces the content map to return the childnode. Otherwise, the property would be returned.${documentMap.aChildNode.aslist}
sizeReturns the number of elements of a childnode${documentMap.aChildNode.size}
toModelReturn a new content map/model based on referenced content map${documentMap.aChildNode.toModel['modelName']}

Property functions of the document map:

NameProperty TypeFunctionExample
asStringAnyReturn the String for the given Property. Use this function if there is a ClassCastException for getting the String of a ContentValue. This can happen by using third party libraries.${documentMap.property.asString}
asDateValueStringReturn the DateValue for the given String-Property${documentMap.property.asDateValue}
createTagAnyReturn the given property in the specified html tag. It can be placed further attributes for the tag. The default tag is a "div".
You will need that function, for getting an easy support for the live preview feature. If the delivery is configured as preview, there are additional information about the given property written in the tag:
data-sophora-uuid -> UUID of the used document
data-sophora-property -> Used property name
${documentMap.aProperty.createTag['div id="topline"']}
${documentMap.aProperty.createTag}
${documentMap.aProperty.createTag['p']}
existAnyReturns true if the property is set in the document map.${documentMap.property.exist}
keepSophoraMarkupStringWhen accessing a (Richtext-)Property, all Sophora Markup is automatically replaced by corresponding HTML. By using this function, the original Sophora Markup is returned.${documentMap.aTextProperty.keepSophoraMarkup}
labelString / Select ValueReturn the label of a select value property. Without this extension the key of the given select value is returned.${documentMap.aSelectValueProperty.label}
selectValueLabelPathStringReturns the select value path of a select value property. By default the separtor " / " is used. A custom separtor may be passed to the function${documentMap.aSelectValueProperty.selectValueLabelPath}
selectValuePathList<SelectValue>Returns the select value path of a select value property as a list of the corresponding select value objects.${documentMap.aSelectValueProperty.selectValuePath}
referencedDocumentIContentMap / List<IContentMap>Return the IContentMap for the referenced document of a select value property. if its a multivalue property a list of IContentMap will be returned.${documentMap.aSelectValueProperty.referencedDocument}
removeSophoraMarkupStringThis suffix removes the Sophora Markup completely from the text. (This only affects markup-Tags and their corresponding HTML elements. Other HTML tags contained in Richtext, like bold, italic, list, linebreak, anchor, table, etc. are not affected.)${documentMap.aTextProperty.removeSophoraMarkup}
removeTextLinksStringThis suffix removes the Sophora Textlinks completely from the text. (This only affects a-Tags with the externalId attribute. Other HTML tags contained in Richtext, like bold, italic, list, linebreak, anchor, table, etc. are not affected).${documentMap.aTextProperty.removeTextLinks}
removeSophoraMarkupAndTextLinksStringApplies both of the above remove* functions. Use it to get a clean text without any unexpected HTML/XML elements. (This only affects tags specifically used by Sophora. Other HTML tags contained in Richtext, like bold, italic, list, linebreak, anchor, table, etc. are not affected)${documentMap.aTextProperty.removeSophoraMarkupAndTextLinks}
replaceSophoraMarkupStringAll Sophora Markup of a (richtext) property is automatically replaced by corresponding HTML.${documentMap.aProperty.replaceSophoraMarkup}
replaceTextLinksStringConverts the internal Sophora Textlinks (which refer to documents by their external id) of a (richtext) property into valid HTML links (referring to a web page). You can add additional attributes by adding a parameter in squared brackets.${documentMap.aProperty.replaceTextLinks}
${documentMap.aProperty.replaceTextLinks['class="aLink"']}
replaceSophoraMarkupAndTextLinksStringApplies both of the above replace* functions. This is the default for all String properties.${documentMap.aProperty.replaceSophoraMarkupAndTextLinks}
${documentMap.aProperty.replaceSophoraMarkupAndTextLinks['class="aLink"']}
dateDateReturns the date value as a Date object.${documentMap.property.date}
isoDateReturns the date value as an ISO-8601-formatted string.${documentMap.property.iso}
format string'DateReturns the date value as a string with the given format. For format options, see org.joda.time.format.DateTimeFormat.${documentMap.property['yyyy-MM-DD']}
doubleDoubleReturns the double value as a Double.${documentMap.property.double}
longLongReturns the long value as a Long.${documentMap.property.long}
binaryDataBinaryData (BinaryReferenceValue)Returns a IBinaryData-Object${documentMap.property.binaryData}
createHtml5FormString (input field type FormWizard)Returns a String which contains a simple HTML form. The property must contain a form specification created by a form wizard field. Else an empty String will be returned.${documentMap.property.createHtml5Form}
toFormString (input field type FormWizard)Returns the model class OnlineForm for the stored text. If the field does not contain a form specification created by a form wizard field, an empty OnlineForm will be returned.${documentMap.property.toForm}

Implementing Custom Functions

Custom functions may be implemented for properties, nodes, and child nodes. For example, consider the following (somewhat contrived) example:

The intention is that each property gets a new function named "replaceVowels". In this case, we're taking the "headline" property of some document, replacing all vowels with the string "xxx", and output the result. The following is the complete implementation of the "replaceVowels" function:

import com.subshell.sophora.api.content.IProperty;
import com.subshell.sophora.delivery.content.model.annotation.PropertyFunction;
import com.subshell.sophora.delivery.content.model.function.FunctionParameter;
 
public class PropertyFunctions {
    @PropertyFunction
    public String replaceVowels(IProperty property, FunctionParameter param) {
        return property.getString().replaceAll("[aeiou]+", param.getValue());
    }
}

The interesting bit is that you don't have to implement any interfaces at all. Instead, everything is annotation-driven. Also, you don't need to declare a pre-defined parameter list. You only need to declare the parameters you need.

For all custom java extensions (functions, models, url generation, etc.) an automatic reload is provided. When a compiled java class (in the directory WEB_INF/classes) changes, the Sophora Delivery notices this change and reloads the class automatically. To use this feature the automatic context reload of the web container (e.g. tomcat) should be switched off.

Function Types

The following are the supported types of functions:

AnnotationType of FunctionParameter
@PropertyFunctionWorks on a single property. 
@NodeFunctionWorks on a single node.useReferenceNode=true/false (default: false)
If you need the referenceNode of the ContentMap in a @NodeFunction, just use the useReferenceNode parameter. If the ContentMap contains a referenceNode, the INode Parameter of the function method will be the referenceNode otherwise it will be the node. 
@NodeCollectionFunctionWorks on a collection of nodes. 
Renaming Functions

The name of your custom function is derived from the name of the Java function. In the example, the function name to use in the JSP is "replaceVowels" just as it is written in the Java code. If for some reason the Java name can't be used, or if you want to make a function available under more than one name, you can do so like this (this works for all types of functions):

// renames the method to "replaceAllVowels"
// (NOTE: the original Java name will be ignored)
@PropertyFunction("replaceAllVowels")
public String doReplaceVowels() { ... }
 
// make this method available under the "nameX" and "nameY" names
// (as before, the original Java name will be ignored)
@NodeFunction({"nameX", "nameY"}
public int doZ() { ... }
Return Types

In your method, you can just return anything you like, be it a String, int or any complex object (as long as it can be used from within JSP/JSTL.)

Parameters

Your function Java method can take a number of parameters, but it doesn't have to. What parameters it can take depends on the type of function being implement

Parameter Type@PropertyFunction@NodeFunction@NodeCollectionFunction
INodeThe node that contains the property.The node or the referenceNode, if the parameter useReferenceNode is true and the referenceNode is not null.The parent node of the node collection.
NodeTypeThe nodetype of theabove node.
INode[][NO][NO]The node collection in question.
IPropertyThe property in question.[NO][NO]
StringThe name of the property. It is useful
when the requested property is not present
in the node, but nevertheless the property name
is needed. 
[NO][NO]
FunctionParameterDeclares that the function takes a single parameter.

If declared, the JSP would look like this:
${propertyOrNode.myFunction['param']}

If not declared, the JSP would look like this:
${propertyOrNode.myFunction}
IContentMapContext A context thatcan be used toget more information.
IContentMapcontentMap from current documentcontentMap from current documentcontentMap from current document

Again, you only need to declare the parameters you need. The order of the parameters is irrelevant. You can also specify supertypes if you don't need a specific type. For example, instead of declaring INode you can also declare just IContent.

Restricting Access

By default, all property functions and node functions are available for all node types. To restrict the set of node types these functions are available for, you can use the @NodeTypeRestriction annotation:

@NodeFunction
@NodeTypeRestriction("sophora-mix:document")
public boolean foo() { ...}
 
@PropertyFunction
@NodeTypeRestriction({"my-content:foo", "my-content:bar"})
public long bar() { ... }
Parsing of Richtext Properties

For property functions an annotation exists that replaces Sophora markup tags with valid HTML. It is named ReplaceSophoraMarkupAndLinks. If you add this annotation to your property function the text of the property will already be parsed to valid HTML. For details look at the function replaceSophoraMarkupAndTextLinks in the table 'Property functions of the document map' above.

Enabling Function Usage

To make your functions available for use in JSP, you need to add the sophora.delivery.function.packageNames key to your sophora.properties. Add the Java package names containing your function classes as a comma-separated list:

sophora.properties

sophora.delivery.function.packageNames=com.example.basicfunction, com.example.complexfunction, ...

Implementing Custom Models

While the default Sophora content map is useful for most projects, you might want to enhance its functionality to be able to use a more model-driven approach to templating. For example, suppose that your repository has a "video" node type, where each video consists of a number of "assets" of some sort. In JSP, you might want to use a "video" model that allows you to directly access the contained "asset" objects.

To use a model-driven approach, you need to 1.) extend the AbstractSophoraModel class, and 2.) add the @Model annotation:

import com.subshell.sophora.delivery.content.model.AbstractSophoraModel;
import com.subshell.sophora.delivery.content.model.annotation.Model;
 
@Model("myVideoModel")
public class VideoModel extends AbstractSophoraModel {
    public List<Asset> getAssets() {
        // ... get assets from somewhere...
    }
}

Instances of this class behave exactly the same as the content map, should you need that functionality. You can invoek the addtional methods like this in your JSP:

<c:forEach var="asset" value="${document.assets}">
    ... do something with the single asset...
</c:forEach>

You can also use the model by invoking the function toModel on a node or child node.

${document.toModel['myVideoModel']}
${document.aChildNode.toModel['myVideoModel']}

Please note that methods defined in your model class will always take precedence over the regular content map mechanism. This includes property/node/node collection functions as well.

Using Spring Dependency Injection in Models

The sophora delivery creates instances of the model objects on a per request basis. This is done in a spring-like fashion supporting constructor and field injection. If an instance of a model object is needed then:

  • The most specific constructor is determined
  • If the contructor has arguments they are filled by their type with the content map or beans from the current spring application context
  • The setter for IContentMap is called
  • The model object is initialized afterwards (thus performing field injection through the current spring application context and calling methods annotated with PostContruct)

It is not necessary to annotate a contructor with Autowired or to have any constructor arguments after all.

There are several possibilities to control what the current spring application context is. By default this is the delivery internal spring context with access to some default beans, especially the sophora client.

In case you want to use your own application context you first have to integrate Spring into your web application. Please refer to the Spring documentation for that purpose. It is also possible to use the Spring MVC framework alongside a sophora delivery. Just consider that the suffix .servlet is in the list of the noCachingSuffixes and that the URL mapping of the Spring DispatcherServlet points to a path within one of your sites.
In order to make your context available to the Sophora Delivery, just use the com.subshell.sophora.delivery.api.context.DeliveryContextLoaderListener instead of the regular org.springframework.web.context.ContextLoaderListener in your web.xml. This will register your spring application context as the default spring application context.

It is also possible to use one root context and a separate spring context for each site. This is useful should different beans or configurations be applied to different sites. Each context must be registered at the sophora delivery. A context is registered for one or multiple structure nodes. It is then used for the models of all documents, which are requested recursively under its registered structure nodes. So the choice of which context to use is determined by the requested URL and not by the structure node where the requested document is actually located.

A typical implementation of registering an ApplicationContext looks like this:

@Component
public class ContextBridge {
 @Autowired
 private ApplicationContext appContext;
 @PostConstruct
 private void setContext() {
 IStructureNode demosite = DeliveryUtils.getSophoraClient().getStructureNodeByPath("/demosite", (UUID) null);
 DeliveryUtils.registerApplicationContext(demosite.getUUID(), appContext);
 }
}
Implementing IContentMapExtended

The interface IContentMapExtended offers additional methods to be used directly on a IContentMap-Object. These are getPrimaryType(), getUUID(), toModel(String name) and getStructureNode(). Each method has a default implementation based on getNode() and getContext(). Thus they don't have to be overridden. After implementing IContentMapExtended the new methods can directly be used, for example in a model.

@Model("myVideoModel")
public class VideoModel extends AbstractSophoraModel implements IContentMapExtended{
	// valid model class so far
}
Enabling Model Usage

To make your model classes available for use in JSP, you need to add the sophora.delivery.model.packageNames key to your sophora.properties. Add the Java package names containing your model classes as a comma-separated list:

sophora.properties

sophora.delivery.model.packageNames=com.example.model1, com.example.model2, ...

Also, you have to insert the name of the model in the templates.xml for the templates, in which you want it to be used automatically.

<nodetype name="demo-nt:video">
	<templateset extends="default">
		<template type="default" model="myVideoModel">/demosite/templates/exampleTemplate.jsp</template>
	</templateset>
</nodetype>

URL Generation

There are some issues to consider in order to generate URLs and HTML anchor tags to link to other sophora documents. For the template development it is important to distinct between links that are included in copytext or richtext (these are referred as text links in the following) and between links which are generated in a jsp template.

Basically an URL is generated with the url tag of the Sophora tag lib. A typical usage is denoted in the following code snippet:

<sophora:url var="url" uuid="${document.uuid}"/>

The generated url may be used for the href attribute of an anchor tag. If the linked document is a document of the same site, the generated url is relative to the site. As the urls differ in the cases of a mounted and unmounted delivery, the url is generated accordingly.

Breaking changes concerning text links in version 1.50

Until version 1.50

Text links were stored in the internal sophora markup in the following format: <link position="n">text</link>. The generation of the HTML anchor tags was provided by each sophora web application.

After version 1.50

Text links are stored in the internal sophora markup in the following format: <a href="" externalId="externalId">Text</a>. The generation of the HTML anchor tags is provided by the sophora delivery itself. The url of a document is determined and added to the href attribute, while the externalId attribute is deleted. This translation is transparently done during the access to the property value. But note that this applies only for the ContentMap and not for the deprecated LazyContentMap. If the default sophora mechanisms to generate the anchor tags is not sufficient for your requirements, it is possible to customise the generation by providing implementations of the interfaces com.subshell.sophora.delivery.api.links.IAnchorTagBuilder and com.subshell.sophora.delivery.api.links.IUrlGenerationStrategy.
The Sophora Server converts transparently all copytexts in the 'old' format to the 'new' format, so you do not have to deal with both formats.

Customisation of the URL and anchor tag generation

The generation of urls and anchor tags is typically adapted in great parts. In general there are the following kinds of adoptions in the url generation:

  • A custom suffix. E.g.for a download document, or a .png image
  • A template type
  • Query and Url parameters
  • Labeled deliveries
  • Providing of the domain of a document which is located in another site in sophora

These points addresses only the generation of the url itself, but the generation of the anchor tag is also adopted in great parts.

After version 1.50 it is best practice to implement these adoption with the help of implementation of the interfaces IAnchorTagBuilder and IUrlGenerationStrategy.
There should be at most one implementation of each interface in your web application. This implementations should be placed inside the function package, which is configured via the configuration key 'sophora.delivery.function.packageNames'. They are afterwards automatically used by the sophora delivery, without any further configuration. Please refer the javadoc documentation of the interfaces for further information.
This possibility to adopt the the url generation is recommended over a custom jsp logic, as the implementations of the interfaces IAnchorTagBuilder and IUrlGenerationStrategy are also used during the automatic generation of anchor tags of text links.

The adoptions are often dependent on the context. Therefore an arbitrary parameterization of the implementations is necessary. This is done with the help of the java tag context of the sophora tag lib. In the following code snippet a context parameter is used to decide if the anchor tag should have a prepended image:

In a .jsp file:

<sophora:context withIcons="${true}>
    ${copytextparagraph.text}
 </sophora:context>

Consideration of the context parameter withIcons

public class CustomAnchorTagBuilder implements IAnchorTagBuilder {
    @Override
    public String createOpeningAnchorTag(INode document, INode linkTarget, String url, IContentMapContext context) {
        Map<String, Object> contextParameters = context.getContextParameters();
        Boolean withIcons = contextParameters.get("withIcons") != null ?
                (Boolean) contextParameters.get("withIcons"): false;
        if (withIcons) {
            return "<img src=" + getImageSrc() + "/><a href=\"" + url + "\">";
        } else {
            return "<a href=\"" + url + "\">";
        }
    }
	@Override
	public String createClosingAnchorTag(INode document, INode linkTarget, IContentMapContext context) {
		return "</a>";
	}
	@Override
	public String createLinkText(INode document, INode linkTarget, String linkText, IContentMapContext context) {
		return linkText;
	}
}

Anchor tag generation

Thus, your implementation of the interface IAnchorTagBuilder is used for non text links, you should use the tag createAnchorTag of the sophora delivery tag lib, in order to create arbitray anchor tags.

If it is not possible to transfer your existing logic from jsp tags to java, it is still possible to use your jsp tags. But note that this possibility is not recommended and should only be used as a temporary workaround. You may use the following code snippet as clue for this approach.

<c:set var="paragraphText">${paragraph.text.keepSophoraMarkup}</c:set>
<rx:text id="tBodyText">${paragraphText}</rx:text>
<rx:regexp id="tRowExpr">//si</rx:regexp>
<rx:match id="tRowMatch" text="tBodyText" regexp="tRowExpr">
    <c:set var="hit"><rx:group number="0"/></c:set>
    <c:set var="hit2"><rx:group number="2"/></c:set>
    <c:if test="${!empty hit2}">
        <c:set var="externalID">${fn:replace(hit2,'"','')}</c:set>
        <sophora:getDocument var="linkDocument" externalid="${externalID }"/>
        <sc:createLinkWithCurrentMap documentMap="${linkDocument}" withIcon="true" var="replaceStartString"/>
        <c:set var="paragraphText" value="${fn:replace(paragraphText, hit, replaceStartString)}" />
    </c:if>
</rx:match>

Note that in this example the tag sc:createLinkWithCurrentMap is the custom unaltered tag from an earlier version than 1.50. It may be necessary to perform further transformations on the result, because of the usage of the function keepSophoraMarkup.

Linking to documents of another site

When a document of another site is linked, the url of the document must be absolute and not relative. So the domain of the corresponding site must be known by the delivery. This is respected by the sophora delivery, if the domain of the site is configured correctly.
Please make sure that the configuration key
sophora.delivery.site.example.domain=http://www.example.com
is present for each of your site.

Accessing Static Resources

Static resources are files provided by the web application. Usually, static resources like images, CSS or Javascript files are accessed in the following way.

<link href="/resources/css/style.css" rel="stylesheet" type="text/css" />

In development context, the tomcat webapp is accessed directly. In live context, the context path is hidden by the apache in most cases. Thus, urls of static resources may change depending on the setup. Assuming that the name of your website is "testsite", the path of a file "style.css" would look like this:

Delivery EnvironmentExemplary URL
localhttp://localhost:8080/sophora-webapp/testsite/resources/css/style.css
livehttp://www.sophora-delivery.com/resources/css/style.css

The sophora staticUrl tag keeps urls of static resources independent of the current environment, so that you do not need to customise the templates:

<link href="<sophora:staticUrl path="/resources/css/style.css"/>" rel="stylesheet" type="text/css" />

Which of these two variants is chosen, is determined by the configuration property sophora.delivery.render.isMounted.

Configuring the Templates within the templates.xml File

In order to set the template src/web/testsite/story.jsp as default whenever a story document is requested, you need to add an according entry to the central templates.xml file:

<nodetype name="sophora-content-nt:story">
    <templateset>
        <template type="default">/testsite/story.jsp</template>
    </templateset>
</nodetype>

Fragmentation into SSI

SSI (Server Side Includes) have the advantage that the very same, dynamically generated HTML snippet can be reused at several places and that the modification of such elements does not result in regeneration of the entire HTML page. SSI represent a space which is filled, for example, with content from another file, by the server during runtime. The server therefore creates an internal request that, if applicable, triggers the generation of the corresponding HTML snippet. Otherwise, the server inserts the cached content at the designated place. This internal request points to a relative URL within the server and initiates the same mechanisms as a page request from a web browser.

For an eased usage of SSI, the Sophora tag lib provides the sophora:getInclude tag. For example, if you want to include an entire document by using the template type "default" with the help of SSI, enter the following code:

<sophora:getInclude type="default" sophoraid="${document['sophora:id']}"/>  
Since SSI result in an internal request, it is no longer possible to access the Sophora URL parameters from the original request. To keep those parameters anyway, you have to pass them on explicitly. The sophora:getInclude tag therefore comes along with the optional attribute urlParams which expects a Map<String, String> as input. This map is then transformed into Sophora URL parameters in form of _parameterName-parameterValue.
<sophora:getInclude type="default" sophoraid="${document['sophora:id']}" urlParams="${urlParams}"/>

Of course you may also pass on any other Map<String, String> objects to be retained as URL parameters.

Query Parameters, SSI fragmentation and Caching
Please be advised that query parameters will only be considered at render-time. Once a fragment has been rendered and is stored in the cache, query parameters are ignored for further requests of that fragment. To stay aware of query parameters, either translate them to Sophora Url parameters, or do not cache the fragment (for example by including it using the suffix .jsp)

Caching

All cached files are managed with the help of a cache database. It contains information about which documents or structure nodes are referenced by the cached files.

When a new version of a document or structure node is published or when a document or structure node is set offline, is disabled or deleted, all connected files are removed from the cache. Thus, it is wise to compartmentalise the access to documents into several SSI fragments. Thereby you can assure that only sections of a HTML page that are actually affected by changes are generated again.

This lightweight structure is convenient when for instance, the title of a link that is attached to a page below an article changes. If that link has been included by SSI, only that small part needs to be rendered again instead of the entire article. Also, if the headline of a document is changed, it is needless to render the navigation. There are many more use cases, but you probably got the idea...

Nontheless, you should decide thoroughly about whether or not using SSI in the individual situations, since every SSI entails another request internally as well as another file within the cache directory. If the generation creates complex server requests, like when some parts of the teasers stem from a filter that operates upon a query, the 'outsourcing' of this section to a SSI is reasonable. The same applies when a document contains multiple extensive lists from which teasers need to be created.

Registration of content in the Cache Database

Calls that lead to a document's registration:

  • The default content map is always registered.
  • Every document that is requested by the sophora:getDocument or sophora:getContentMap tag is registered.
  • When accessing a list of references, like ${current.teaserlist} or ${current.documentbox.teaserlist}, all referenced documents are registered.

Calls that result in a structure node's registration within the cache:

  • If a structure node is directly accessed in the URL by invoking /<path>/index.html, the corresponding structure node is registered.
  • Every structure node that is accessed by the sophora:getStructureNode tag is registered.
  • All structure nodes that are retrieved by the getChildStructureNodes tag are registered.
  • All structure nodes that are retrieved by the sophora:getHierarchyPath tag are registered.

Exemplary SSI for a teaser list:
Within the main template "content.jsp" the teaser list will be rendered by SSI by applying the tag sophora:getInclude:

<%-- A document's regular content goes here ... --%>
<%-- Displaying a teaser list follows: --%>
<sophora:getInclude type="teaserlist" sophoraId="${current.id}"/>

We define the template file for the teaserlist-type include in the "templates.xml":

<template type="teaserlist">/testsite/teaserlist.jsp</template>

The actual "teaserlist.jsp" file contains only the jsp code for rendering the teaser:

<ul>
<c:forEach items="${current.teaserlist}" var="teaser">
    <li>
    <%-- Display of the teaser...--%>
    </li>
</c:forEach/>
</ul>

Time based flushing of cache entries

In some templates you might need to flush the cache periodically in order to enforce the regeneration of individual sections or pages. For example, if you use the search result from a filter within a document, it will not update automatically when the search result changes. Thus, one has to make sure that the corresponding HTML components are removed from the cache on a regular basis and that a new search run is triggered to keep the search result page up to date.

By using the sophora:addCacheEntry tag you can define a cache entry's lifetime after which it will be removed from the cache automatically.

<%-- Remove entry after 300 seconds --%>
<sophora:addCacheEntry time="300"/>

Invalidation of query results

An Html fragment representing the result of a query is usually cached, so that the query must not be executed on each request. Such a query could be the list of the newest documents of a certain type, with a certain tag and structure node. Regarding the caching of html fragments it is irrelevant if the query is based on JCR, SOLR or any other search engine. The cached html fragment should ideally be invalidated when the result of the underlying query changes. But unfortunately there is no practicable way to detect a changed query result. If one of the documents changes, which are part of the query result, the html fragment is invalidated through the normal dependency tracking of sophora. Just make sure that the html fragment has a dependency to each document of the query result. This is not necessarily the case. If the rendering of the entries of the query result is carried out by one SSI respectively, the html frame, containing the SSI directives has only property dependencies (like sophora:id, sophora:structureNode) to the entries of the query result. A full dependency to a document can be achieved by using the tag sophora:registerFullDependency. For the detection of new documents in the query result are cache expressions used.

Cache Expressions

An cache expression is a set of criteria which matches against documents. They are registered for cache fragmentes and are persistent in the cache database. Whenever a document changes, it is checked against all registered cache expressions. If it matches against a cache expression, the corresponding cache fragments are invalidated.

Cache expressions have the following syntax:

<criteria>;<criteria>;<criteria>

One criteria is either:

  • a property match like sophora-content:title=value1,value2
  • a nodetype match like nodeType=sophora-content-nt:story,sophora-content-nt:audio
  • a recursive structure match like structurePath=<UUID>

A document must match against all criteria, in order to match a cache expression. The criteria are connected by a logical 'and'. If a property or nodetype has multiple values, the values are connected by a logical 'or'. If you would like to use a semicolon in a criteria, it has to be escaped (\;). A recursive structure match expects the uuid of a structure node and matches all documents which are located at the specified structure node or at a sub structure node.

Cache expressions may be registred with the tag <sophora:addCacheExpression expression="...."/>. The cache expression must be build analogously to the query, so that the cache expression matches all documents which are also matched by the query.

With cache expressions it is possible to invalidate cache fragments, representing the result of queries, only if an invalidation is necessary. So a periodical invalidation can be avoided. There is only one use case where the periodical invalidation is still necessary - a time based query. Consider a query which matches all documents which were edited yesterday. The result of the query depends on its execution date and it returns a different result, even if no document was changed in the meantime. For such a purpose periodical invalidation is still necessary.

Dynamic delivery - bypassing the cache

When caching is activated, all requests that are made for urls with the suffix .html are cached. If you want the page to be rendered anew every time a request is made, you need to bypass the cache filter by using a no-caching-suffix, which is by default .jsp. Further no-caching-suffixes can be set in the configuration of the CacheFilter in the web.xml file.

Dynamic pages

Thus, if you want to create a link to a dynamic (read: "non-cached") page, you need to create the link with a no-caching-suffix like .jsp:

<sophora:url sophoraId="test100" suffix="jsp" var="url"/>

Dynamic page fragments using SSI

Naturally, the same mechanism applies to SSI fragments in your templates. By using a no-caching-suffix when including a fragment, one part of the requested html file can be cached, whereas the fragment will be rendered dynamically each time. Simply provide a no-caching-suffix like .jsp when creating the include:

<sophora:getInclude sophoraId="test100" type="dynamicFragment" suffix="jsp"/>

Using the .ignore file mechanism to prevent cache flushes on template changes

Normally, when a template-file is updated, all cache entries that are using this template are flushed from the cache. That can result in flushes of huge numbers of cache entries for certain templates. If you don't want the update of the template file to cause any flushes, you can create an ignore-file for the template. The ignore-file tells the cache to ignore all changes of a template. It must be placed in the same directory as the template, using the templates name, but the suffix ".ignore". The ignore-file for the template "src/web/site/template.jsp" would be "src/web/site/template.ignore". When you remove the ignore-file of a template from the webapp, the next change in the template will be recognized and a resource change flush will happen.

Images

Image Servlet

The ImageServlet helps you to render different image variants for a given image document.

Given URL-Scheme

To be able to use the ImageServlet, the image variant are set with the "v"-key in the  URL-Parameters:

/trendcities/london/hamburg102~_v-big.jpg

Configuration

web.xml

In order to address this servlet it is necessary to configure it in your web.xml:

<servlet>
   <servlet-name>image</servlet-name>
   <servlet-class>com.subshell.sophora.delivery.servlet.ImageServlet</servlet-class>
</servlet>
.
.
.
<servlet-mapping>
   <servlet-name>image</servlet-name>
   <url-pattern>/system/servlet/image.servlet</url-pattern>
</servlet-mapping>
templates.xml

Mapping your image node type to the image servlet, configure the template.xml like this:

<nodetype name="sophora-extension-nt:image">
   <templateset>
      <template type="default">/system/servlet/image.servlet</template>
   </templateset>
</nodetype>

SVG Images

Since the binary data of SVG images is stored next to the binary data of JPGs, PNGs, BMPs (and so on) in a separate binary property named sophora-extension:rawdata the access to these binaries is a little bit different.

In the JSP template you have to check whether the rawdata exists and then call a node function, e.g. svgData:

<c:if test="${!empty teaserDocument.image[0].rawdata}">
   MimeType: ${teaserDocument.image[0].rawdata.mimeType}
   SVG: ${teaserDocument.image[0].svgData}
</c:if>

Here is the code of the node function:

@NodeFunction
public String svgData(IContentMapContext context, INode node) {
   if (node.hasProperty("sophora-extension:rawdata")) {
       BinaryData binaryData = node.getBinaryData("sophora-extension:rawdata");
       return new String(binaryData.getBytes());
   }
   return null;
}

Developer Toolbar

The developer toolbar provides various information about the current document. Moreover it allows you to control the caching behaviour of a request. When enabled, the toolbar will always be shown on the very top of the site. With the help of the button right-hand side it is possible to hide the toolbar.

Configuration

To enable the developer toolbar in your development environment you need to add the following parameter to your configuration:
sophora.delivery.showDevToolbar=true

This configuration is used to show the developer toolbar in development environment and to hide it in productive or preview environments.
For detailed information on how to configure your delivery see the chapter Delivery Configuration.

Furthermore it is necessary to include the developer toolbar in the desired templates:

<%@ taglib uri="http://www.subshell.com/sophora/jsp" prefix="sophora" %>
<sophora:devToolbar />

Information Panels

The toolbar contains five panels which provide different information about the current document. Each panel is resizable in its height and can be opened up by clicking on the corresponding menu item in the toolbar.

Document

The document panel contains the JSON representation of the rendered document.

Structure

This panel shows the structure hierachy path of the current request. This structure hierachy path may differ from the structure node of the requested document. To get more information about each structure node you can click on the node's name and expand a JSON representing of the selected structure node.

Caching

In the caching panel you can analyse the caching metrics of the current request and of its included server side includes (SSIs). The caching information is only complete, if all included SSIs are generated by the servlet container and not played out by the sophora cache store. To force the dynamic generation of the current document and of its SSIs, you can use the Caching-Button. If an included SSIs is played out by the sophora cache the caching information and the caching information of its included SSIs is not present.

Variables

The variables panel shows the variables of the request scope, which are available in the template of the rendered document. To see the value of a variable you can expand the entry by clicking on it.

Info

The info panel shows 3 basic information about the current document for quick and handy access:

  • The document's Sophora-Id
  • The document's UUID
  • The primary document type

Buttons

The developer toolbar contains two buttons which allow you to control the caching and to toggle the visibility of the toolbar.

Caching Button

The caching button allows you to toggle the caching behaviour of the request. The current behaviour is represented by the button's color and label as seen in the image below. A green button illustrates that the normal caching behavior is used, while a red button indicates that the request and all included SSIs are dynamically generated. You can inspect the caching information for your document in the Caching-Panel.

If you use an Apache webserver in your development environment it is necessary to add the following rewrite rules to support a dynamic generation of cache fragments.

# If the parameter forceGenerate is set, the requests are forwarded to the servlet container.
RewriteCond %{QUERY_STRING} ^.*?forceGenerate=true.*$
RewriteRule ^/(.*)$ /sophora-demosite/demosite/$1 [PT]

Note that you have to adopt the context and the site in the above example.

Minimising The Toolbar

To hide the developer toolbar click on the triangle arrow button on the right-hand side of the toolbar. You can expand the toolbar by pressing on the small button again.

Live Preview

To support a live preview in the DeskClient's Preview View see the chapter Live Preview.

Solr-Taglib