From c20ddb09349814db1bd1aa4071c31dd9e6762bb0 Mon Sep 17 00:00:00 2001 From: jensp Date: Sun, 21 Jan 2018 11:52:03 +0000 Subject: [PATCH] CCM NG: First part of Theme Utility functions git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5210 8810af33-2d31-482b-a856-94f89814c4df --- ccm-core/pom.xml | 10 + .../libreccm/theming/utils/SettingsUtils.java | 340 ++++++++++++++++++ .../libreccm/theming/utils/package-info.java | 25 ++ .../theming/xslt/XsltThemeProcessor.java | 149 +++++++- .../resources/themes/ccm/category-page.xsl | 17 + .../resources/themes/ccm/settings.properties | 1 + pom.xml | 6 + 7 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 ccm-core/src/main/java/org/libreccm/theming/utils/SettingsUtils.java create mode 100644 ccm-core/src/main/java/org/libreccm/theming/utils/package-info.java create mode 100644 ccm-core/src/main/resources/themes/ccm/settings.properties diff --git a/ccm-core/pom.xml b/ccm-core/pom.xml index 5d5ad63ac..089c0c7fb 100644 --- a/ccm-core/pom.xml +++ b/ccm-core/pom.xml @@ -202,6 +202,16 @@ 0.9.11 + + org.glassfish + javax.json + + + + net.sf.saxon + Saxon-HE + + org.primefaces primefaces diff --git a/ccm-core/src/main/java/org/libreccm/theming/utils/SettingsUtils.java b/ccm-core/src/main/java/org/libreccm/theming/utils/SettingsUtils.java new file mode 100644 index 000000000..2e6b15cbb --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/utils/SettingsUtils.java @@ -0,0 +1,340 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.libreccm.theming.ThemeInfo; +import org.libreccm.theming.ThemeProvider; +import org.libreccm.theming.Themes; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.logging.Level; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +/** + * Methods for reading configuration options from the theme. Most themes have + * some configuration options. The class provides several methods for reading + * settings from different file formats. The following formats are currently + * supported: + * + * + * + * For property files and JSON files it is expected that the name of the setting + * is the key. For XML files the following simple format is expected: + * + *
+ *  <settings>
+ *      <setting key="nameOfSetting">value of setting</setting>
+ *      <setting key="anotherSetting">value of setting</setting>
+ *      ...
+ *  </settings/>
+ * 
+ * + * The file type is determined by the file extension of the file to read. + * + * @author Jens Pelzetter + */ +@RequestScoped +public class SettingsUtils { + + private static final Logger LOGGER = LogManager + .getLogger(SettingsUtils.class); + + /** + * Retrieve the value of a setting. + * + * @param fromTheme The theme from which the setting is read. + * @param themeProvider The provider of the the theme. + * @param filePath The path of the settings file relative to the root + * directory of the theme. + * @param settingName The name of the setting. + * + * @return The value of the setting as String. + */ + public String getSetting(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String filePath, + final String settingName) { + + return getSetting(fromTheme, + themeProvider, + filePath, + settingName, + null); + } + + /** + * Retrieve the value of a setting. If the provided file does not have a + * value for the setting the provided default value is used. + * + * @param fromTheme The theme from which the setting is read. + * @param themeProvider The provider of the theme. + * @param filePath The path of the settings file relative to the root + * directory of the theme. + * @param settingName The name of the setting. + * @param defaultValue The default value for the setting. + * + * @return The value of the setting as String. If the provided file has no + * value for the setting the provided default value is used. + */ + public String getSetting(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String filePath, + final String settingName, + final String defaultValue) { + + Objects.requireNonNull(fromTheme); + Objects.requireNonNull(themeProvider); + Objects.requireNonNull(filePath); + Objects.requireNonNull(settingName); + + if (filePath.isEmpty() || filePath.matches("\\s*")) { + throw new IllegalArgumentException( + "The path of the settings file can't be empty."); + } + + if (settingName.isEmpty() || settingName.matches("\\s*")) { + throw new IllegalArgumentException( + "The name of the settings file can't be empty."); + } + + final Optional fileInputStream = themeProvider + .getThemeFileAsStream(fromTheme.getName(), + fromTheme.getVersion(), + filePath); + if (!fileInputStream.isPresent()) { + LOGGER.warn( + "Configuration file \"{}\" was not found in theme \"{}\".", + filePath, + fromTheme.getName()); + return defaultValue; + } + + if (filePath.toLowerCase(Locale.ROOT).endsWith(".properties")) { + return getSettingFromPropertiesFile(fileInputStream.get(), + settingName) + .orElse(defaultValue); + } else if (filePath.toLowerCase(Locale.ROOT).endsWith(".xml")) { + return getSettingFromXmlFile(fileInputStream.get(), settingName) + .orElse(defaultValue); + } else if (filePath.toLowerCase(Locale.ROOT).endsWith(".json")) { + return getSettingFromJsonFile(fileInputStream.get(), settingName) + .orElse(defaultValue); + } else { + throw new IllegalArgumentException( + "The file path must point to file in a supported format. " + + "Supported formats are \".properties\", \".xml\" and \".json\"."); + } + } + + /** + * Retrieve the boolean value of a setting. This method reads the value of a + * setting using null null null null null null null null null null null null + * null null null null null null null null null null {@link #getSetting(java.lang.String, java.lang.String, java.lang.String, java.lang.String) + * and converts it into a {@code boolean} value using + * {@link Boolean#parseBoolean(java.lang.String)}. + * + * @param fromTheme The name of the theme from which the setting is + * read. + * @param themeProvider The provider of the theme. + * @param filePath The path of the settings file relative to the root + * directory of the theme. + * @param settingName The name of the setting. + * @param defaultValue The default value. + * + * @return The value of the setting as {@code boolean} or the default value + * if the configuration file has no value for the setting. + */ + public boolean getSettingAsBoolean(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String filePath, + final String settingName, + final boolean defaultValue) { + + final String result = getSetting(fromTheme, + themeProvider, + filePath, + settingName, + Boolean.toString(defaultValue)); + return Boolean.parseBoolean(result); + } + + /** + * Retrieve the value of setting and convert it to a {@code long} value. + * This method reads the value using + * {@link #getSetting(java.lang.String, java.lang.String, java.lang.String, java.lang.String)} + * and converts the value to a value of type {@code long} using + * {@link Long#parseLong(java.lang.String)}. + * + * + * @param fromTheme The name of the theme from which the setting is + * read. + * @param themeProvider The provider of the theme. + * @param filePath The path of the settings file relative to the root + * directory of the theme. + * @param settingName The name of the setting. + * @param defaultValue The default value. + * + * @return The value of the setting as {@code boolean} or the default value + * if the configuration file has no value for the setting. + * + * @throws NumberFormatException If the the value can't be converted into a + * {@code long} value. + */ + public long getSettingAsLong(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String filePath, + final String settingName, + final long defaultValue) { + + final String result = getSetting(fromTheme, + themeProvider, + filePath, + settingName, + Long.toString(defaultValue)); + return Long.parseLong(result); + } + + /** + * Retrieve the value of setting and convert it to a {@code double} value. + * This method reads the value using + * {@link #getSetting(java.lang.String, java.lang.String, java.lang.String, java.lang.String)} + * and converts the value to a value of type {@code double} using + * {@link Long#parseLong(java.lang.String)}. + * + * + * @param fromTheme The name of the theme from which the setting is + * read. + * @param themeProvider The provider of the theme. + * @param filePath The path of the settings file relative to the root + * directory of the theme. + * @param settingName The name of the setting. + * @param defaultValue The default value. + * + * @return The value of the setting as {@code boolean} or the default value + * if the configuration file has no value for the setting. + * + * @throws NumberFormatException If the the value can't be converted into a + * {@code double} value. + */ + public double getSettingAsDouble(final ThemeInfo fromTheme, + final ThemeProvider themeProvider, + final String filePath, + final String settingName, + final double defaultValue) { + + final String result = getSetting(fromTheme, + themeProvider, + filePath, + settingName, + settingName); + return Double.parseDouble(result); + } + + private Optional getSettingFromJsonFile( + final InputStream fileInputStream, + final String settingName) { + + final JsonReader jsonReader = Json.createReader(fileInputStream); + final JsonObject settings = jsonReader.readObject(); + final String result = settings.getString(settingName); + + return Optional.ofNullable(result); + } + + private Optional getSettingFromPropertiesFile( + final InputStream fileInputStream, + final String settingName) { + + final Properties settings = new Properties(); + try { + settings.load(fileInputStream); + } catch (IOException ex) { + LOGGER.warn("Failed to load setting file."); + return Optional.empty(); + } + + if (settings.containsKey(settingName)) { + return Optional.of(settings.getProperty(settingName)); + } else { + return Optional.empty(); + } + } + + private Optional getSettingFromXmlFile( + final InputStream fileInputStream, + final String settingName) { + + final Map settings; +// = new HashMap<>(); +// final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); +// final XMLStreamReader xmlStreamReader; +// try { +// xmlStreamReader = xmlInputFactory +// .createXMLStreamReader(fileInputStream); +// } catch (XMLStreamException ex) { +// LOGGER.error("Failed to read XML settings file."); +// return Optional.empty(); +// } +// +// while + + final JacksonXmlModule xmlModule = new JacksonXmlModule(); + final ObjectMapper mapper = new XmlMapper(xmlModule); + try { + settings = mapper + .reader() + .withRootName("settings") + .readValue(fileInputStream); + } catch (IOException ex) { + LOGGER.error("Failed to read Xfinal XML settings file."); + return Optional.empty(); + } + + if (settings.containsKey(settingName)) { + return Optional.of(settings.get(settingName)); + } else { + return Optional.empty(); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/utils/package-info.java b/ccm-core/src/main/java/org/libreccm/theming/utils/package-info.java new file mode 100644 index 000000000..ed0c58c95 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/utils/package-info.java @@ -0,0 +1,25 @@ +/* + * 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 + */ +/** + * This package provides several utility methods/functions for themes. + * They are implemented as static methods. The implementations of the + * {@link org.libreccm.theming.ThemeProcessor} interface are responsible for + * making the functions available in the the templates they process. + */ +package org.libreccm.theming.utils; diff --git a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java index 32b90b37b..e2a11e13e 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java +++ b/ccm-core/src/main/java/org/libreccm/theming/xslt/XsltThemeProcessor.java @@ -40,10 +40,22 @@ import javax.xml.parsers.ParserConfigurationException; import static org.libreccm.theming.ThemeConstants.*; import com.fasterxml.jackson.databind.SerializationFeature; +import net.sf.saxon.Configuration; +import net.sf.saxon.TransformerFactoryImpl; +import net.sf.saxon.expr.XPathContext; +import net.sf.saxon.lib.ExtensionFunctionCall; +import net.sf.saxon.lib.ExtensionFunctionDefinition; +import net.sf.saxon.om.Item; +import net.sf.saxon.om.Sequence; +import net.sf.saxon.om.StructuredQName; +import net.sf.saxon.trans.XPathException; +import net.sf.saxon.value.SequenceType; +import net.sf.saxon.value.StringValue; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.libreccm.theming.ProcessesThemes; import org.libreccm.theming.manifest.ThemeTemplate; +import org.libreccm.theming.utils.SettingsUtils; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -54,6 +66,7 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.Optional; +import javax.inject.Inject; import javax.xml.transform.ErrorListener; import javax.xml.transform.Result; import javax.xml.transform.Transformer; @@ -78,6 +91,9 @@ public class XsltThemeProcessor implements ThemeProcessor { private static final Logger LOGGER = LogManager .getLogger(XsltThemeProcessor.class); + @Inject + private SettingsUtils settingsUtils; + @Override public String process(final Map page, final ThemeInfo theme, @@ -160,7 +176,16 @@ public class XsltThemeProcessor implements ThemeProcessor { final StreamSource xslFileStreamSource = new StreamSource(reader); final TransformerFactory transformerFactory = TransformerFactory - .newInstance(); + .newInstance(TransformerFactoryImpl.class.getName(), + getClass().getClassLoader()); + final TransformerFactoryImpl transformerFactoryImpl + = (TransformerFactoryImpl) transformerFactory; + final Configuration configuration = transformerFactoryImpl + .getConfiguration(); + configuration.registerExtensionFunction(new Greeter()); + configuration + .registerExtensionFunction( + new GetSettingFunctionDefinition(theme, themeProvider)); final Transformer transformer; try { transformer = transformerFactory.newTransformer(xslFileStreamSource); @@ -212,4 +237,126 @@ public class XsltThemeProcessor implements ThemeProcessor { return resultWriter.toString(); } + private class GetSettingFunctionDefinition extends ExtensionFunctionDefinition { + + private final ThemeInfo theme; + private final ThemeProvider themeProvider; + + public GetSettingFunctionDefinition(final ThemeInfo themeInfo, + final ThemeProvider themeProvider) { + this.theme = themeInfo; + this.themeProvider = themeProvider; + } + + @Override + public StructuredQName getFunctionQName() { + return new StructuredQName("ccm", + "http://xmlns.libreccm.org", + "getSetting"); + } + + @Override + public SequenceType[] getArgumentTypes() { + return new SequenceType[]{SequenceType.SINGLE_STRING, + SequenceType.SINGLE_STRING, + SequenceType.SINGLE_STRING}; + } + + @Override + public int getMinimumNumberOfArguments() { + return 2; + } + + @Override + public int getMaximumNumberOfArguments() { + return 3; + } + + @Override + public SequenceType getResultType(final SequenceType[] sts) { + return SequenceType.SINGLE_STRING; + } + + @Override + public ExtensionFunctionCall makeCallExpression() { + + return new ExtensionFunctionCall() { + + @Override + public Sequence call(final XPathContext xPathContext, + final Sequence[] arguments) + throws XPathException { + + final String result; + final String filePath = ((Item) arguments[0]) + .getStringValue(); + final String settingName = ((Item) arguments[1]) + .getStringValue(); + switch (arguments.length) { + case 2: + result = settingsUtils.getSetting(theme, + themeProvider, + filePath, + settingName); + break; + case 3: + final String defaultValue = ((Item) arguments[2]) + .getStringValue(); + result = settingsUtils.getSetting(theme, + themeProvider, + filePath, + settingName, + defaultValue); + break; + default: + throw new UnexpectedErrorException( + "Illegal number of arguments."); + } + + return StringValue.makeStringValue(result); + } + + }; + } + + } + + private class Greeter extends ExtensionFunctionDefinition { + + @Override + public StructuredQName getFunctionQName() { + return new StructuredQName("ccm", + "http://xmlns.libreccm.org", + "greet"); + } + + @Override + public SequenceType[] getArgumentTypes() { + return new SequenceType[]{SequenceType.SINGLE_STRING}; + } + + @Override + public SequenceType getResultType(final SequenceType[] sts) { + return SequenceType.SINGLE_STRING; + } + + @Override + public ExtensionFunctionCall makeCallExpression() { + return new ExtensionFunctionCall() { + + @Override + public Sequence call(final XPathContext xPathContext, + final Sequence[] arguments) + throws XPathException { + + final String name = arguments[0].toString(); + return StringValue + .makeStringValue(String.format("Hello %s.", name)); + } + + }; + } + + } + } diff --git a/ccm-core/src/main/resources/themes/ccm/category-page.xsl b/ccm-core/src/main/resources/themes/ccm/category-page.xsl index d0f8c6c2b..e66fd4d88 100644 --- a/ccm-core/src/main/resources/themes/ccm/category-page.xsl +++ b/ccm-core/src/main/resources/themes/ccm/category-page.xsl @@ -26,6 +26,23 @@

+

Example of Saxon Extension Function

+
+            
+        
+ +

Example of Theme Utils

+
+
+ getSetting +
+
+ + + +
+
+ diff --git a/ccm-core/src/main/resources/themes/ccm/settings.properties b/ccm-core/src/main/resources/themes/ccm/settings.properties new file mode 100644 index 000000000..1e804789f --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm/settings.properties @@ -0,0 +1 @@ +example.setting=Properties from the theme. diff --git a/pom.xml b/pom.xml index 28ea1f650..ab18f6e8f 100644 --- a/pom.xml +++ b/pom.xml @@ -548,6 +548,12 @@
+ + org.glassfish + javax.json + 1.1.2 + + net.sf.saxon Saxon-HE