Moved FreeMarkerPresentationManager to ccm-themedirector

git-svn-id: https://svn.libreccm.org/ccm/trunk@5830 8810af33-2d31-482b-a856-94f89814c4df
master
jensp 2019-02-15 18:54:53 +00:00
parent 7a814be765
commit f1a433190e
17 changed files with 1792 additions and 19 deletions

Binary file not shown.

View File

@ -15,38 +15,48 @@
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.l10n;
*/package org.libreccm.l10n;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.libreccm.l10n.jaxb.LocalizedStringValuesAdapter;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* A helper class for localisable string properties. This class is declared as
* embeddable, so that it can be used in every other entity. The localised
* values are stored in a {@link Map}. This class is <em>not</em> designed to be
* overwritten. But because it is an entity class we can't make the class final.
*
* @author <a href="mailto:tosmers@uni-bremen.de>Tobias Osmers<\a>
* @version created on 6/15/16
*/
//@XmlAccessorType(XmlAccessType.FIELD)
public class LocalizedString {
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
//@XmlElement(name = "values", namespace = L10N_XML_NS)
//@XmlJavaTypeAdapter(LocalizedStringValuesAdapter.class)
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "localized-string",
namespace = L10NConstants.L10N_XML_NS)
@XmlAccessorType(XmlAccessType.FIELD)
public class LocalizedString implements Serializable {
private static final long serialVersionUID = 1L;
/**
* The localised values of the string.
*/
@XmlElement(name = "values", namespace = L10NConstants.L10N_XML_NS)
@XmlJavaTypeAdapter(LocalizedStringValuesAdapter.class)
private Map<Locale, String> values;
/**
* Constructor. Only creates the initial, empty map for new instances.
*/
public LocalizedString() {
this.values = new HashMap<>();
values = new HashMap<>();
}
/**
@ -70,7 +80,11 @@ public class LocalizedString {
* @param values The new map of values.
*/
protected void setValues(final Map<Locale, String> values) {
this.values = values;
if (values == null) {
this.values = new HashMap<>();
} else {
this.values = new HashMap<>(values);
}
}
/**
@ -80,7 +94,6 @@ public class LocalizedString {
* application is running on. In most cases this is not what you
* want. Use {@link #getValue(java.util.Locale)} instead.
*/
@JsonIgnore
public String getValue() {
return getValue(Locale.getDefault());
}
@ -93,7 +106,6 @@ public class LocalizedString {
* @return The localised for the {@code locale} or {@code null} if there is
* no value for the provided locale.
*/
@JsonIgnore
public String getValue(final Locale locale) {
return values.get(locale);
}
@ -140,4 +152,42 @@ public class LocalizedString {
public Set<Locale> getAvailableLocales() {
return values.keySet();
}
@Override
public int hashCode() {
int hash = 7;
hash = 41 * hash + Objects.hashCode(this.values);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof LocalizedString)) {
return false;
}
final LocalizedString other = (LocalizedString) obj;
if (!other.canEqual(this)) {
return false;
}
return Objects.equals(values, other.getValues());
}
public boolean canEqual(final Object obj) {
return obj instanceof LocalizedString;
}
@Override
public String toString() {
return String.format(
"%s{ "
+ "%s"
+ " }",
super.toString(),
Objects.toString(values));
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.l10n.jaxb;
import static org.libreccm.l10n.L10NConstants.*;
import java.io.Serializable;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlValue;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class LocalizedStringValue implements Serializable {
private static final long serialVersionUID = 8435485565736441379L;
@XmlAttribute(name = "lang", namespace = L10N_XML_NS)
private String locale;
@XmlValue
private String value;
public String getLocale() {
return locale;
}
public void setLocale(final String locale) {
this.locale = locale;
}
public String getValue() {
return value;
}
public void setValue(final String value) {
this.value = value;
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + Objects.hashCode(locale);
hash = 97 * hash + Objects.hashCode(value);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof LocalizedStringValue)) {
return false;
}
final LocalizedStringValue other = (LocalizedStringValue) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(locale, other.getLocale())) {
return false;
}
return Objects.equals(value, other.getValue());
}
public boolean canEqual(final Object obj) {
return obj instanceof LocalizedStringValue;
}
@Override
public final String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "locale = %s, "
+ "value = \"%s\"%s"
+ " }",
super.toString(),
Objects.toString(locale),
value,
data);
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.l10n.jaxb;
import static org.libreccm.l10n.L10NConstants.*;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlAccessorType(XmlAccessType.FIELD)
public class LocalizedStringValues implements Serializable {
private static final long serialVersionUID = 1L;
@JacksonXmlElementWrapper(useWrapping = false)
@XmlElement(name = "value", namespace = L10N_XML_NS)
private List<LocalizedStringValue> values;
public List<LocalizedStringValue> getValues() {
return new ArrayList<>(values);
}
public void setValues(final List<LocalizedStringValue> values) {
this.values = new ArrayList<>(values);
}
@Override
public int hashCode() {
int hash = 3;
hash = 41 * hash + Objects.hashCode(values);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof LocalizedStringValues)) {
return false;
}
final LocalizedStringValues other = (LocalizedStringValues) obj;
if (!other.canEqual(this)) {
return false;
}
return Objects.equals(values, other.getValues());
}
public boolean canEqual(final Object obj) {
return obj instanceof LocalizedStringValues;
}
@Override
public final String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "values = %s%s"
+ " }",
super.toString(),
Objects.toString(values),
data);
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.l10n.jaxb;
import org.libreccm.l10n.LocalizedString;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.adapters.XmlAdapter;
/**
* JAXB adapter for {@link LocalizedString#values} to produce a more compact XML
* for the values map.
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class LocalizedStringValuesAdapter
extends XmlAdapter<LocalizedStringValues, Map<Locale, String>> {
@Override
public Map<Locale, String> unmarshal(final LocalizedStringValues values)
throws Exception {
return values
.getValues()
.stream()
.collect(Collectors.toMap(value -> new Locale(value.getLocale()),
value -> value.getValue()));
}
@Override
public LocalizedStringValues marshal(final Map<Locale, String> values)
throws Exception {
final List<LocalizedStringValue> list = values
.entrySet()
.stream()
.map(this::generateValue)
.collect(Collectors.toList());
final LocalizedStringValues result = new LocalizedStringValues();
result.setValues(list);
return result;
}
private LocalizedStringValue generateValue(
final Map.Entry<Locale, String> entry) {
final LocalizedStringValue value = new LocalizedStringValue();
value.setLocale(entry.getKey().toString());
value.setValue(entry.getValue());
return value;
}
}

View File

@ -0,0 +1,117 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.libreccm.theming;
import static org.libreccm.theming.ThemeConstants.*;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "application-template", namespace = THEMES_XML_NS)
@XmlAccessorType(XmlAccessType.FIELD)
public class ApplicationTemplate {
@XmlElement(name = "application-name", namespace = THEMES_XML_NS)
private String applicationName;
@XmlElement(name = "application-class", namespace = THEMES_XML_NS)
private String applicationClass;
@XmlElement(name = "template", namespace = THEMES_XML_NS)
private String template;
public String getApplicationName() {
return applicationName;
}
public void setApplicationName(final String applicationName) {
this.applicationName = applicationName;
}
public String getApplicationClass() {
return applicationClass;
}
public void setApplicationClass(final String applicationClass) {
this.applicationClass = applicationClass;
}
public String getTemplate() {
return template;
}
public void setTemplate(final String template) {
this.template = template;
}
@Override
public int hashCode() {
int hash = 7;
hash = 79 * hash + Objects.hashCode(applicationName);
hash = 79 * hash + Objects.hashCode(applicationClass);
hash = 79 * hash + Objects.hashCode(template);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ApplicationTemplate)) {
return false;
}
final ApplicationTemplate other = (ApplicationTemplate) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(applicationName, other.getApplicationName())) {
return false;
}
if (!Objects.equals(applicationClass, other.getApplicationClass())) {
return false;
}
return Objects.equals(template, other.getTemplate());
}
public boolean canEqual(final Object obj) {
return obj instanceof ApplicationTemplate;
}
@Override
public final String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "applicationName = \"%s\", "
+ "applicationClass = \"%s\", "
+ "template = \"%s\"%s"
+ " }",
super.toString(),
applicationName,
applicationClass,
template,
data
);
}
}

View File

@ -0,0 +1,165 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.libreccm.theming;
import static org.libreccm.theming.ThemeConstants.*;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "contentitem-template", namespace = THEMES_XML_NS)
@XmlAccessorType(XmlAccessType.FIELD)
public class ContentItemTemplate {
@XmlElement(name = "view", namespace = THEMES_XML_NS)
private ContentItemViews view;
@XmlElement(name = "contenttype", namespace = THEMES_XML_NS)
private String contentType;
@XmlElement(name = "style", namespace = THEMES_XML_NS)
private String style;
@XmlElement(name = "contentsection", namespace = THEMES_XML_NS)
private String contentSection;
@XmlElement(name = "category", namespace = THEMES_XML_NS)
private String category;
@XmlElement(name = "template", namespace = THEMES_XML_NS)
private String template;
public ContentItemViews getView() {
return view;
}
public void setView(final ContentItemViews view) {
this.view = view;
}
public String getContentType() {
return contentType;
}
public void setContentType(final String contentType) {
this.contentType = contentType;
}
public String getStyle() {
return style;
}
public void setStyle(final String style) {
this.style = style;
}
public String getContentSection() {
return contentSection;
}
public void setContentSection(final String contentSection) {
this.contentSection = contentSection;
}
public String getCategory() {
return category;
}
public void setCategory(final String category) {
this.category = category;
}
public String getTemplate() {
return template;
}
public void setTemplate(final String template) {
this.template = template;
}
@Override
public int hashCode() {
int hash = 7;
hash = 73 * hash + Objects.hashCode(view);
hash = 73 * hash + Objects.hashCode(contentType);
hash = 73 * hash + Objects.hashCode(style);
hash = 73 * hash + Objects.hashCode(contentSection);
hash = 73 * hash + Objects.hashCode(category);
hash = 73 * hash + Objects.hashCode(template);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ContentItemTemplate)) {
return false;
}
final ContentItemTemplate other = (ContentItemTemplate) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(contentType, other.getContentType())) {
return false;
}
if (!Objects.equals(style, other.getStyle())) {
return false;
}
if (!Objects.equals(contentSection, other.getContentSection())) {
return false;
}
if (!Objects.equals(category, other.getCategory())) {
return false;
}
if (view != other.getView()) {
return false;
}
return Objects.equals(template, other.getTemplate());
}
public boolean canEqual(final Object obj) {
return obj instanceof ContentItemTemplate;
}
@Override
public final String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "contentType = \"%s\", "
+ "style = \"%s\", "
+ "contentSection = \"%s\", "
+ "category = \"%s\""
+ "template = \"%s\"%s"
+ " }",
super.toString(),
contentType,
style,
contentSection,
category,
template,
data);
}
}

View File

@ -0,0 +1,18 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.libreccm.theming;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public enum ContentItemViews {
DETAIL,
GREETING_ITEM,
LIST,
PORTLET_ITEM,
}

View File

@ -0,0 +1,353 @@
package org.libreccm.theming;
import com.arsdigita.bebop.Bebop;
import com.arsdigita.bebop.page.PageTransformer;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.globalization.GlobalizationHelper;
import com.arsdigita.subsite.Site;
import com.arsdigita.templating.PresentationManager;
import com.arsdigita.themedirector.ThemeDirector;
import com.arsdigita.util.UncheckedWrapperException;
import com.arsdigita.web.Web;
import com.arsdigita.xml.Document;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.cache.WebappTemplateLoader;
import freemarker.ext.dom.NodeModel;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.libreccm.theming.manifest.ThemeManifest;
import org.libreccm.theming.manifest.ThemeManifestUtil;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class FreeMarkerPresentationManager implements PresentationManager {
@Override
public void servePage(final Document document,
final HttpServletRequest request,
final HttpServletResponse response) {
final org.w3c.dom.Document w3cDocument = document.getInternalDocument();
final Node root = w3cDocument.getDocumentElement();
final String currentSiteName = Web.getConfig().getSiteName();
Site subSite;
try {
subSite = Site.findByHostname(currentSiteName);
} catch (DataObjectNotFoundException ex) {
subSite = null;
}
final boolean isSubSite = subSite != null;
final String defaultTheme;
if (subSite == null) {
defaultTheme = ThemeDirector
.getThemeDirector()
.getDefaultTheme()
.getURL();
} else {
defaultTheme = subSite.getStyleDirectory();
}
final String selectedTheme;
if (request.getParameter("theme") == null) {
selectedTheme = defaultTheme;
} else {
selectedTheme = request.getParameter("theme");
}
final String previewParam = request.getParameter("preview-theme");
boolean preview = previewParam != null
&& ("true".equalsIgnoreCase(previewParam)
|| "yes".equalsIgnoreCase(previewParam));
final StringBuilder themePathBuilder = new StringBuilder(
"/themes/");
if (preview) {
themePathBuilder.append("devel-themedir/");
} else {
themePathBuilder.append("published-themedir/");
}
themePathBuilder.append(selectedTheme).append("/");
final String themePath = themePathBuilder.toString();
final String themeManifestPath = String.format(
"%s" + ThemeConstants.THEME_MANIFEST_JSON, themePath);
final ServletContext servletContext = Web.getServletContext();
// final String themeManifest = "";
// final String themeManifest = new BufferedReader(
// new InputStreamReader(
// servletContext.getResourceAsStream(themeManifestPath),
// StandardCharsets.UTF_8))
// .lines()
// .collect(Collectors.joining(System.lineSeparator()));
//
// String name = "???";
// final JsonFactory jsonFactory = new JsonFactory();
// try {
// final JsonParser parser = jsonFactory.createParser(servletContext
// .getResourceAsStream(themeManifestPath));
//
// while (!parser.isClosed()) {
//
// final JsonToken token = parser.nextToken();
// if (JsonToken.FIELD_NAME.equals(token)) {
// final String fieldName = parser.getCurrentName();
//
// if ("name".equals(fieldName)) {
//
// final JsonToken valueToken = parser.nextToken();
// final String value = parser.getValueAsString();
// name = value;
// }
// }
//
// }
//
// } catch (IOException ex) {
// throw new UncheckedWrapperException(ex);
// }
final InputStream manifestInputStream = servletContext
.getResourceAsStream(themeManifestPath);
if (manifestInputStream == null) {
final PageTransformer pageTransformer = new PageTransformer();
pageTransformer.servePage(document, request, response);
return;
}
final ThemeManifestUtil manifestUtil = ThemeManifestUtil.getInstance();
final ThemeManifest manifest = manifestUtil
.loadManifest(manifestInputStream,
themeManifestPath);
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JaxbAnnotationModule());
final Templates templates;
try {
templates = objectMapper.readValue(
servletContext.getResourceAsStream(
String.format("%stemplates.json", themePath)),
Templates.class);
} catch (IOException ex) {
throw new UncheckedWrapperException(ex);
}
// ToDo
final NamedNodeMap pageAttrs = root.getAttributes();
final Node applicationNameAttr = pageAttrs.getNamedItem("application");
final Node applicationClassAttr = pageAttrs.getNamedItem("class");
final String applicationName = applicationNameAttr.getNodeValue();
final String applicationClass = applicationClassAttr.getNodeValue();
final Optional<ApplicationTemplate> applicationTemplate
= findApplicationTemplate(
templates,
applicationName,
applicationClass);
final String applicationTemplatePath;
if (applicationTemplate.isPresent()) {
applicationTemplatePath = applicationTemplate.get().getTemplate();
} else {
applicationTemplatePath = templates.getDefaultApplicationTemplate();
}
if ("XSL_FALLBACK.XSL".equals(applicationTemplatePath)) {
final PageTransformer pageTransformer = new PageTransformer();
pageTransformer.servePage(document, request, response);
return;
}
final Configuration configuration = new Configuration(
Configuration.VERSION_2_3_28);
final WebappTemplateLoader themeTemplateLoader
= new WebappTemplateLoader(servletContext,
themePath);
final WebappTemplateLoader macrosLoader = new WebappTemplateLoader(
servletContext,
"/themes/freemarker");
final MultiTemplateLoader templateLoader = new MultiTemplateLoader(
new TemplateLoader[]{themeTemplateLoader, macrosLoader});
// configuration.setServletContextForTemplateLoading(servletContext,
// themePath);
configuration.setTemplateLoader(templateLoader);
configuration.setDefaultEncoding("UTF-8");
final Map<String, Object> data = new HashMap<>();
// The XML document
data.put("model", NodeModel.wrap(root));
// Parameters (in XSL provided as XSL parameters)
data.put("contextPath", request.getContextPath());
data.put("contextPrefix",
Web.getWebContext().getRequestURL().getContextPath());
data.put("dcpOnButtons",
Bebop.getConfig().doubleClickProtectionOnButtons());
data.put("dcpOnLinks",
Bebop.getConfig().doubleClickProtectionOnLinks());
data.put("dispatcherPrefix", com.arsdigita.web.URL.getDispatcherPath());
final String host;
if (request.getServerPort() == 80) {
host = String.format("%s://%s",
request.getScheme(),
request.getServerName());
} else {
host = String.format("%s://%s:%d",
request.getScheme(),
request.getServerName(),
request.getServerPort());
}
data.put("host", host);
data.put("internalTheme",
Web.getWebContext().getRequestURL().getContextPath()
+ com.arsdigita.web.URL.INTERNAL_THEME_DIR);
data.put("negotiatedLanguage",
GlobalizationHelper.getNegotiatedLocale().getLanguage());
data.put("requestScheme", request.getScheme());
data.put("rootContextPrefix",
Web.getConfig().getDispatcherContextPath());
final Locale selectedLocale = GlobalizationHelper
.getSelectedLocale(request);
if (selectedLocale == null) {
data.put("selectedLanguage", "");
} else {
data.put("selectedLanguage", selectedLocale.getLanguage());
}
data.put("serverName", request.getServerName());
data.put("serverPort", request.getServerPort());
data.put("userAgent", request.getHeader("user-Agent"));
final Template template;
try {
template = configuration.getTemplate(applicationTemplatePath);
} catch (IOException ex) {
throw new UncheckedWrapperException(ex);
}
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType("text/html");
try (PrintWriter writer = response.getWriter()) {
template.process(data, writer);
// writer.append("Data:\n");
// writer
// .append("Current Site Name: ")
// .append(currentSiteName).append("\n");
// writer
// .append("isSubSite: ")
// .append(Boolean.toString(isSubSite))
// .append("\n");
// writer
// .append("default theme: ")
// .append(defaultTheme)
// .append("\n");
// writer
// .append("selected theme: ")
// .append(selectedTheme)
// .append("\n");
// writer
// .append("preview theme? ")
// .append(Boolean.toString(preview))
// .append("\n");
// writer
// .append("themePath: ")
// .append(themePath)
// .append("\n");
// writer
// .append("themeManifestPath: ")
// .append(themeManifestPath)
// .append("\n");
// writer
// .append("themeManifest: ")
// .append(manifest.toString())
// .append("\n");
// writer
// .append("theme name: ")
// .append(manifest.getName())
// .append("\n");
// writer
// .append("Application name: ")
// .append(applicationName)
// .append("\n");
// writer
// .append("Application class: ")
// .append(applicationClass)
// .append("\n");
// writer
// .append("Application templates:\n");
// for (final ApplicationTemplate template : templates
// .getApplications()) {
// writer
// .append("\t")
// .append(template.toString())
// .append("\n");
// }
} catch (IOException | TemplateException ex) {
throw new UncheckedWrapperException(ex);
}
// throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
}
private Optional<ApplicationTemplate> findApplicationTemplate(
final Templates templates,
final String applicationName,
final String applicationClass) {
final Optional<ApplicationTemplate> forNameAndClass = templates
.getApplications()
.stream()
.filter(template -> filterApplicationTemplates(template,
applicationName,
applicationClass))
.findAny();
if (forNameAndClass.isPresent()) {
return forNameAndClass;
} else {
final Optional<ApplicationTemplate> forName = templates
.getApplications()
.stream()
.filter(tpl -> tpl.getApplicationName().equals(applicationName))
.findAny();
return forName;
}
}
private boolean filterApplicationTemplates(
final ApplicationTemplate template,
final String applicationName,
final String applicationClass) {
return template.getApplicationName().equals(applicationName)
&& template.getApplicationClass().equals(applicationClass);
}
}

View File

@ -0,0 +1,108 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.libreccm.theming;
import static org.libreccm.theming.ThemeConstants.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "templates", namespace = THEMES_XML_NS)
@XmlAccessorType(XmlAccessType.FIELD)
public class Templates {
@XmlElementWrapper(name = "applications", namespace = THEMES_XML_NS)
@XmlElement(name = "applications", namespace = THEMES_XML_NS)
private List<ApplicationTemplate> applications;
@XmlElement(name = "default-application-template",
namespace = THEMES_XML_NS)
private String defaultApplicationTemplate;
@XmlElementWrapper(name = "contentitems", namespace = THEMES_XML_NS)
@XmlElement(name = "contentitems", namespace = THEMES_XML_NS)
private List<ContentItemTemplate> contentItems;
@XmlElement(name = "default-contentitem-template",
namespace = THEMES_XML_NS)
private String defaultContentItemsTemplate;
public Templates() {
applications = new ArrayList<>();
contentItems= new ArrayList<>();
}
public List<ApplicationTemplate> getApplications() {
return Collections.unmodifiableList(applications);
}
public void addApplication(final ApplicationTemplate template) {
applications.add(template);
}
public void removeApplication(final ApplicationTemplate template) {
applications.remove(template);
}
public void setApplications(final List<ApplicationTemplate> applications) {
this.applications = new ArrayList<>(applications);
}
public String getDefaultApplicationTemplate() {
return defaultApplicationTemplate;
}
public void setDefaultApplicationTemplate(
final String defaultApplicationTemplate) {
this.defaultApplicationTemplate = defaultApplicationTemplate;
}
public List<ContentItemTemplate> getContentItems() {
return Collections.unmodifiableList(contentItems);
}
public void addContentItem(final ContentItemTemplate template) {
contentItems.add(template);
}
public void removeContentItem(final ContentItemTemplate template) {
contentItems.remove(template);
}
public void setContentItems(final List<ContentItemTemplate> contentItems) {
this.contentItems = new ArrayList<>(contentItems);
}
public String getDefaultContentItemsTemplate() {
return defaultContentItemsTemplate;
}
public void setDefaultContentItemsTemplate(
final String defaultContentItemsTemplate) {
this.defaultContentItemsTemplate = defaultContentItemsTemplate;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.theming;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public final class ThemeConstants {
public static final String PAGE_PARAMETER_TEMPLATE = "template";
public final static String THEME_MANIFEST_JSON = "theme.json";
public final static String THEME_MANIFEST_XML = "theme.xml";
public final static String THEMES_XML_NS = "http://themes.libreccm.org";
private ThemeConstants() {
//Nothing
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.theming.manifest;
import org.libreccm.l10n.LocalizedString;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import static org.libreccm.theming.ThemeConstants.*;
import java.io.Serializable;
/**
* Each theme contains a Manifest (either in XML or JSON format) which provides
* informations about the theme.
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "theme", namespace = THEMES_XML_NS)
@XmlAccessorType(XmlAccessType.FIELD)
public class ThemeManifest implements Serializable {
private static final long serialVersionUID = 699497658459398231L;
/**
* The name of the theme. Usually the same as the name of directory which
* contains the theme.
*/
@XmlElement(name = "name", namespace = THEMES_XML_NS)
private String name;
/**
* The type of the theme, for example XSLT.
*/
@XmlElement(name = "type", namespace = THEMES_XML_NS)
private String type;
@XmlElement(name = "master-theme", namespace = THEMES_XML_NS)
private String masterTheme;
/**
* The (localised) title of the theme.
*/
@XmlElement(name = "title", namespace = THEMES_XML_NS)
private LocalizedString title;
/**
* A (localised) description of the theme.
*/
@XmlElement(name = "description", namespace = THEMES_XML_NS)
private LocalizedString description;
/**
* The templates provided by the theme.
*/
@XmlElementWrapper(name = "templates", namespace = THEMES_XML_NS)
@XmlElement(name = "template", namespace = THEMES_XML_NS)
private List<ThemeTemplate> templates;
/**
* Path of the default template.
*/
@XmlElement(name = "default-template", namespace = THEMES_XML_NS)
private String defaultTemplate;
public ThemeManifest() {
templates = new ArrayList<>();
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getMasterTheme() {
return masterTheme;
}
public void setMasterTheme(final String masterTheme) {
this.masterTheme = masterTheme;
}
public LocalizedString getTitle() {
return title;
}
public void setTitle(final LocalizedString title) {
this.title = title;
}
public LocalizedString getDescription() {
return description;
}
public void setDescription(final LocalizedString description) {
this.description = description;
}
public List<ThemeTemplate> getTemplates() {
return Collections.unmodifiableList(templates);
}
public void setTemplates(final List<ThemeTemplate> templates) {
this.templates = new ArrayList<>(templates);
}
public void addThemeTemplate(final ThemeTemplate template) {
templates.add(template);
}
public void removeThemeTemplate(final ThemeTemplate template) {
templates.remove(template);
}
public String getDefaultTemplate() {
return defaultTemplate;
}
public void setDefaultTemplate(final String defaultTemplate) {
this.defaultTemplate = defaultTemplate;
}
@Override
public int hashCode() {
int hash = 7;
hash = 83 * hash + Objects.hashCode(name);
hash = 83 * hash + Objects.hashCode(type);
hash = 83 * hash + Objects.hashCode(masterTheme);
hash = 83 * hash + Objects.hashCode(title);
hash = 83 * hash + Objects.hashCode(description);
hash = 83 * hash + Objects.hashCode(templates);
hash = 83 * hash + Objects.hashCode(defaultTemplate);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ThemeManifest)) {
return false;
}
final ThemeManifest other = (ThemeManifest) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(name, other.getName())) {
return false;
}
if (!Objects.equals(type, other.getType())) {
return false;
}
if (!Objects.equals(masterTheme, other.getMasterTheme())) {
return false;
}
if (!Objects.equals(title, other.getTitle())) {
return false;
}
if (!Objects.equals(description, other.getDescription())) {
return false;
}
if (!Objects.equals(templates, other.getTemplates())) {
return false;
}
return Objects.equals(defaultTemplate, other.getDefaultTemplate());
}
public boolean canEqual(final Object obj) {
return obj instanceof ThemeManifest;
}
@Override
public String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "name = \"%s\", "
+ "type = \"%s\", "
+ "masterTheme = \"%s\", "
+ "title = \"%s\", "
+ "description = \"%s\", "
+ "templates = %s, "
+ "defaultTemplate%s"
+ " }",
super.toString(),
name,
type,
masterTheme,
Objects.toString(title),
Objects.toString(description),
Objects.toString(templates),
defaultTemplate,
data);
}
}

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.theming.manifest;
import com.arsdigita.util.UncheckedWrapperException;
import static org.libreccm.theming.ThemeConstants.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
/**
* A Utility class for loading them manifest file of a theme.
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
public class ThemeManifestUtil implements Serializable {
private static final long serialVersionUID = -7650437144515619682L;
private static final ThemeManifestUtil INSTANCE = new ThemeManifestUtil();
private ThemeManifestUtil() {};
public static final ThemeManifestUtil getInstance() {
return INSTANCE;
}
/**
* Reads the manifest file at {@code path}.
*
* @param path The path of the manifest file.
*
* @return The parsed manifest file.
*/
public ThemeManifest loadManifest(final Path path) {
// final String pathStr = path.toString().toLowerCase(Locale.ROOT);
final BufferedReader reader;
try {
reader = Files.newBufferedReader(path, Charset.forName("UTF-8"));
} catch (IOException ex) {
throw new UncheckedWrapperException(ex);
}
return parseManifest(reader, path.toString());
// final ObjectMapper mapper;
// if (pathStr.endsWith(THEME_MANIFEST_JSON)) {
// mapper = new ObjectMapper();
// } else if (pathStr.endsWith(THEME_MANIFEST_XML)) {
// final JacksonXmlModule xmlModule = new JacksonXmlModule();
// mapper = new XmlMapper(xmlModule);
// } else {
// throw new IllegalArgumentException(String
// .format("The provided path \"%s\" does not point to a theme "
// + "manifest file.",
// path.toString()));
// }
//
// mapper.registerModule(new JaxbAnnotationModule());
//
// final ThemeManifest manifest;
// try {
// manifest = mapper.readValue(reader, ThemeManifest.class);
// } catch (IOException ex) {
// throw new UnexpectedErrorException(ex);
// }
// return manifest;
}
public ThemeManifest loadManifest(final InputStream inputStream,
final String fileName) {
final InputStreamReader reader;
try {
reader = new InputStreamReader(inputStream, "UTF-8");
} catch (UnsupportedEncodingException ex) {
throw new UncheckedWrapperException(ex);
}
return parseManifest(reader, fileName);
// final ObjectMapper mapper;
// if (fileName.endsWith(THEME_MANIFEST_JSON)) {
// mapper = new ObjectMapper();
// } else if (fileName.endsWith(THEME_MANIFEST_XML)) {
// final JacksonXmlModule xmlModule = new JacksonXmlModule();
// mapper = new XmlMapper(xmlModule);
// } else {
// throw new IllegalArgumentException(String
// .format("The provided path \"%s\" does not point to a theme "
// + "manifest file.",
// fileName));
// }
//
// mapper.registerModule(new JaxbAnnotationModule());
//
// final ThemeManifest manifest;
// try {
// manifest = mapper.readValue(reader, ThemeManifest.class);
// } catch (IOException ex) {
// throw new UnexpectedErrorException(ex);
// }
// return manifest;
}
public String serializeManifest(final ThemeManifest manifest,
final String format) {
final ObjectMapper mapper;
switch (format) {
case THEME_MANIFEST_JSON:
mapper = new ObjectMapper();
break;
case THEME_MANIFEST_XML:
final JacksonXmlModule xmlModule = new JacksonXmlModule();
mapper = new XmlMapper(xmlModule);
break;
default:
throw new IllegalArgumentException(
"Unsupported format for ThemeManifest");
}
mapper.registerModule(new JaxbAnnotationModule());
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
final StringWriter writer = new StringWriter();
try {
mapper.writeValue(writer, manifest);
} catch (IOException ex) {
throw new UncheckedWrapperException(ex);
}
return writer.toString();
}
private ThemeManifest parseManifest(final Reader reader,
final String path) {
final String pathStr = path.toLowerCase(Locale.ROOT);
final ObjectMapper mapper;
if (pathStr.endsWith(THEME_MANIFEST_JSON)) {
mapper = new ObjectMapper();
} else if (pathStr.endsWith(THEME_MANIFEST_XML)) {
final JacksonXmlModule xmlModule = new JacksonXmlModule();
mapper = new XmlMapper(xmlModule);
} else {
throw new IllegalArgumentException(String
.format("The provided path \"%s\" does not point to a theme "
+ "manifest file.",
path));
}
mapper.registerModule(new JaxbAnnotationModule());
final ThemeManifest manifest;
try {
manifest = mapper.readValue(reader, ThemeManifest.class);
} catch (IOException ex) {
throw new UncheckedWrapperException(ex);
}
return manifest;
}
}

View File

@ -0,0 +1,160 @@
/*
* Copyright (C) 2017 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.theming.manifest;
import org.libreccm.l10n.LocalizedString;
import java.io.Serializable;
import java.util.Objects;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
/**
* Informations about a template provided by a theme.
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@XmlRootElement(name = "template", namespace = "http://themes.libreccm.org")
@XmlAccessorType(XmlAccessType.FIELD)
public class ThemeTemplate implements Serializable {
private static final long serialVersionUID = -9034588759798295569L;
/**
* The name of the template (usually the filename).
*/
@XmlElement(name = "name", namespace = "http://themes.libreccm.org")
private String name;
/**
* The (localised) title of the template.
*/
@XmlElement(name = "title", namespace = "http://themes.libreccm.org")
private LocalizedString title;
/**
* A (localised) description of the template.
*/
@XmlElement(name = "description", namespace = "http://themes.libreccm.org")
private LocalizedString description;
/**
* Path of template relative to the directory of the theme.
*/
@XmlElement(name = "path", namespace = "http://themes.libreccm.org")
private String path;
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public LocalizedString getTitle() {
return title;
}
public void setTitle(final LocalizedString title) {
this.title = title;
}
public LocalizedString getDescription() {
return description;
}
public void setDescription(final LocalizedString description) {
this.description = description;
}
public String getPath() {
return path;
}
public void setPath(final String path) {
this.path = path;
}
@Override
public int hashCode() {
int hash = 3;
hash = 67 * hash + Objects.hashCode(name);
hash = 67 * hash + Objects.hashCode(title);
hash = 67 * hash + Objects.hashCode(description);
hash = 67 * hash + Objects.hashCode(path);
return hash;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof ThemeTemplate)) {
return false;
}
final ThemeTemplate other = (ThemeTemplate) obj;
if (!other.canEqual(this)) {
return false;
}
if (!Objects.equals(name, other.getName())) {
return false;
}
if (!Objects.equals(path, other.getPath())) {
return false;
}
if (!Objects.equals(title, other.getTitle())) {
return false;
}
return Objects.equals(description, other.getDescription());
}
public boolean canEqual(final Object obj) {
return obj instanceof ThemeTemplate;
}
@Override
public String toString() {
return toString("");
}
public String toString(final String data) {
return String.format("%s{ "
+ "name = \"%s\", "
+ "title = %s, "
+ "description = %s, "
+ "path = \"%s\"%s"
+ " }",
super.toString(),
name,
Objects.toString(title),
Objects.toString(description),
path,
data);
}
}

View File

@ -0,0 +1,23 @@
<#ftl ns_prefixes={
"bebop":"http://www.arsdigita.com/bebop/1.0",
"cms":"http://www.arsdigita.com/cms/1.0",
"nav":"http://ccm.redhat.com/navigation",
"ui": "http://www.arsdigita.com/ui/1.0"}
>
<#macro availableLanguages>
<#assign langs=["empty"]>
<#if (model["/bebop:page/cms:contentPanel"]?size > 0)>
<#assign langs=model["/bebop:page/cms:contentPanel/availableLanguages/language/@locale"]>
<#elseif (model["/bebop:page/nav:greetingItem"]?size > 0)>
<#assign langs=model["/bebop:page/nav:greetingItem/availableLanguages/language/@locale"]>
<#else>
<#assign langs=model["/bebop:page/ui:siteBanner/supportedLanguages/language/@locale"]>
</#if>
<#list langs?sort as lang>
<#nested lang, lang==negotiatedLanguage>
</#list>
</#macro>

View File

@ -0,0 +1,12 @@
<#ftl ns_prefixes={
"bebop":"http://www.arsdigita.com/bebop/1.0",
"cms":"http://www.arsdigita.com/cms/1.0",
"nav":"http://ccm.redhat.com/navigation",
"ui": "http://www.arsdigita.com/ui/1.0"}
>
<#-- Move to Navigation tags? -->
<#macro pageTitle>
${model["//nav:categoryMenu/nav:category/@title"]}
</#macro>