diff --git a/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java b/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java
index 8c179c2d5..cf2b69679 100644
--- a/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java
+++ b/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java
@@ -30,6 +30,9 @@ public interface LoginConstants {
public static final String LOGIN_BUNDLE
= "com.arsdigita.ui.login.LoginResources";
+
+ public static final String LOGIN_UI_BUNDLE
+ = "org.libreccm.ui.LoginBundle";
public static final GlobalizedMessage SUBMIT = LoginHelper.getMessage(
"login.submit");
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java
new file mode 100644
index 000000000..19d17d176
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2020 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.mvc.freemarker;
+
+import freemarker.template.Configuration;
+import freemarker.template.SimpleNumber;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import org.eclipse.krazo.engine.ViewEngineBase;
+import org.eclipse.krazo.engine.ViewEngineConfig;
+import org.libreccm.theming.ThemeInfo;
+import org.libreccm.theming.ThemeProvider;
+import org.libreccm.theming.utils.L10NUtils;
+import org.libreccm.theming.utils.SettingsUtils;
+import org.libreccm.theming.utils.TextUtils;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.annotation.Priority;
+import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.context.spi.Context;
+import javax.enterprise.context.spi.CreationalContext;
+import javax.enterprise.inject.spi.Bean;
+import javax.enterprise.inject.spi.BeanManager;
+import javax.inject.Inject;
+import javax.mvc.MvcContext;
+import javax.mvc.engine.ViewEngine;
+import javax.mvc.engine.ViewEngineContext;
+import javax.mvc.engine.ViewEngineException;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Customized version of the Freemarker View Engine. This class is based of the
+ * View Engine from the Krazo project, but has been extended:
+ *
+ *
Named Beans are supported
+ *
Freemarker template have access to the MvcContext under the name
+ * {@code mvc}, as in Facelet-based templates
+ *
The current {@link HttpServletRequest} is made avaiable in Freemarker
+ * templates as {@link request}.
+ *
The following utility functions are made available:
+ *
+ *
{@code getSetting}: retreives the value of a setting from the theme
+ *
{@code localize}: retreives a localized value from the theme
+ *
{@code truncateText}: Truncates text to a specific length.
+ *
+ *
+ *
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationScoped
+@Priority(ViewEngine.PRIORITY_APPLICATION)
+public class FreemarkerViewEngine extends ViewEngineBase {
+
+ @Inject
+ private BeanManager beanManager;
+
+ @Inject
+ @ViewEngineConfig
+ private Configuration configuration;
+
+ @Inject
+ private MvcContext mvc;
+
+ @Inject
+ private L10NUtils l10nUtils;
+
+ @Inject
+ private SettingsUtils settingsUtils;
+
+ @Inject
+ private TextUtils textUtils;
+
+ @Inject
+ private ThemeTemplateUtil themeTemplateUtil;
+
+ @Override
+ public boolean supports(String view) {
+ return view.endsWith(".ftl");
+ }
+
+ @Override
+ public void processView(final ViewEngineContext context)
+ throws ViewEngineException {
+
+ final Charset charset = resolveCharsetAndSetContentType(context);
+
+ try (final Writer writer = new OutputStreamWriter(
+ context.getOutputStream(), charset
+ )) {
+ final Template template = configuration.getTemplate(
+ resolveView(context)
+ );
+
+ final Map model = new HashMap<>();
+ model.put("mvc", mvc);
+ model.put("request", context.getRequest(HttpServletRequest.class));
+
+ final Optional templateInfo = themeTemplateUtil
+ .getTemplateInfo(context.getView());
+ final ThemeProvider themeProvider = themeTemplateUtil
+ .findThemeProvider(templateInfo.get().getThemeInfo());
+ if (templateInfo.isPresent()) {
+ final ThemeInfo themeInfo = templateInfo.get().getThemeInfo();
+ model.put("getSetting",
+ new GetSettingMethod(themeInfo, themeProvider)
+ );
+ model.put("localize",
+ new LocalizeMethod(themeInfo, themeProvider)
+ );
+ }
+ model.put("truncateText", new TruncateTextMethod());
+
+ final Map namedBeans = beanManager
+ .getBeans(Object.class)
+ .stream()
+ .filter(bean -> bean.getName() != null)
+ .map(this::findBeanInstance)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(
+ Collectors.toMap(
+ NamedBeanInstance::getName,
+ NamedBeanInstance::getBeanInstance
+ )
+ );
+
+ model.putAll(namedBeans);
+ model.putAll(context.getModels().asMap());
+
+ template.process(model, writer);
+ } catch (TemplateException | IOException e) {
+ throw new ViewEngineException(e);
+ }
+ }
+
+ /**
+ * Helper method for retrieving a an instance of a named bean using CDI.
+ *
+ * @param bean The bean to retrieve.
+ *
+ * @return An instance of the bean.
+ */
+ @SuppressWarnings("rawtypes")
+ private Optional findBeanInstance(final Bean> bean) {
+ final Context context = beanManager.getContext(bean.getScope());
+ final CreationalContext creationalContext = beanManager
+ .createCreationalContext(bean);
+ @SuppressWarnings("unchecked")
+ final Object instance = context.get(bean, creationalContext);
+
+ if (instance == null) {
+ return Optional.empty();
+ } else {
+ return Optional.of(
+ new NamedBeanInstance(bean.getName(), instance)
+ );
+ }
+ }
+
+ /**
+ * Helper class encapsulating the information about a named bean.
+ */
+ private class NamedBeanInstance {
+
+ /**
+ * The name of the bean.
+ */
+ private final String name;
+
+ /**
+ * The bean instance.
+ */
+ private final Object beanInstance;
+
+ public NamedBeanInstance(String name, Object beanInstance) {
+ this.name = name;
+ this.beanInstance = beanInstance;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Object getBeanInstance() {
+ return beanInstance;
+ }
+
+ }
+
+ /**
+ * Retrieves a setting from the theme using the {@link SettingsUtils}.
+ */
+ 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.");
+ }
+ }
+
+ }
+
+ /**
+ * Retrieves a localized value from the theme using the {@link L10NUtils}.
+ */
+ 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);
+ }
+
+ }
+
+ /**
+ * Truncates text to a specific length.
+ */
+ 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.");
+ }
+ }
+
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java
index 6b31bbc11..d10c970f8 100644
--- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java
@@ -53,7 +53,9 @@ class KrazoTemplateLoader implements TemplateLoader {
@Override
public Object findTemplateSource(final String name) throws IOException {
- if (name.startsWith("@themes") || name.startsWith("/@themes")) {
+ if (name.startsWith("@themes")
+ || name.startsWith("/@themes")
+ || name.startsWith("WEB-INF/views/@themes")) {
return null;
} else {
// Freemarker drops "/"
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java
index a209a462a..83c5c6fd9 100644
--- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java
@@ -28,28 +28,33 @@ import org.eclipse.krazo.engine.ViewEngineConfig;
import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer;
import org.libreccm.theming.Themes;
-import javax.enterprise.context.ApplicationScoped;
+import javax.enterprise.inject.Produces;
import javax.enterprise.inject.Specializes;
import javax.inject.Inject;
+import javax.mvc.Models;
import javax.servlet.ServletContext;
-import javax.ws.rs.Produces;
/**
- * Extends the default configuration for Freemarker of Eclipse Krazo to
- * support Freemarker templates in CCM themes.
- *
+ * Extends the default configuration for Freemarker of Eclipse Krazo to support
+ * Freemarker templates in CCM themes.
+ *
* @author Jens Pelzetter
*/
-@ApplicationScoped
-public class MvcFreemarkerConfigurationProducer
+public class MvcFreemarkerConfigurationProducer
extends DefaultConfigurationProducer {
-
+
+ @Inject
+ private Models models;
+
@Inject
private ServletContext servletContext;
-
+
@Inject
private Themes themes;
+ @Inject
+ private ThemeTemplateUtil themeTemplateUtil;
+
@Produces
@ViewEngineConfig
@Specializes
@@ -58,7 +63,7 @@ public class MvcFreemarkerConfigurationProducer
final Configuration configuration = new Configuration(
Configuration.VERSION_2_3_30
);
-
+
configuration.setDefaultEncoding("UTF-8");
configuration.setTemplateExceptionHandler(
TemplateExceptionHandler.RETHROW_HANDLER
@@ -70,7 +75,7 @@ public class MvcFreemarkerConfigurationProducer
new MultiTemplateLoader(
new TemplateLoader[]{
new KrazoTemplateLoader(servletContext),
- new ThemesTemplateLoader(themes),
+ new ThemesTemplateLoader(themes, themeTemplateUtil),
// For loading Freemarker macro libraries from WEB-INF
// resources
new WebappTemplateLoader(
@@ -82,8 +87,8 @@ public class MvcFreemarkerConfigurationProducer
}
)
);
-
+
return configuration;
}
-
+
}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java
new file mode 100644
index 000000000..55d6fe3d2
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 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.mvc.freemarker;
+
+import org.libreccm.theming.ThemeInfo;
+
+/**
+ * Encapulates the data of a template.
+ *
+ * @author Jens Pelzetter
+ */
+class TemplateInfo {
+
+ /**
+ * Info about the theme providing the template.
+ */
+ private final ThemeInfo themeInfo;
+
+ /**
+ * The path of the template,
+ */
+ private final String filePath;
+
+ public TemplateInfo(ThemeInfo themeInfo, String filePath) {
+ this.themeInfo = themeInfo;
+ this.filePath = filePath;
+ }
+
+ public ThemeInfo getThemeInfo() {
+ return themeInfo;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java
new file mode 100644
index 000000000..dc9721245
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2021 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.mvc.freemarker;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.libreccm.core.UnexpectedErrorException;
+import org.libreccm.theming.ThemeInfo;
+import org.libreccm.theming.ThemeProvider;
+import org.libreccm.theming.ThemeVersion;
+import org.libreccm.theming.Themes;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
+
+/**
+ * Utility class for retreving a {@link TemplateInfo} instance for a template.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+class ThemeTemplateUtil {
+
+ private static final Logger LOGGER = LogManager.getLogger(
+ ThemeTemplateUtil.class);
+
+ @Inject
+ private Instance themeProviders;
+
+ @Inject
+ private Themes themes;
+
+ /**
+ * Checks if the provided path points to a template.
+ *
+ * @param templatePath The path of the template.
+ *
+ * @return {@code true} if the path points to a template, {@code false}
+ * otherwise.
+ */
+ public boolean isValidTemplatePath(final String templatePath) {
+ return templatePath.startsWith("@themes")
+ || templatePath.startsWith("/@themes");
+ }
+
+ /**
+ * Get the {@link TemplateInfo} for the template.
+ *
+ * @param templatePath The path of the template.
+ *
+ * @return An {@link Optional} with a {@link TemplateInfo} for the template.
+ * If the template is not available, an empty {@link Optional} is
+ * returned.
+ */
+ public Optional getTemplateInfo(final String templatePath) {
+ if (!isValidTemplatePath(templatePath)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Provided template \"%s\" path does not start with "
+ + "\"@theme\" or \"/@theme\".",
+ templatePath
+ )
+ );
+ }
+
+ final String[] tokens;
+ if (templatePath.startsWith("/")) {
+ tokens = templatePath.substring(1).split("/");
+ } else {
+ tokens = templatePath.split("/");
+ }
+
+ return getTemplateInfo(tokens);
+ }
+
+ /**
+ * Find the {@link ThemeProvider} for a theme.
+ *
+ * @param forTheme The theme
+ *
+ * @return The {@link ThemeProvider} for the theme.
+ */
+ public ThemeProvider findThemeProvider(final ThemeInfo forTheme) {
+ final Instance extends ThemeProvider> provider = themeProviders
+ .select(forTheme.getProvider());
+
+ if (provider.isUnsatisfied()) {
+ LOGGER.error("ThemeProvider \"{}\" not found.",
+ forTheme.getProvider().getName());
+ throw new UnexpectedErrorException(
+ String.format(
+ "ThemeProvider \"%s\" not found.",
+ forTheme.getProvider().getName()
+ )
+ );
+ }
+
+ return provider.get();
+ }
+
+ /**
+ * Retrieves the {@link TemplateInfo} for a template.
+ *
+ * @param tokens The tokens of the template path.
+ *
+ * @return An {@link Optional} with a {@link TemplateInfo} for the template.
+ * If the template is not available, an empty {@link Optional} is
+ * returned.
+ */
+ private Optional getTemplateInfo(final String[] tokens) {
+ if (tokens.length >= 4) {
+ final String themeName = tokens[1];
+ final ThemeVersion themeVersion = ThemeVersion.valueOf(
+ tokens[2]
+ );
+ final String filePath = String.join(
+ "/",
+ Arrays.copyOfRange(
+ tokens, 3, tokens.length, String[].class
+ )
+ );
+
+ final Optional themeInfo = themes.getTheme(
+ themeName, themeVersion
+ );
+
+ if (themeInfo.isPresent()) {
+ return Optional.of(new TemplateInfo(themeInfo.get(), filePath));
+ } else {
+ return Optional.empty();
+ }
+ } else {
+ throw new IllegalArgumentException(
+ String.format(
+ "Template path has wrong format. Expected at least "
+ + "four tokens separated by slashes, but found only %d",
+ tokens.length
+ )
+ );
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java
index 478a505a5..eaf9ede78 100644
--- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java
+++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java
@@ -19,7 +19,6 @@
package org.libreccm.mvc.freemarker;
import freemarker.cache.TemplateLoader;
-import org.libreccm.theming.ThemeInfo;
import org.libreccm.theming.ThemeVersion;
import org.libreccm.theming.Themes;
@@ -27,7 +26,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
-import java.util.Arrays;
import java.util.Optional;
/**
@@ -39,8 +37,13 @@ class ThemesTemplateLoader implements TemplateLoader {
private final Themes themes;
- public ThemesTemplateLoader(final Themes themes) {
+ private final ThemeTemplateUtil themeTemplateUtil;
+
+ public ThemesTemplateLoader(
+ final Themes themes, final ThemeTemplateUtil themeTemplateUtil
+ ) {
this.themes = themes;
+ this.themeTemplateUtil = themeTemplateUtil;
}
/**
@@ -53,73 +56,37 @@ class ThemesTemplateLoader implements TemplateLoader {
* of the theme from which the template is loaded. {@code $version} is the
* version of the theme to use. This token is converted to
* {@link ThemeVersion}. Valid values are therefore {@code DRAFT} and
- * {@code LIVE}. The remainder of the path is the path to the file inside the
- * theme.
+ * {@code LIVE}. The remainder of the path is the path to the file inside
+ * the theme.
*
* @param path The path of the file. The path must include the theme and its
* version.
*
* @return An {@link InputStream} for the template if the template was found
- * in the theme. Otherwise {@code null} is returned.
+ * in the theme. Otherwise {@code null} is returned.
*
* @throws IOException
*/
@Override
public Object findTemplateSource(final String path) throws IOException {
- if (path.startsWith("@themes") || path.startsWith("/@themes")) {
- final String[] tokens;
- if (path.startsWith("/")) {
- tokens = path.substring(1).split("/");
+ if (themeTemplateUtil.isValidTemplatePath(path)) {
+ final Optional templateInfo = themeTemplateUtil
+ .getTemplateInfo(path);
+
+ if (templateInfo.isPresent()) {
+ final Optional source = themes.getFileFromTheme(
+ templateInfo.get().getThemeInfo(),
+ templateInfo.get().getFilePath()
+ );
+
+ if (source.isPresent()) {
+ return source.get();
+ } else {
+ return null;
+ }
} else {
- tokens = path.split("/");
+ return null;
}
- return findTemplateSource(tokens);
- } else {
- return null;
- }
- }
-
- private InputStream findTemplateSource(final String[] tokens) {
- if (tokens.length >= 4) {
- final String themeName = tokens[1];
- final ThemeVersion themeVersion = ThemeVersion
- .valueOf(tokens[2]);
- final String filePath = String.join(
- "/",
- Arrays.copyOfRange(
- tokens, 3, tokens.length, String[].class
- )
- );
-
- return findTemplateSource(themeName, themeVersion, filePath);
- } else {
- return null;
- }
- }
-
- private InputStream findTemplateSource(
- final String themeName,
- final ThemeVersion themeVersion,
- final String filePath
- ) {
- final Optional themeInfo = themes.getTheme(
- themeName, themeVersion
- );
- if (themeInfo.isPresent()) {
- return findTemplateSource(themeInfo.get(), filePath);
- } else {
- return null;
- }
- }
-
- private InputStream findTemplateSource(
- final ThemeInfo themeInfo, final String filePath
- ) {
- final Optional source = themes.getFileFromTheme(
- themeInfo, filePath
- );
- if (source.isPresent()) {
- return source.get();
} else {
return null;
}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java
index ed89e5afd..0c03cec1f 100644
--- a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java
+++ b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java
@@ -39,6 +39,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -53,25 +54,31 @@ import javax.json.JsonObject;
import javax.json.JsonReader;
/**
- * Implementation of {@link ThemeProvider} for serves themes stored in the
+ * Implementation of {@link ThemeProvider} for serves themes stored in the
* classpath.
- *
+ *
* @author Jens Pelzetter
*/
@RequestScoped
public class StaticThemeProvider implements ThemeProvider {
private static final long serialVersionUID = 1L;
+
private static final Logger LOGGER = LogManager.getLogger(
StaticThemeProvider.class);
private static final String THEMES_DIR = "/themes";
+
private static final String THEMES_PACKAGE = "themes";
+
private static final String THEME_MANIFEST_JSON_PATH = THEMES_DIR
+ "/%s/theme.json";
+
private static final String THEME_MANIFEST_XML_PATH = THEMES_DIR
+ "/%s/theme.xml";
+
private static final String THEME_MANIFEST_JSON = "theme.json";
+
private static final String THEME_MANIFEST_XML = "theme.xml";
@Inject
@@ -84,7 +91,7 @@ public class StaticThemeProvider implements ThemeProvider {
public String getName() {
return "StaticThemeProvider";
}
-
+
@Override
public List getThemes() {
@@ -472,18 +479,26 @@ public class StaticThemeProvider implements ThemeProvider {
final String fileName = path.get(0);
- final Optional fileData = currentDirectory
+ final Optional fileDataResult = currentDirectory
.stream()
.map(value -> (JsonObject) value)
.filter(value -> filterFileData(value, fileName))
.findAny();
if (path.size() == 1) {
- return fileData;
+ return fileDataResult;
} else {
-
- if (fileData.get().getBoolean("isDirectory")) {
- return findFile(path.subList(1, path.size()),
- fileData.get().getJsonArray("files"));
+ final JsonObject fileData = fileDataResult
+ .orElseThrow(
+ () -> new NoSuchElementException(
+ String.format(
+ "File %s not found.", path
+ )
+ )
+ );
+ if (fileData.getBoolean("isDirectory")) {
+ return findFile(
+ path.subList(1, path.size()), fileData.getJsonArray("files")
+ );
} else {
return Optional.empty();
}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java
index 76133ed7e..27f1a5b76 100644
--- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java
+++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java
@@ -34,6 +34,9 @@ import javax.xml.bind.annotation.XmlRootElement;
import static org.libreccm.theming.ThemeConstants.*;
import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
/**
* Each theme contains a Manifest (either in XML or JSON format) which provides
@@ -88,8 +91,16 @@ public class ThemeManifest implements Serializable {
@XmlElement(name = "default-template", namespace = THEMES_XML_NS)
private String defaultTemplate;
+ @XmlElement(name = "mvc-templates", namespace = THEMES_XML_NS)
+ private Map mvcTemplates;
+
+ @XmlElement(name = "views", namespace = THEMES_XML_NS)
+ private Map> views;
+
public ThemeManifest() {
templates = new ArrayList<>();
+ mvcTemplates = new HashMap<>();
+ views = new HashMap<>();
}
public String getName() {
@@ -156,6 +167,62 @@ public class ThemeManifest implements Serializable {
this.defaultTemplate = defaultTemplate;
}
+ public Map getMvcTemplates() {
+ return Collections.unmodifiableMap(mvcTemplates);
+ }
+
+ public Optional getMvcTemplate(final String name) {
+ return Optional.ofNullable(mvcTemplates.get(name));
+ }
+
+ public void addMvcTemplate(
+ final String name, final ThemeTemplate template
+ ) {
+ mvcTemplates.put(name, template);
+ }
+
+ protected void setMvcTemplates(
+ final Map mvcTemplates
+ ) {
+ this.mvcTemplates = mvcTemplates;
+ }
+
+ public Map> getViews() {
+ return Collections.unmodifiableMap(views);
+ }
+
+ public Map getViewsOfApplication(final String application) {
+ if (views.containsKey(application)) {
+ return views.get(application);
+ } else {
+ return Collections.emptyMap();
+ }
+ }
+
+ public void addViewsOfApplication(
+ final String application, final Map viewsOfApplication
+ ) {
+ views.put(application, viewsOfApplication);
+ }
+
+ public void addViewToApplication(
+ final String application, final String view, final String templateName
+ ) {
+ final Map applicationViews;
+ if (views.containsKey(application)) {
+ applicationViews = views.get(application);
+ } else {
+ applicationViews = new HashMap<>();
+ views.put(application, applicationViews);
+ }
+
+ applicationViews.put(view, templateName);
+ }
+
+ protected void setViews(final Map> views) {
+ this.views = new HashMap<>(views);
+ }
+
@Override
public int hashCode() {
int hash = 7;
@@ -166,6 +233,8 @@ public class ThemeManifest implements Serializable {
hash = 83 * hash + Objects.hashCode(description);
hash = 83 * hash + Objects.hashCode(templates);
hash = 83 * hash + Objects.hashCode(defaultTemplate);
+ hash = 83 * hash + Objects.hashCode(mvcTemplates);
+ hash = 83 * hash + Objects.hashCode(views);
return hash;
}
@@ -202,7 +271,14 @@ public class ThemeManifest implements Serializable {
if (!Objects.equals(templates, other.getTemplates())) {
return false;
}
- return Objects.equals(defaultTemplate, other.getDefaultTemplate());
+ if (!Objects.equals(defaultTemplate, other.getDefaultTemplate())) {
+ return false;
+ }
+ if (!Objects.equals(mvcTemplates, other.getMvcTemplates())) {
+ return false;
+ }
+
+ return Objects.equals(views, other.getViews());
}
public boolean canEqual(final Object obj) {
@@ -216,24 +292,30 @@ public class ThemeManifest implements Serializable {
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);
+ return String.format(
+ "%s{ "
+ + "name = \"%s\", "
+ + "type = \"%s\", "
+ + "masterTheme = \"%s\", "
+ + "title = \"%s\", "
+ + "description = \"%s\", "
+ + "templates = %s, "
+ + "defaultTemplate, "
+ + "mvcTemplates = %s,"
+ + "views = %s%s"
+ + " }",
+ super.toString(),
+ name,
+ type,
+ masterTheme,
+ Objects.toString(title),
+ Objects.toString(description),
+ Objects.toString(templates),
+ defaultTemplate,
+ Objects.toString(mvcTemplates),
+ Objects.toString(views),
+ data
+ );
}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java
index 9a3ef5c2a..aada91252 100644
--- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java
+++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java
@@ -18,7 +18,8 @@
*/
package org.libreccm.theming.manifest;
-import static org.libreccm.theming.ThemeConstants.*;
+import static org.libreccm.theming.ThemeConstants.THEME_MANIFEST_JSON;
+import static org.libreccm.theming.ThemeConstants.THEME_MANIFEST_XML;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -60,8 +61,6 @@ public class ThemeManifestUtil implements Serializable {
* @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"));
@@ -70,34 +69,11 @@ public class ThemeManifestUtil implements Serializable {
}
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) {
-
+ public ThemeManifest loadManifest(
+ final InputStream inputStream, final String fileName
+ ) {
final InputStreamReader reader;
try {
reader = new InputStreamReader(inputStream, "UTF-8");
@@ -106,34 +82,11 @@ public class ThemeManifestUtil implements Serializable {
}
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) {
-
+ public String serializeManifest(
+ final ThemeManifest manifest, final String format
+ ) {
final ObjectMapper mapper;
switch (format) {
diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java
new file mode 100644
index 000000000..18315fdd9
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2021 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.mvc;
+
+import org.libreccm.theming.ThemeFileInfo;
+import org.libreccm.theming.ThemeProvider;
+import org.libreccm.theming.ThemeVersion;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.enterprise.inject.Any;
+import javax.enterprise.inject.Instance;
+import javax.inject.Inject;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+
+/**
+ * Provides access to the resources/assets of themes.
+ *
+ * @see ThemeResources
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Path("/")
+public class ThemeResourceProvider {
+
+ /**
+ * Injection point for the available {@link ThemeProvider}s.
+ */
+ @Inject
+ @Any
+ private Instance providers;
+
+ /**
+ * Serves a resources from a theme. This endpoint is mounted at
+ * {@code /@themes/{theme}/{themeVersion}/{path:.+}}. If the provided theme
+ * is not found, the provided theme is not available in the requested
+ * version, or if the theme does not provide the requested resource the
+ * endpoint will response with a 404 response. If the response is found, a
+ * response with the asset and the correct mime type is returned.
+ *
+ * @param themeName The name of the theme providing the resource.
+ * @param themeVersionParam The version of the theme to use (either
+ * {@code LIVE} or {@code DRAFT}.
+ * @param pathParam The path of the resource to serve.
+ *
+ * @return A response with the resource and the correct mime type, or a 404
+ * response if the resource, the theme version or the theme is not
+ * available.
+ */
+ @GET
+ @Path("/{theme}/{themeVersion}/{path:.+}")
+ public Response getThemeFile(
+ @PathParam("theme") final String themeName,
+ @PathParam("themeVersion") final String themeVersionParam,
+ @PathParam("path") final String pathParam
+ ) {
+ final Optional provider = findProvider(themeName);
+ final ThemeVersion themeVersion = ThemeVersion.valueOf(
+ themeVersionParam
+ );
+
+ if (provider.isPresent()) {
+ final Optional fileInfo = provider
+ .get()
+ .getThemeFileInfo(themeName, themeVersion, pathParam);
+
+ if (fileInfo.isPresent()) {
+ final ThemeFileInfo themeFileInfo = fileInfo.get();
+ if (themeFileInfo.isDirectory()) {
+ return Response.status(Response.Status.FORBIDDEN).build();
+ } else {
+ final Optional inputStream = provider
+ .get()
+ .getThemeFileAsStream(
+ themeName, themeVersion, pathParam
+ );
+ if (inputStream.isPresent()) {
+ final InputStream inStream = inputStream.get();
+ return Response
+ .ok(inStream)
+ .type(themeFileInfo.getMimeType())
+ .build();
+ } else {
+ return Response
+ .status(Response.Status.NOT_FOUND)
+ .entity(
+ String.format(
+ "File \"%s\" does not exist in version of "
+ + "theme %s.",
+ pathParam,
+ themeVersion,
+ themeName
+ )
+ )
+ .build();
+ }
+ }
+ } else {
+ return Response
+ .status(Response.Status.NOT_FOUND)
+ .entity(
+ String.format(
+ "File \"%s\" does not exist in the %s "
+ + "version of theme %s.",
+ pathParam,
+ themeVersion,
+ themeName
+ )
+ )
+ .build();
+ }
+ } else {
+ return Response
+ .status(Response.Status.NOT_FOUND)
+ .entity(String.format("Theme \"%s\" does not exist.",
+ themeName))
+ .build();
+ }
+ }
+
+ /**
+ * Helper method for finding the provider of a theme.
+ *
+ * @param forTheme The theme.
+ *
+ * @return An {@link Optional} with the provider of the theme. If there is
+ * no matching provider, an empty {@link Optional} is returned.
+ */
+ private Optional findProvider(final String forTheme) {
+ final List providersList = new ArrayList<>();
+ providers
+ .forEach(provider -> providersList.add(provider));
+
+ return providersList
+ .stream()
+ .filter(current -> current.providesTheme(forTheme,
+ ThemeVersion.DRAFT))
+ .findAny();
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java
new file mode 100644
index 000000000..246bf19c6
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2021 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.mvc;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+/**
+ * JAX-RS application providing the resources/assets of a theme (images, CSS
+ * files, etc) under the {@code /@themes} URL.
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationPath("/@themes")
+public class ThemeResources extends Application {
+
+ @Override
+ public Set> getClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(ThemeResourceProvider.class);
+ return classes;
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java
new file mode 100644
index 000000000..70809320e
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2020 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.mvc;
+
+import org.libreccm.sites.Site;
+import org.libreccm.sites.SiteRepository;
+import org.libreccm.theming.ThemeInfo;
+import org.libreccm.theming.ThemeVersion;
+import org.libreccm.theming.Themes;
+import org.libreccm.theming.manifest.ThemeManifest;
+import org.libreccm.theming.manifest.ThemeTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mvc.Models;
+import javax.servlet.ServletContext;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ * Main integration point for MVC application with the theme system.
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+public class ThemesMvc {
+
+ @Inject
+ private Models models;
+
+ @Inject
+ private ServletContext servletContext;
+
+ @Inject
+ private SiteRepository siteRepo;
+
+ @Inject
+ private Themes themes;
+
+ /**
+ * Get the template for a specific application and view from the current
+ * theme.
+ *
+ * @param uriInfo URI is required for some tasks in inside the method.
+ * @param application The application for which the template is requested.
+ * @param view The view for which the template is requested.
+ *
+ * @return The path of the template to use for the view of the application.
+ */
+ public String getMvcTemplate(
+ final UriInfo uriInfo,
+ final String application,
+ final String view
+ ) {
+ final Site site = getSite(uriInfo);
+ final String theme = parseThemeParam(uriInfo);
+ final ThemeVersion themeVersion = parsePreviewParam(uriInfo);
+ final ThemeInfo themeInfo = getTheme(
+ site,
+ theme,
+ themeVersion
+ );
+ final ThemeManifest manifest = themeInfo.getManifest();
+ final Map views = manifest.getViewsOfApplication(
+ application
+ );
+ final String viewTemplateName;
+ if (views.containsKey(view)) {
+ viewTemplateName = views.get(view);
+ } else {
+ final Map defaultAppViews = manifest
+ .getViewsOfApplication(application);
+ if (defaultAppViews.containsKey("default")) {
+ viewTemplateName = defaultAppViews.get("default");
+ } else {
+ throw new WebApplicationException(
+ String.format(
+ "Theme \"%s\" does not provide a template for view "
+ + "\"%s\" of application \"%s\", and there is no "
+ + "default template configured.",
+ themeInfo.getName(),
+ view,
+ application
+ )
+ );
+ }
+ }
+
+ final ThemeTemplate themeTemplate = manifest
+ .getMvcTemplate(viewTemplateName)
+ .orElseThrow(
+ () -> new WebApplicationException(
+ String.format(
+ "Theme \"%s\" maps view \"%s\" of application \"%s\" "
+ + "to template \"%s\" but not template with this "
+ + "name was found in the theme.",
+ themeInfo.getName(),
+ view,
+ application,
+ viewTemplateName
+ )
+ )
+ );
+
+ models.put("contextPath", servletContext.getContextPath());
+ models.put("themeName", themeInfo.getName());
+ models.put("themeVersion", themeInfo.getVersion());
+ models.put(
+ "themeUrl",
+ String.format(
+ "%s/@themes/%s/%s",
+ servletContext.getContextPath(),
+ themeInfo.getName(),
+ themeInfo.getVersion()
+ )
+ );
+
+ return String.format(
+ "/@themes/%s/%s/%s",
+ themeInfo.getName(),
+ Objects.toString(themeVersion),
+ themeTemplate.getPath()
+ );
+ }
+
+ /**
+ * Helper method of retrieving the current site.
+ *
+ * @param uriInfo Used to extract the current site.
+ *
+ * @return The current site.
+ */
+ private Site getSite(final UriInfo uriInfo) {
+ Objects.requireNonNull(uriInfo);
+
+ final String domain = uriInfo.getBaseUri().getHost();
+
+ final Site site;
+ if (siteRepo.hasSiteForDomain(domain)) {
+ site = siteRepo.findByDomain(domain).get();
+ } else {
+ site = siteRepo
+ .findDefaultSite()
+ .orElseThrow(() -> new NotFoundException(
+ "No matching Site and no default Site."));
+ }
+
+ return site;
+ }
+
+ /**
+ * Helper method for retrieving a the theme to use.
+ *
+ * @param site The current site.
+ * @param theme The theme to retrieve.
+ * @param themeVersion The version of the theme to retrieve.
+ *
+ * @return A {@link ThemeInfo} object providing access to to the theme and
+ * its resources.
+ */
+ private ThemeInfo getTheme(
+ final Site site,
+ final String theme,
+ final ThemeVersion themeVersion) {
+ if ("--DEFAULT--".equals(theme)) {
+ return themes
+ .getTheme(site.getDefaultTheme(), themeVersion)
+ .orElseThrow(
+ () -> new WebApplicationException(
+ String.format(
+ "The configured default theme \"%s\" for "
+ + "site \"%s\" is not available.",
+ site.getDefaultTheme(),
+ site.getDomainOfSite()
+ ),
+ Response.Status.INTERNAL_SERVER_ERROR
+ )
+ );
+ } else {
+ return themes
+ .getTheme(theme, themeVersion)
+ .orElseThrow(
+ () -> new WebApplicationException(
+ String.format(
+ "The theme \"%s\" is not available.",
+ theme
+ ),
+ Response.Status.BAD_REQUEST
+ )
+ );
+ }
+ }
+
+ /**
+ * Helper method for parsing the {@code theme} query parameter which can be
+ * used to override the default theme of a site.
+ *
+ * @param uriInfo Information about the current URI.
+ *
+ * @return The value of the {@link theme} query parameter if present, or
+ * {@code --DEFAULT--} if the query parameter is not present.
+ */
+ private String parseThemeParam(final UriInfo uriInfo) {
+ if (uriInfo.getQueryParameters().containsKey("theme")) {
+ return uriInfo.getQueryParameters().getFirst("theme");
+ } else {
+ return "--DEFAULT--";
+ }
+ }
+
+ /**
+ * Helper method for parsing the {@code preview} query parameter. The
+ * {@code preview} query parameter allows it to test the draft version of a
+ * theme.
+ *
+ * @param uriInfo Information about the current URI.
+ *
+ * @return If the value of the parameter is {@code theme} or {@code all}
+ * {@link ThemeVersion#DRAFT} is returned. If the query parameter is
+ * not present or has another value, {@link ThemeVersion#LIVE} is
+ * returned.
+ */
+ private ThemeVersion parsePreviewParam(final UriInfo uriInfo) {
+ if (uriInfo.getQueryParameters().containsKey("preview")) {
+ final List values = uriInfo
+ .getQueryParameters()
+ .get("preview");
+ if (values.contains("theme") || values.contains("all")) {
+ return ThemeVersion.DRAFT;
+ } else {
+ return ThemeVersion.LIVE;
+ }
+ } else {
+ return ThemeVersion.LIVE;
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java
new file mode 100644
index 000000000..cc845360e
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2021 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
+ */
+/**
+ * Integration of the Theming System with Jakarta EE MVC.
+ */
+package org.libreccm.theming.mvc;
diff --git a/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java b/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java
index dea89b408..da877e4ba 100644
--- a/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java
+++ b/ccm-core/src/main/java/org/libreccm/ui/IsAuthenticatedFilter.java
@@ -58,7 +58,7 @@ public class IsAuthenticatedFilter implements ContainerRequestFilter {
Response.temporaryRedirect(
URI.create(
String.format(
- "/%s/ccm/register?return_url=%s",
+ "/%s/@login?returnUrl=%s",
contextPath,
returnUrl
)
diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java
new file mode 100644
index 000000000..10d821a8f
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2020 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.ui.login;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@ApplicationPath("/@login")
+public class LoginApplication extends Application {
+
+ @Override
+ public Set> getClasses() {
+ final Set> classes = new HashSet<>();
+ classes.add(LoginController.class);
+
+ return classes;
+ }
+
+
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java
new file mode 100644
index 000000000..f006369e5
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 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.ui.login;
+
+import com.arsdigita.kernel.KernelConfig;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.subject.Subject;
+import org.libreccm.configuration.ConfigurationManager;
+import org.libreccm.security.ChallengeManager;
+import org.libreccm.security.User;
+import org.libreccm.security.UserRepository;
+import org.libreccm.theming.mvc.ThemesMvc;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Optional;
+
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.mail.MessagingException;
+import javax.mvc.Controller;
+import javax.mvc.Models;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.RedirectionException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@Controller
+@Path("/")
+@RequestScoped
+public class LoginController {
+
+ @Inject
+ private ChallengeManager challengeManager;
+
+ @Inject
+ private ConfigurationManager confManager;
+
+ @Inject
+ private Models models;
+
+ @Inject
+ private HttpServletRequest request;
+
+ @Inject
+ private Subject subject;
+
+ @Inject
+ private ThemesMvc themesMvc;
+
+ @Inject
+ private UserRepository userRepository;
+
+ @GET
+ @Path("/")
+ public String getLoginForm(
+ @Context final UriInfo uriInfo,
+ @QueryParam("returnUrl") @DefaultValue("") final String returnUrl
+ ) {
+ models.put(
+ "emailIsPrimaryIdentifier", isEmailPrimaryIdentifier()
+ );
+ if (models.get("loginFailed") == null) {
+ models.put("loginFailed", false);
+ }
+ models.put("returnUrl", returnUrl);
+ return themesMvc.getMvcTemplate(uriInfo, "login", "loginForm");
+ }
+
+ @POST
+ @Path("/")
+ public Object processLogin(
+ @Context final UriInfo uriInfo,
+ @FormParam("login") final String login,
+ @FormParam("password") final String password,
+ @FormParam("rememberMe") final String rememberMeValue,
+ @FormParam("returnUrl") @DefaultValue("") final String returnUrl
+ ) {
+ final UsernamePasswordToken token = new UsernamePasswordToken(
+ login, password
+ );
+ token.setRememberMe("on".equals(rememberMeValue));
+ try {
+ subject.login(token);
+ } catch (AuthenticationException ex) {
+ models.put("loginFailed", true);
+ return getLoginForm(uriInfo, returnUrl);
+ }
+
+ try {
+ return Response.seeOther(
+ new URI(
+ request.getScheme(),
+ "",
+ request.getServerName(),
+ request.getServerPort(),
+ String.join(request.getContextPath(), returnUrl),
+ "",
+ ""
+ )
+ ).build();
+ } catch (URISyntaxException ex) {
+ throw new WebApplicationException(
+ Response.Status.INTERNAL_SERVER_ERROR
+ );
+ }
+ }
+
+ @GET
+ @Path("/recover-password")
+ public String getRecoverPasswordForm(@Context final UriInfo uriInfo) {
+ return themesMvc.getMvcTemplate(uriInfo, "login", "recoverPassword");
+ }
+
+ @POST
+ @Path("/recover-password")
+ public String recoverPassword(
+ @Context final UriInfo uriInfo,
+ @FormParam("email") final String email
+ ) {
+ final Optional user = userRepository.findByEmailAddress(email);
+ if (user.isPresent()) {
+ try {
+ challengeManager.sendPasswordRecover(user.get());
+ } catch (MessagingException ex) {
+ models.put("failedToSendRecoverMessage", true);
+ return getRecoverPasswordForm(uriInfo);
+ }
+ }
+
+ return themesMvc.getMvcTemplate(uriInfo, "login", "passwordRecovered");
+ }
+
+ private boolean isEmailPrimaryIdentifier() {
+ final KernelConfig kernelConfig = confManager.findConfiguration(
+ KernelConfig.class
+ );
+ return kernelConfig.emailIsPrimaryIdentifier();
+ }
+
+}
diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java
new file mode 100644
index 000000000..5ae2a2795
--- /dev/null
+++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 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.ui.login;
+
+import com.arsdigita.ui.login.LoginConstants;
+
+import org.libreccm.l10n.GlobalizationHelper;
+
+import java.text.MessageFormat;
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.enterprise.context.RequestScoped;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/**
+ *
+ * @author Jens Pelzetter
+ */
+@RequestScoped
+@Named("LoginMessages")
+public class LoginMessages extends AbstractMap{
+
+ /**
+ * Provides access to the locale negoiated by LibreCCM.
+ */
+ @Inject
+ private GlobalizationHelper globalizationHelper;
+
+ /**
+ * The {@link ResourceBundle} to use.
+ */
+ private ResourceBundle messages;
+
+ /**
+ * Loads the resource bundle.
+ */
+ @PostConstruct
+ private void init() {
+ messages = ResourceBundle.getBundle(
+ LoginConstants.LOGIN_UI_BUNDLE,
+ globalizationHelper.getNegotiatedLocale()
+ );
+ }
+
+ /**
+ * Retrieves a message from the resource bundle.
+ *
+ * @param key The key of the message.
+ * @return The translated message or {@code ???message???} if the the key is
+ * not found in the resource bundle (message is replaced with the key).
+ */
+ public String getMessage(final String key) {
+ if (messages.containsKey(key)) {
+ return messages.getString(key);
+ } else {
+ return String.format("???%s???", key);
+ }
+ }
+
+ /**
+ * Retrieves a message with placeholders.
+ *
+ * @param key The key of the message.
+ * @param parameters The parameters for the placeholders.
+ * @return The translated message or {@code ???message???} if the the key is
+ * not found in the resource bundle (message is replaced with the key).
+ */
+ public String getMessage(
+ final String key, final List