CCM NG: Implementieren eines ThemeProcessors für Freemarker (#2759)

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5289 8810af33-2d31-482b-a856-94f89814c4df
jensp 2018-02-16 20:36:10 +00:00
parent ee857ec4c8
commit 3ceec91105
13 changed files with 783 additions and 82 deletions

View File

@ -204,6 +204,11 @@
<artifactId>javax.json</artifactId> <artifactId>javax.json</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
<dependency> <dependency>
<groupId>net.sf.saxon</groupId> <groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId> <artifactId>Saxon-HE</artifactId>

View File

@ -0,0 +1,119 @@
/*
* Copyright (C) 2018 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.freemarker;
import freemarker.cache.TemplateLoader;
import freemarker.cache.TemplateLookupStrategy;
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import org.libreccm.core.UnexpectedErrorException;
import org.libreccm.theming.ThemeInfo;
import org.libreccm.theming.Themes;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@ApplicationScoped
class FreemarkerConfigurationProvider {
@Inject
private Themes themes;
private final Map<ThemeInfo, Configuration> configurations = new HashMap<>();
protected Configuration getConfiguration(final ThemeInfo forTheme) {
if (configurations.containsKey(forTheme)) {
return configurations.get(forTheme);
} else {
final Configuration configuration = new Configuration(
Configuration.VERSION_2_3_27);
configuration.setDefaultEncoding("UTF-8");
configuration
.setTemplateExceptionHandler(
TemplateExceptionHandler.RETHROW_HANDLER);
configuration.setLogTemplateExceptions(false);
configuration.setWrapUncheckedExceptions(false);
configuration.setLocalizedLookup(false);
configuration.setTemplateLoader(new CcmTemplateLoader(forTheme));
configurations.put(forTheme, configuration);
return configuration;
}
}
private class CcmTemplateLoader implements TemplateLoader {
private final ThemeInfo fromTheme;
public CcmTemplateLoader(final ThemeInfo fromTheme) {
this.fromTheme = fromTheme;
}
@Override
public Object findTemplateSource(final String name) throws IOException {
return themes
.getFileFromTheme(fromTheme, name)
.orElseThrow(() -> new UnexpectedErrorException(String
.format("Failed to open Freemarker Template \"%s\" from "
+ "theme \"%s\".",
name,
fromTheme.getName())));
}
@Override
public long getLastModified(final Object templateSource) {
return -1;
}
@Override
public Reader getReader(final Object templateSource,
final String encoding) throws IOException {
final InputStream inputStream = (InputStream) templateSource;
return new InputStreamReader(inputStream, encoding);
}
@Override
public void closeTemplateSource(final Object templateSource)
throws IOException {
final InputStream inputStream = (InputStream) templateSource;
inputStream.close();
}
}
}

View File

@ -0,0 +1,245 @@
/*
* Copyright (C) 2018 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.freemarker;
import static org.libreccm.theming.ThemeConstants.*;
import freemarker.template.SimpleNumber;
import freemarker.template.SimpleScalar;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateScalarModel;
import org.libreccm.core.UnexpectedErrorException;
import org.libreccm.theming.ProcessesThemes;
import org.libreccm.theming.ThemeInfo;
import org.libreccm.theming.ThemeProcessor;
import org.libreccm.theming.ThemeProvider;
import org.libreccm.theming.manifest.ThemeTemplate;
import org.libreccm.theming.utils.L10NUtils;
import org.libreccm.theming.utils.SettingsUtils;
import org.libreccm.theming.utils.SystemInfoUtils;
import org.libreccm.theming.utils.TextUtils;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@ProcessesThemes("freemarker")
@RequestScoped
public class FreemarkerThemeProcessor implements ThemeProcessor {
private static final long serialVersionUID = -5631706431004020559L;
@Inject
private FreemarkerConfigurationProvider configurationProvider;
@Inject
private L10NUtils l10nUtils;
@Inject
private SystemInfoUtils systemInfoUtils;
@Inject
private SettingsUtils settingsUtils;
@Inject
private TextUtils textUtils;
@Override
public String process(final Map<String, Object> page,
final ThemeInfo theme,
final ThemeProvider themeProvider) {
final String pathToTemplate;
if (page.containsKey(PAGE_PARAMETER_TEMPLATE)) {
final String templateName = (String) page
.get(PAGE_PARAMETER_TEMPLATE);
final Optional<ThemeTemplate> template = theme
.getManifest()
.getTemplates()
.stream()
.filter(current -> current.getName().equals(templateName))
.findAny();
if (template.isPresent()) {
pathToTemplate = template.get().getPath();
} else {
throw new UnexpectedErrorException(String
.format("Theme \"%s\" does provide template \"%s\".",
theme.getName(),
templateName));
}
} else {
pathToTemplate = theme.getManifest().getDefaultTemplate();
}
page.put("getContextPath", new GetContextPathMethod());
page.put("getSetting", new GetSettingMethod(theme, themeProvider));
page.put("localize", new LocalizeMethod(theme, themeProvider));
page.put("truncateText", new TruncateTextMethod());
final Template template;
try {
template = configurationProvider
.getConfiguration(theme)
.getTemplate(pathToTemplate);
} catch (IOException ex) {
throw new UnexpectedErrorException(ex);
}
final StringWriter writer = new StringWriter();
try {
template.process(page, writer);
} catch (TemplateException | IOException ex) {
throw new UnexpectedErrorException(ex);
}
return writer.toString();
}
private class GetContextPathMethod implements TemplateMethodModelEx {
@Override
public Object exec(final List arguments) throws TemplateModelException {
return systemInfoUtils.getContextPath();
}
}
private class GetSettingMethod implements TemplateMethodModelEx {
private final ThemeInfo fromTheme;
private final ThemeProvider themeProvider;
public GetSettingMethod(final ThemeInfo fromTheme,
final ThemeProvider themeProvider) {
this.fromTheme = fromTheme;
this.themeProvider = themeProvider;
}
@Override
public Object exec(final List arguments) throws TemplateModelException {
switch (arguments.size()) {
case 2: {
final String filePath = ((TemplateScalarModel) arguments
.get(0))
.getAsString();
final String settingName = ((TemplateScalarModel) arguments
.get(0))
.getAsString();
return settingsUtils.getSetting(fromTheme,
themeProvider,
filePath,
settingName);
}
case 3: {
final String filePath
= ((TemplateScalarModel) arguments.get(0))
.getAsString();
final String settingName
= ((TemplateScalarModel) arguments.get(1))
.getAsString();
final String defaultValue
= ((TemplateScalarModel) arguments.get(2))
.getAsString();
return settingsUtils.getSetting(fromTheme,
themeProvider,
filePath,
settingName,
defaultValue);
}
default:
throw new TemplateModelException(
"Illegal number of arguments.");
}
}
}
private class LocalizeMethod implements TemplateMethodModelEx {
private final ThemeInfo fromTheme;
private final ThemeProvider themeProvider;
public LocalizeMethod(final ThemeInfo fromTheme,
final ThemeProvider themeProvider) {
this.fromTheme = fromTheme;
this.themeProvider = themeProvider;
}
@Override
public Object exec(final List arguments) throws TemplateModelException {
if (arguments.isEmpty()) {
throw new TemplateModelException("No string to localize.");
}
final String bundle;
if (arguments.size() > 1) {
bundle = ((TemplateScalarModel) arguments.get(1)).getAsString();
} else {
bundle = "theme-bundle";
}
final String key = ((TemplateScalarModel) arguments.get(0))
.getAsString();
return l10nUtils.getText(fromTheme, themeProvider, bundle, key);
}
}
private class TruncateTextMethod implements TemplateMethodModelEx {
@Override
public Object exec(final List arguments) throws TemplateModelException {
if (arguments.size() == 2) {
final String text = ((TemplateScalarModel) arguments.get(0))
.getAsString();
final int length = ((SimpleNumber) arguments.get(1))
.getAsNumber()
.intValue();
return textUtils.truncateText(text, length);
} else {
throw new TemplateModelException("Illegal number of arguments.");
}
}
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (C) 2018 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.utils;
import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.theming.ThemeInfo;
import org.libreccm.theming.ThemeProvider;
import org.libreccm.theming.xslt.XsltThemeProcessor;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
public class L10NUtils implements Serializable {
private static final long serialVersionUID = 7077097386650257429L;
@Inject
private GlobalizationHelper globalizationHelper;
public ResourceBundle getBundle(final ThemeInfo fromTheme,
final ThemeProvider themeProvider,
final String bundleName) {
return ResourceBundle
.getBundle(
bundleName,
globalizationHelper.getNegotiatedLocale(),
new LocalizedResourceBundleControl(fromTheme,
themeProvider));
}
public String getText(final ThemeInfo fromTheme,
final ThemeProvider themeProvider,
final String bundleName,
final String key) {
final ResourceBundle bundle = getBundle(fromTheme,
themeProvider,
bundleName);
return bundle.getString(key);
}
public String getText(final ThemeInfo fromTheme,
final ThemeProvider themeProvider,
final String bundleName,
final String key,
final String arguments) {
final ResourceBundle bundle = getBundle(fromTheme,
themeProvider,
bundleName);
return MessageFormat.format(bundle.getString(key), arguments);
}
private class LocalizedResourceBundleControl
extends ResourceBundle.Control {
private final ThemeInfo theme;
private final ThemeProvider themeProvider;
public LocalizedResourceBundleControl(
final ThemeInfo theme,
final ThemeProvider themeProvider) {
this.theme = theme;
this.themeProvider = themeProvider;
}
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Arrays.asList("java.properties");
}
@Override
public ResourceBundle newBundle(final String baseName,
final Locale locale,
final String format,
final ClassLoader classLoader,
final boolean reload)
throws IllegalAccessException,
InstantiationException,
IOException {
if ("java.properties".equals(format)) {
final String bundleName = toBundleName(baseName, locale);
final Optional<InputStream> inputStream = themeProvider
.getThemeFileAsStream(theme.getName(),
theme.getVersion(),
String.format("%s.properties",
bundleName));
if (inputStream.isPresent()) {
return new PropertyResourceBundle(inputStream.get());
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
}
}
}

View File

@ -57,6 +57,7 @@ import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.theming.ProcessesThemes; import org.libreccm.theming.ProcessesThemes;
import org.libreccm.theming.Themes; import org.libreccm.theming.Themes;
import org.libreccm.theming.manifest.ThemeTemplate; import org.libreccm.theming.manifest.ThemeTemplate;
import org.libreccm.theming.utils.L10NUtils;
import org.libreccm.theming.utils.SettingsUtils; import org.libreccm.theming.utils.SettingsUtils;
import org.libreccm.theming.utils.SystemInfoUtils; import org.libreccm.theming.utils.SystemInfoUtils;
import org.libreccm.theming.utils.TextUtils; import org.libreccm.theming.utils.TextUtils;
@ -105,7 +106,7 @@ public class XsltThemeProcessor implements ThemeProcessor {
.getLogger(XsltThemeProcessor.class); .getLogger(XsltThemeProcessor.class);
@Inject @Inject
private GlobalizationHelper globalizationHelper; private L10NUtils l10nUtils;
@Inject @Inject
private SettingsUtils settingsUtils; private SettingsUtils settingsUtils;
@ -182,15 +183,6 @@ public class XsltThemeProcessor implements ThemeProcessor {
pathToTemplate = theme.getManifest().getDefaultTemplate(); pathToTemplate = theme.getManifest().getDefaultTemplate();
} }
// final InputStream xslFileInputStream = themeProvider
// .getThemeFileAsStream(theme.getName(),
// theme.getVersion(),
// pathToTemplate)
// .orElseThrow(() -> new UnexpectedErrorException(String
// .format("Failed to open XSL file \"%s\" from theme \"%s\" for "
// + "reading.",
// pathToTemplate,
// theme.getName())));
final InputStream xslFileInputStream = themes final InputStream xslFileInputStream = themes
.getFileFromTheme(theme, .getFileFromTheme(theme,
pathToTemplate) pathToTemplate)
@ -231,9 +223,6 @@ public class XsltThemeProcessor implements ThemeProcessor {
final Transformer transformer; final Transformer transformer;
try { try {
transformer = transformerFactory.newTransformer(xslFileStreamSource); transformer = transformerFactory.newTransformer(xslFileStreamSource);
// transformer.setURIResolver(new CcmUriResolver(theme.getName(),
// theme.getVersion(),
// themeProvider));
transformer.setErrorListener(new ErrorListener() { transformer.setErrorListener(new ErrorListener() {
@Override @Override
@ -492,15 +481,11 @@ public class XsltThemeProcessor implements ThemeProcessor {
key, key,
bundle); bundle);
final ResourceBundle resourceBundle = ResourceBundle
.getBundle(
bundle,
globalizationHelper.getNegotiatedLocale(),
new LocalizedResourceBundleControl(theme,
themeProvider));
return StringValue return StringValue
.makeStringValue(resourceBundle.getString(key)); .makeStringValue(l10nUtils.getText(theme,
themeProvider,
bundle,
key));
} }
}; };
@ -508,67 +493,6 @@ public class XsltThemeProcessor implements ThemeProcessor {
} }
private class LocalizedResourceBundleControl
extends ResourceBundle.Control {
private final ThemeInfo theme;
private final ThemeProvider themeProvider;
public LocalizedResourceBundleControl(
final ThemeInfo theme,
final ThemeProvider themeProvider) {
this.theme = theme;
this.themeProvider = themeProvider;
}
@Override
public List<String> getFormats(final String baseName) {
Objects.requireNonNull(baseName);
return Arrays.asList("java.properties");
}
@Override
public ResourceBundle newBundle(final String baseName,
final Locale locale,
final String format,
final ClassLoader classLoader,
final boolean reload)
throws IllegalAccessException,
InstantiationException,
IOException {
if ("java.properties".equals(format)) {
final String bundleName = toBundleName(baseName, locale);
final Optional<InputStream> inputStream = themeProvider
.getThemeFileAsStream(theme.getName(),
theme.getVersion(),
String.format("%s.properties",
bundleName));
if (inputStream.isPresent()) {
return new PropertyResourceBundle(inputStream.get());
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
} else {
return super.newBundle(baseName,
locale,
format,
classLoader,
reload);
}
}
}
private class TruncateTextFunctionDefinition private class TruncateTextFunctionDefinition
extends ExtensionFunctionDefinition { extends ExtensionFunctionDefinition {

View File

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<title>Category page</title>
<link rel="stylesheet" href="${getContextPath()}/theming/ccm/style.css" />
</head>
<body>
<main>
<ul class="news">
<#list newsList.items as item>
<li>
<span>
<!--<pre>
<xsl:value-of select="count(./attachments[name='.images']/attachments[1])" />
</pre>-->
<#list item.attachments as attachmentList>
<#if attachmentList.name = ".images">
<img src="${getContextPath()}/content-sections/info/images/uuid-${attachmentList.attachments[0].asset.uuid}"
width="354"
height="286"
alt="" />
</#if>
</#list>
</span>
<span>
"${item.title}"
</span>
</li>
</#list>
</ul>
<div class="boxes">
<#list articles.items as item>
<div>
<h1>
${item.title}"
</h1>
<p>
<#list item.attachments as attachmentList>
<#if attachmentList.name = ".images">
<img src="${getContextPath()}/content-sections/info/images/uuid-${attachmentList.attachments[0].asset.uuid}"
alt="" />
</#if>
</#list>
${item.description}"
</p>
</div>
</#list>
</div>
<h2>Example of Theme Utils</h2>
<dl>
<dt>
<code>getContextPath</code>
</dt>
<dd>
<code>
${getContextPath()}
</code>
</dd>
<dt>
<code>getSetting</code>
</dt>
<dd>
<code>
${getSetting("settings.properties", "example.setting", "n/a")}
</code>
</dd>
<dt>
<code>truncateText</code>
</dt>
<dd>
<code>
${truncateText("0123456789 123456789 123456789", 20)}
</code>
</dd>
<dt>
<code>localized('label.critical')</code>
</dt>
<dd>
${localize("label.critical", "texts/labels")}
</dd>
<dt>
<code>localized('label.error')</code>
</dt>
<dd>
${localize("label.error", "texts/labels")}
</dd>
<dt>
<code>localized('label.ok')</code>
</dt>
<dd>
${localize("label.ok", "texts/labels")}
</dd>
<dt>
<code>localized('label.warning')</code>
</dt>
<dd>
${localize("label.warning", "texts/labels")}
</dd>
</dl>
</main>
<#include "footer.html.ftl">
</body>
</html>

View File

@ -0,0 +1,19 @@
<footer>
<ul>
<li>
<a href="/impressum">
<!--Impressum-->
${localize("footer.impressum")}
</a>
</li>
<li>
<a href="/privacy">
<!--Privacy-->
${localize("footer.privacy")}
</a>
</li>
<li>
<code>imported</code>
</li>
</ul>
</footer>

View File

@ -0,0 +1,2 @@
example.setting=Properties from the Freemarker theme.

View File

@ -0,0 +1,110 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
ul.news {
background-color: #000;
margin: 0 auto 3em auto;
width: 100vw;
padding: 3em;
}
ul.news li {
display: flex;
margin-left: auto;
margin-right: auto;
max-width: 50em;
}
ul.news li img {
max-height: 20em;
}
ul.news li span {
color: #fff;
flex: 1;
font-size: 2rem;
padding-left: 1em;
padding-right: 1em;
}
main div.boxes {
display: flex;
margin: 3em auto 3em auto;
max-width: 80em;
}
main div.boxes div {
position: relative;
flex: 1;
margin: 0 3em;
}
main div.boxes div p img {
width: 100%;
}
main div.boxes div a {
}
footer {
background-color: #000;
color: #fff;
width: 100vw;
}
footer ul {
list-style: none;
margin-left: auto;
margin-right: auto;
max-width: 80em;
padding-top: 4em;
padding-bottom: 4em;
}
footer ul li {
display: inline-block;
}
footer ul li:not(:first-child) {
margin-left: 4em;
}
footer ul li a:link {
color: #fff;
text-decoration: none;
}
footer ul li a:focus, footer ul li a:hover {
color: #fff;
text-decoration: underline;
}

View File

@ -0,0 +1,5 @@
label.critical=Critical
label.error=Error
label.ok=OK
label.warning=Warning

View File

@ -0,0 +1,3 @@
footer.impressum=Impressum
footer.privacy=Privacy

View File

@ -0,0 +1,6 @@
{
"name": "ccm-freemarker",
"type": "freemarker",
"default-template": "category-page.html.ftl"
}

View File

@ -580,6 +580,12 @@
<version>1.1.2</version> <version>1.1.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.27-incubating</version>
</dependency>
<dependency> <dependency>
<groupId>net.sf.saxon</groupId> <groupId>net.sf.saxon</groupId>
<artifactId>Saxon-HE</artifactId> <artifactId>Saxon-HE</artifactId>