Merge pull request 'mvc-theme-integration' (#8) from mvc-theme-integration into master
Reviewed-on: #8
commit
3a33be81bb
|
|
@ -31,6 +31,9 @@ public interface LoginConstants {
|
||||||
public static final String LOGIN_BUNDLE
|
public static final String LOGIN_BUNDLE
|
||||||
= "com.arsdigita.ui.login.LoginResources";
|
= "com.arsdigita.ui.login.LoginResources";
|
||||||
|
|
||||||
|
public static final String LOGIN_UI_BUNDLE
|
||||||
|
= "org.libreccm.ui.LoginBundle";
|
||||||
|
|
||||||
public static final GlobalizedMessage SUBMIT = LoginHelper.getMessage(
|
public static final GlobalizedMessage SUBMIT = LoginHelper.getMessage(
|
||||||
"login.submit");
|
"login.submit");
|
||||||
public static final GlobalizedMessage PRIMARY_EMAIL = LoginHelper
|
public static final GlobalizedMessage PRIMARY_EMAIL = LoginHelper
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
* <ul>
|
||||||
|
* <li>Named Beans are supported</li>
|
||||||
|
* <li>Freemarker template have access to the MvcContext under the name
|
||||||
|
* {@code mvc}, as in Facelet-based templates</li>
|
||||||
|
* <li>The current {@link HttpServletRequest} is made avaiable in Freemarker
|
||||||
|
* templates as {@link request}.</li>
|
||||||
|
* <li>The following utility functions are made available:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code getSetting}: retreives the value of a setting from the theme</li>
|
||||||
|
* <li>{@code localize}: retreives a localized value from the theme</li>
|
||||||
|
* <li>{@code truncateText}: Truncates text to a specific length.</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@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<String, Object> model = new HashMap<>();
|
||||||
|
model.put("mvc", mvc);
|
||||||
|
model.put("request", context.getRequest(HttpServletRequest.class));
|
||||||
|
|
||||||
|
final Optional<TemplateInfo> 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<String, Object> 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<NamedBeanInstance> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,9 @@ class KrazoTemplateLoader implements TemplateLoader {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object findTemplateSource(final String name) throws IOException {
|
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;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
// Freemarker drops "/"
|
// Freemarker drops "/"
|
||||||
|
|
|
||||||
|
|
@ -28,28 +28,33 @@ import org.eclipse.krazo.engine.ViewEngineConfig;
|
||||||
import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer;
|
import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer;
|
||||||
import org.libreccm.theming.Themes;
|
import org.libreccm.theming.Themes;
|
||||||
|
|
||||||
import javax.enterprise.context.ApplicationScoped;
|
import javax.enterprise.inject.Produces;
|
||||||
import javax.enterprise.inject.Specializes;
|
import javax.enterprise.inject.Specializes;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.mvc.Models;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
import javax.ws.rs.Produces;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extends the default configuration for Freemarker of Eclipse Krazo to
|
* Extends the default configuration for Freemarker of Eclipse Krazo to support
|
||||||
* support Freemarker templates in CCM themes.
|
* Freemarker templates in CCM themes.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
*/
|
*/
|
||||||
@ApplicationScoped
|
|
||||||
public class MvcFreemarkerConfigurationProducer
|
public class MvcFreemarkerConfigurationProducer
|
||||||
extends DefaultConfigurationProducer {
|
extends DefaultConfigurationProducer {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Models models;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private ServletContext servletContext;
|
private ServletContext servletContext;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private Themes themes;
|
private Themes themes;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ThemeTemplateUtil themeTemplateUtil;
|
||||||
|
|
||||||
@Produces
|
@Produces
|
||||||
@ViewEngineConfig
|
@ViewEngineConfig
|
||||||
@Specializes
|
@Specializes
|
||||||
|
|
@ -70,7 +75,7 @@ public class MvcFreemarkerConfigurationProducer
|
||||||
new MultiTemplateLoader(
|
new MultiTemplateLoader(
|
||||||
new TemplateLoader[]{
|
new TemplateLoader[]{
|
||||||
new KrazoTemplateLoader(servletContext),
|
new KrazoTemplateLoader(servletContext),
|
||||||
new ThemesTemplateLoader(themes),
|
new ThemesTemplateLoader(themes, themeTemplateUtil),
|
||||||
// For loading Freemarker macro libraries from WEB-INF
|
// For loading Freemarker macro libraries from WEB-INF
|
||||||
// resources
|
// resources
|
||||||
new WebappTemplateLoader(
|
new WebappTemplateLoader(
|
||||||
|
|
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@RequestScoped
|
||||||
|
class ThemeTemplateUtil {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(
|
||||||
|
ThemeTemplateUtil.class);
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Instance<ThemeProvider> 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<TemplateInfo> 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<TemplateInfo> 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> 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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
package org.libreccm.mvc.freemarker;
|
package org.libreccm.mvc.freemarker;
|
||||||
|
|
||||||
import freemarker.cache.TemplateLoader;
|
import freemarker.cache.TemplateLoader;
|
||||||
import org.libreccm.theming.ThemeInfo;
|
|
||||||
import org.libreccm.theming.ThemeVersion;
|
import org.libreccm.theming.ThemeVersion;
|
||||||
import org.libreccm.theming.Themes;
|
import org.libreccm.theming.Themes;
|
||||||
|
|
||||||
|
|
@ -27,7 +26,6 @@ import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,8 +37,13 @@ class ThemesTemplateLoader implements TemplateLoader {
|
||||||
|
|
||||||
private final Themes themes;
|
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.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
|
* of the theme from which the template is loaded. {@code $version} is the
|
||||||
* version of the theme to use. This token is converted to
|
* version of the theme to use. This token is converted to
|
||||||
* {@link ThemeVersion}. Valid values are therefore {@code DRAFT} and
|
* {@link ThemeVersion}. Valid values are therefore {@code DRAFT} and
|
||||||
* {@code LIVE}. The remainder of the path is the path to the file inside the
|
* {@code LIVE}. The remainder of the path is the path to the file inside
|
||||||
* theme.
|
* the theme.
|
||||||
*
|
*
|
||||||
* @param path The path of the file. The path must include the theme and its
|
* @param path The path of the file. The path must include the theme and its
|
||||||
* version.
|
* version.
|
||||||
*
|
*
|
||||||
* @return An {@link InputStream} for the template if the template was found
|
* @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
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Object findTemplateSource(final String path) throws IOException {
|
public Object findTemplateSource(final String path) throws IOException {
|
||||||
if (path.startsWith("@themes") || path.startsWith("/@themes")) {
|
if (themeTemplateUtil.isValidTemplatePath(path)) {
|
||||||
final String[] tokens;
|
final Optional<TemplateInfo> templateInfo = themeTemplateUtil
|
||||||
if (path.startsWith("/")) {
|
.getTemplateInfo(path);
|
||||||
tokens = path.substring(1).split("/");
|
|
||||||
|
if (templateInfo.isPresent()) {
|
||||||
|
final Optional<InputStream> source = themes.getFileFromTheme(
|
||||||
|
templateInfo.get().getThemeInfo(),
|
||||||
|
templateInfo.get().getFilePath()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (source.isPresent()) {
|
||||||
|
return source.get();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} else {
|
} 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> 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<InputStream> source = themes.getFileFromTheme(
|
|
||||||
themeInfo, filePath
|
|
||||||
);
|
|
||||||
if (source.isPresent()) {
|
|
||||||
return source.get();
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
@ -62,16 +63,22 @@ import javax.json.JsonReader;
|
||||||
public class StaticThemeProvider implements ThemeProvider {
|
public class StaticThemeProvider implements ThemeProvider {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(
|
private static final Logger LOGGER = LogManager.getLogger(
|
||||||
StaticThemeProvider.class);
|
StaticThemeProvider.class);
|
||||||
|
|
||||||
private static final String THEMES_DIR = "/themes";
|
private static final String THEMES_DIR = "/themes";
|
||||||
|
|
||||||
private static final String THEMES_PACKAGE = "themes";
|
private static final String THEMES_PACKAGE = "themes";
|
||||||
|
|
||||||
private static final String THEME_MANIFEST_JSON_PATH = THEMES_DIR
|
private static final String THEME_MANIFEST_JSON_PATH = THEMES_DIR
|
||||||
+ "/%s/theme.json";
|
+ "/%s/theme.json";
|
||||||
|
|
||||||
private static final String THEME_MANIFEST_XML_PATH = THEMES_DIR
|
private static final String THEME_MANIFEST_XML_PATH = THEMES_DIR
|
||||||
+ "/%s/theme.xml";
|
+ "/%s/theme.xml";
|
||||||
|
|
||||||
private static final String THEME_MANIFEST_JSON = "theme.json";
|
private static final String THEME_MANIFEST_JSON = "theme.json";
|
||||||
|
|
||||||
private static final String THEME_MANIFEST_XML = "theme.xml";
|
private static final String THEME_MANIFEST_XML = "theme.xml";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
|
@ -472,18 +479,26 @@ public class StaticThemeProvider implements ThemeProvider {
|
||||||
|
|
||||||
final String fileName = path.get(0);
|
final String fileName = path.get(0);
|
||||||
|
|
||||||
final Optional<JsonObject> fileData = currentDirectory
|
final Optional<JsonObject> fileDataResult = currentDirectory
|
||||||
.stream()
|
.stream()
|
||||||
.map(value -> (JsonObject) value)
|
.map(value -> (JsonObject) value)
|
||||||
.filter(value -> filterFileData(value, fileName))
|
.filter(value -> filterFileData(value, fileName))
|
||||||
.findAny();
|
.findAny();
|
||||||
if (path.size() == 1) {
|
if (path.size() == 1) {
|
||||||
return fileData;
|
return fileDataResult;
|
||||||
} else {
|
} else {
|
||||||
|
final JsonObject fileData = fileDataResult
|
||||||
if (fileData.get().getBoolean("isDirectory")) {
|
.orElseThrow(
|
||||||
return findFile(path.subList(1, path.size()),
|
() -> new NoSuchElementException(
|
||||||
fileData.get().getJsonArray("files"));
|
String.format(
|
||||||
|
"File %s not found.", path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (fileData.getBoolean("isDirectory")) {
|
||||||
|
return findFile(
|
||||||
|
path.subList(1, path.size()), fileData.getJsonArray("files")
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ import javax.xml.bind.annotation.XmlRootElement;
|
||||||
import static org.libreccm.theming.ThemeConstants.*;
|
import static org.libreccm.theming.ThemeConstants.*;
|
||||||
|
|
||||||
import java.io.Serializable;
|
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
|
* 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)
|
@XmlElement(name = "default-template", namespace = THEMES_XML_NS)
|
||||||
private String defaultTemplate;
|
private String defaultTemplate;
|
||||||
|
|
||||||
|
@XmlElement(name = "mvc-templates", namespace = THEMES_XML_NS)
|
||||||
|
private Map<String, ThemeTemplate> mvcTemplates;
|
||||||
|
|
||||||
|
@XmlElement(name = "views", namespace = THEMES_XML_NS)
|
||||||
|
private Map<String, Map<String, String>> views;
|
||||||
|
|
||||||
public ThemeManifest() {
|
public ThemeManifest() {
|
||||||
templates = new ArrayList<>();
|
templates = new ArrayList<>();
|
||||||
|
mvcTemplates = new HashMap<>();
|
||||||
|
views = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
|
@ -156,6 +167,62 @@ public class ThemeManifest implements Serializable {
|
||||||
this.defaultTemplate = defaultTemplate;
|
this.defaultTemplate = defaultTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, ThemeTemplate> getMvcTemplates() {
|
||||||
|
return Collections.unmodifiableMap(mvcTemplates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<ThemeTemplate> 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<String, ThemeTemplate> mvcTemplates
|
||||||
|
) {
|
||||||
|
this.mvcTemplates = mvcTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Map<String, String>> getViews() {
|
||||||
|
return Collections.unmodifiableMap(views);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getViewsOfApplication(final String application) {
|
||||||
|
if (views.containsKey(application)) {
|
||||||
|
return views.get(application);
|
||||||
|
} else {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addViewsOfApplication(
|
||||||
|
final String application, final Map<String, String> viewsOfApplication
|
||||||
|
) {
|
||||||
|
views.put(application, viewsOfApplication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addViewToApplication(
|
||||||
|
final String application, final String view, final String templateName
|
||||||
|
) {
|
||||||
|
final Map<String, String> 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<String, Map<String, String>> views) {
|
||||||
|
this.views = new HashMap<>(views);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
int hash = 7;
|
int hash = 7;
|
||||||
|
|
@ -166,6 +233,8 @@ public class ThemeManifest implements Serializable {
|
||||||
hash = 83 * hash + Objects.hashCode(description);
|
hash = 83 * hash + Objects.hashCode(description);
|
||||||
hash = 83 * hash + Objects.hashCode(templates);
|
hash = 83 * hash + Objects.hashCode(templates);
|
||||||
hash = 83 * hash + Objects.hashCode(defaultTemplate);
|
hash = 83 * hash + Objects.hashCode(defaultTemplate);
|
||||||
|
hash = 83 * hash + Objects.hashCode(mvcTemplates);
|
||||||
|
hash = 83 * hash + Objects.hashCode(views);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +271,14 @@ public class ThemeManifest implements Serializable {
|
||||||
if (!Objects.equals(templates, other.getTemplates())) {
|
if (!Objects.equals(templates, other.getTemplates())) {
|
||||||
return false;
|
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) {
|
public boolean canEqual(final Object obj) {
|
||||||
|
|
@ -216,24 +292,30 @@ public class ThemeManifest implements Serializable {
|
||||||
|
|
||||||
public String toString(final String data) {
|
public String toString(final String data) {
|
||||||
|
|
||||||
return String.format("%s{ "
|
return String.format(
|
||||||
+ "name = \"%s\", "
|
"%s{ "
|
||||||
+ "type = \"%s\", "
|
+ "name = \"%s\", "
|
||||||
+ "masterTheme = \"%s\", "
|
+ "type = \"%s\", "
|
||||||
+ "title = \"%s\", "
|
+ "masterTheme = \"%s\", "
|
||||||
+ "description = \"%s\", "
|
+ "title = \"%s\", "
|
||||||
+ "templates = %s, "
|
+ "description = \"%s\", "
|
||||||
+ "defaultTemplate%s"
|
+ "templates = %s, "
|
||||||
+ " }",
|
+ "defaultTemplate, "
|
||||||
super.toString(),
|
+ "mvcTemplates = %s,"
|
||||||
name,
|
+ "views = %s%s"
|
||||||
type,
|
+ " }",
|
||||||
masterTheme,
|
super.toString(),
|
||||||
Objects.toString(title),
|
name,
|
||||||
Objects.toString(description),
|
type,
|
||||||
Objects.toString(templates),
|
masterTheme,
|
||||||
defaultTemplate,
|
Objects.toString(title),
|
||||||
data);
|
Objects.toString(description),
|
||||||
|
Objects.toString(templates),
|
||||||
|
defaultTemplate,
|
||||||
|
Objects.toString(mvcTemplates),
|
||||||
|
Objects.toString(views),
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
*/
|
*/
|
||||||
package org.libreccm.theming.manifest;
|
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.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
@ -60,8 +61,6 @@ public class ThemeManifestUtil implements Serializable {
|
||||||
* @return The parsed manifest file.
|
* @return The parsed manifest file.
|
||||||
*/
|
*/
|
||||||
public ThemeManifest loadManifest(final Path path) {
|
public ThemeManifest loadManifest(final Path path) {
|
||||||
|
|
||||||
// final String pathStr = path.toString().toLowerCase(Locale.ROOT);
|
|
||||||
final BufferedReader reader;
|
final BufferedReader reader;
|
||||||
try {
|
try {
|
||||||
reader = Files.newBufferedReader(path, Charset.forName("UTF-8"));
|
reader = Files.newBufferedReader(path, Charset.forName("UTF-8"));
|
||||||
|
|
@ -70,34 +69,11 @@ public class ThemeManifestUtil implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseManifest(reader, path.toString());
|
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,
|
public ThemeManifest loadManifest(
|
||||||
final String fileName) {
|
final InputStream inputStream, final String fileName
|
||||||
|
) {
|
||||||
final InputStreamReader reader;
|
final InputStreamReader reader;
|
||||||
try {
|
try {
|
||||||
reader = new InputStreamReader(inputStream, "UTF-8");
|
reader = new InputStreamReader(inputStream, "UTF-8");
|
||||||
|
|
@ -106,34 +82,11 @@ public class ThemeManifestUtil implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseManifest(reader, fileName);
|
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,
|
public String serializeManifest(
|
||||||
final String format) {
|
final ThemeManifest manifest, final String format
|
||||||
|
) {
|
||||||
final ObjectMapper mapper;
|
final ObjectMapper mapper;
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
|
|
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@RequestScoped
|
||||||
|
@Path("/")
|
||||||
|
public class ThemeResourceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point for the available {@link ThemeProvider}s.
|
||||||
|
*/
|
||||||
|
@Inject
|
||||||
|
@Any
|
||||||
|
private Instance<ThemeProvider> 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<ThemeProvider> provider = findProvider(themeName);
|
||||||
|
final ThemeVersion themeVersion = ThemeVersion.valueOf(
|
||||||
|
themeVersionParam
|
||||||
|
);
|
||||||
|
|
||||||
|
if (provider.isPresent()) {
|
||||||
|
final Optional<ThemeFileInfo> 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> 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<ThemeProvider> findProvider(final String forTheme) {
|
||||||
|
final List<ThemeProvider> providersList = new ArrayList<>();
|
||||||
|
providers
|
||||||
|
.forEach(provider -> providersList.add(provider));
|
||||||
|
|
||||||
|
return providersList
|
||||||
|
.stream()
|
||||||
|
.filter(current -> current.providesTheme(forTheme,
|
||||||
|
ThemeVersion.DRAFT))
|
||||||
|
.findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@ApplicationPath("/@themes")
|
||||||
|
public class ThemeResources extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Class<?>> getClasses() {
|
||||||
|
final Set<Class<?>> classes = new HashSet<>();
|
||||||
|
classes.add(ThemeResourceProvider.class);
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@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<String, String> views = manifest.getViewsOfApplication(
|
||||||
|
application
|
||||||
|
);
|
||||||
|
final String viewTemplateName;
|
||||||
|
if (views.containsKey(view)) {
|
||||||
|
viewTemplateName = views.get(view);
|
||||||
|
} else {
|
||||||
|
final Map<String, String> 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<String> values = uriInfo
|
||||||
|
.getQueryParameters()
|
||||||
|
.get("preview");
|
||||||
|
if (values.contains("theme") || values.contains("all")) {
|
||||||
|
return ThemeVersion.DRAFT;
|
||||||
|
} else {
|
||||||
|
return ThemeVersion.LIVE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ThemeVersion.LIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -58,7 +58,7 @@ public class IsAuthenticatedFilter implements ContainerRequestFilter {
|
||||||
Response.temporaryRedirect(
|
Response.temporaryRedirect(
|
||||||
URI.create(
|
URI.create(
|
||||||
String.format(
|
String.format(
|
||||||
"/%s/ccm/register?return_url=%s",
|
"/%s/@login?returnUrl=%s",
|
||||||
contextPath,
|
contextPath,
|
||||||
returnUrl
|
returnUrl
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@ApplicationPath("/@login")
|
||||||
|
public class LoginApplication extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Class<?>> getClasses() {
|
||||||
|
final Set<Class<?>> classes = new HashSet<>();
|
||||||
|
classes.add(LoginController.class);
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@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> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@RequestScoped
|
||||||
|
@Named("LoginMessages")
|
||||||
|
public class LoginMessages extends AbstractMap<String, String>{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Object> parameters
|
||||||
|
) {
|
||||||
|
return getMessage(key, parameters.toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The translated message or {@code ???message???} if the the key is
|
||||||
|
* not found in the resource bundle (message is replaced with the key).
|
||||||
|
*
|
||||||
|
@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 Object[] parameters
|
||||||
|
) {
|
||||||
|
if (messages.containsKey(key)) {
|
||||||
|
return MessageFormat.format(messages.getString(key), parameters);
|
||||||
|
} else {
|
||||||
|
return String.format("???%s???", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String get(final Object key) {
|
||||||
|
return get((String) key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String get(final String key) {
|
||||||
|
return getMessage(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Map.Entry<String, String>> entrySet() {
|
||||||
|
return messages
|
||||||
|
.keySet()
|
||||||
|
.stream()
|
||||||
|
.collect(
|
||||||
|
Collectors.toMap(key -> key, key -> messages.getString(key))
|
||||||
|
)
|
||||||
|
.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* 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.ui.login;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.ws.rs.ApplicationPath;
|
||||||
|
import javax.ws.rs.core.Application;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@ApplicationPath("/@logout")
|
||||||
|
public class LogoutApplication extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Class<?>> getClasses() {
|
||||||
|
final Set<Class<?>> classes = new HashSet<>();
|
||||||
|
classes.add(LogoutController.class);
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* 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.ui.login;
|
||||||
|
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
|
import org.libreccm.theming.mvc.ThemesMvc;
|
||||||
|
|
||||||
|
import javax.enterprise.context.RequestScoped;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.mvc.Controller;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@Controller
|
||||||
|
@Path("/")
|
||||||
|
@RequestScoped
|
||||||
|
public class LogoutController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ThemesMvc themesMvc;
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/")
|
||||||
|
public String logout(@Context final UriInfo uriInfo) {
|
||||||
|
subject.logout();
|
||||||
|
|
||||||
|
return themesMvc.getMvcTemplate(uriInfo, "logout", "loggedout");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,11 @@
|
||||||
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
|
http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
|
||||||
bean-discovery-mode="all">
|
bean-discovery-mode="all">
|
||||||
|
|
||||||
|
<!-- <alternatives>
|
||||||
|
<class>org.libreccm.mvc.freemarker.MvcFreemarkerConfigurationProducer</class>
|
||||||
|
<class>org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer</class>
|
||||||
|
</alternatives>-->
|
||||||
|
|
||||||
<interceptors>
|
<interceptors>
|
||||||
<class>org.libreccm.security.AuthorizationInterceptor</class>
|
<class>org.libreccm.security.AuthorizationInterceptor</class>
|
||||||
</interceptors>
|
</interceptors>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
login.submit=Login
|
||||||
|
login.primaryEmail=Email Address
|
||||||
|
login.screenname.label=User name
|
||||||
|
login.password.label=Password
|
||||||
|
login.screenname.help=Your user name
|
||||||
|
login.password.help=Your password
|
||||||
|
login.errors.failed=Login failed. Wrong username or password.
|
||||||
|
login.errors.failedToSendRecoverMessage=Failed to send password recover message.
|
||||||
|
login.title=Login
|
||||||
|
login.recover_password.title=Recover password
|
||||||
|
login.recover_password.submit=Recover password
|
||||||
|
login.password_recovered.title=Recover mail send
|
||||||
|
login.password_recovered.message=An email with instructions how to reset your password has been sent.
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
login.submit=Anmelden
|
||||||
|
login.primaryEmail=E-Mail-Addresse
|
||||||
|
login.screenname.label=Benutzername
|
||||||
|
login.password.label=Passwort
|
||||||
|
login.screenname.help=Ihr Benutzername
|
||||||
|
login.password.help=Ihr Passwort
|
||||||
|
login.errors.failed=Anmeldung fehlgeschlagen. Falscher Benutzername oder falsches Passwort.
|
||||||
|
login.errors.failedToSendRecoverMessage=Senden der Wiederherstellungsmail fehlgeschlagen.
|
||||||
|
login.title=Login
|
||||||
|
login.recover_password.title=Passwort zur\u00fccksetzen
|
||||||
|
login.recover_password.submit=Passwort zur\u00fccksetzen
|
||||||
|
login.password_recovered.title=Mail gesendet
|
||||||
|
login.password_recovered.message=Eine E-Mail mit Anweisungen zum Zur\u00fccksetzen Ihres Passworts wurde an Ihre E-Mail-Adresse geschickt.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -0,0 +1,34 @@
|
||||||
|
<#import "../main.html.ftl" as main>
|
||||||
|
|
||||||
|
<@main.ccm_main>
|
||||||
|
<h1>${LoginMessages['login.title']}</h1>
|
||||||
|
<#if (loginFailed)>
|
||||||
|
<div class="alert-error">
|
||||||
|
${LoginMessages['login.errors.failed']}
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
<form action="${mvc.uri('LoginController#processLogin')}"
|
||||||
|
class="login"
|
||||||
|
method="post">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="login">${LoginMessages['login.screenname.label']}</label>
|
||||||
|
<input id="login" name="login" required="true" type="text" />
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="password">
|
||||||
|
${LoginMessages['login.password.label']}
|
||||||
|
</label>
|
||||||
|
<input id="password"
|
||||||
|
name="password"
|
||||||
|
required="true"
|
||||||
|
type="password" />
|
||||||
|
</div>
|
||||||
|
<input type="hidden"
|
||||||
|
name="returnUrl"
|
||||||
|
value="${returnUrl}" />
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
${LoginMessages['login.submit']}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</@main.ccm_main>
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Category page</title>
|
||||||
|
<link rel="stylesheet" href="${themeUrl}/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>${LoginMessages['login.title']}</h1>
|
||||||
|
<#if (loginFailed)>
|
||||||
|
<div class="alert-error">
|
||||||
|
${LoginMessages['login.errors.failed']}
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
<form action="${mvc.uri('LoginController#processLogin')}"
|
||||||
|
method="post">
|
||||||
|
<label for="login">${LoginMessages['login.screenname.label']}</label>
|
||||||
|
<input id="login" name="login" required="true" type="text" />
|
||||||
|
|
||||||
|
<label for="password">
|
||||||
|
${LoginMessages['login.password.label']}
|
||||||
|
</label>
|
||||||
|
<input id="password"
|
||||||
|
name="password"
|
||||||
|
required="true"
|
||||||
|
type="password" />
|
||||||
|
|
||||||
|
<input type="hidden"
|
||||||
|
name="returnUrl"
|
||||||
|
value="${returnUrl}" />
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
${LoginMessages['login.submit']}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
<#include "../footer.html.ftl">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Category page</title>
|
||||||
|
<link rel="stylesheet" href="${getContextPath()}/theming/ccm/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>${LoginMessages['login.password_recovered.title']}</h1>
|
||||||
|
<p>${LoginMessages['login.password_recovered.message']}</p>
|
||||||
|
</main>
|
||||||
|
<#include "footer.html.ftl">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Category page</title>
|
||||||
|
<link rel="stylesheet" href="${getContextPath()}/theming/ccm/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>${LoginMessages['login.recover_password.title']}</h1>
|
||||||
|
<# if (failedToSendRecoverMessage)>
|
||||||
|
<div class="alert-error">
|
||||||
|
${LoginMessages['login.errors.failedToSendRecoverMessage']}
|
||||||
|
</div>
|
||||||
|
</#if>
|
||||||
|
<form action="${mvc.url('LoginController#recoverPassword')}"
|
||||||
|
method="post">
|
||||||
|
<label for="email">${LoginMessages['login.email.label']}</label>
|
||||||
|
<input id="email" name="email" required="true" type="text" />
|
||||||
|
|
||||||
|
<button type="submit">
|
||||||
|
${LoginMessages['login.recover_password.submit']}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
<#include "footer.html.ftl">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Category page</title>
|
||||||
|
<link rel="stylesheet" href="${themeUrl}/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Logout successful</h1>
|
||||||
|
<p>Logout successful</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<#macro ccm_main scripts=[]>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Category page</title>
|
||||||
|
<link rel="stylesheet" href="${themeUrl}/style.css" />
|
||||||
|
<#list scripts as script>
|
||||||
|
<script src="${themeUrl}/${script}" />
|
||||||
|
</#list>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="https://www.libreccm.org">
|
||||||
|
<img alt="LibreCCM Logo"
|
||||||
|
src="${themeUrl}/images/libreccm.png" />
|
||||||
|
</a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<#nested>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>LibreCCM basic theme. The customize create your own theme.</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</#macro>
|
||||||
|
|
@ -5,106 +5,69 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.news {
|
header {
|
||||||
|
padding: 1em 2em;
|
||||||
background-color: #000;
|
|
||||||
|
|
||||||
margin: 0 auto 3em auto;
|
|
||||||
|
|
||||||
width: 100vw;
|
|
||||||
|
|
||||||
padding: 3em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.news li {
|
header img {
|
||||||
|
display: block;
|
||||||
display: flex;
|
|
||||||
|
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|
||||||
max-width: 50em;
|
width: 20vw;
|
||||||
|
max-width: 1020px;
|
||||||
|
max-height: 566px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.news li img {
|
main {
|
||||||
max-height: 20em;
|
max-width: 55em;
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
padding: 2em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.news li span {
|
main h1 {
|
||||||
|
text-align: center;
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
font-size: 2rem;
|
|
||||||
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main div.boxes {
|
main form.login {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
margin: 3em auto 3em auto;
|
margin-top: 1em;
|
||||||
|
|
||||||
max-width: 80em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main div.boxes div {
|
main form.login .form-row {
|
||||||
position: relative;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
flex: 1;
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
margin: 0 3em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main div.boxes div p img {
|
main form.login button[type=submit] {
|
||||||
width: 100%;
|
display: block;
|
||||||
}
|
|
||||||
|
|
||||||
main div.boxes div a {
|
margin-top: 0.75em;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
padding: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
background-color: #000;
|
background-color: #71ac52;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|
||||||
|
padding: 3em 1em;
|
||||||
|
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer ul {
|
footer p {
|
||||||
|
text-align: center;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "images",
|
||||||
|
"isDirectory": true,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "libreccm.png",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "login",
|
||||||
|
"isDirectory": true,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "login-form.html.ftl",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "login-password-recovered.html.ftl",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
}, {
|
||||||
|
"name": "login-recover-password.html.ftl",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "texts",
|
||||||
|
"isDirectory": true,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "labels.properties",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "category-page.html.ftl",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "footer.html.ftl",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "settings.properties",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "style.css",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "theme-bundle.properties",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "text/plain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "theme-index.json",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "theme.json",
|
||||||
|
"isDirectory": false,
|
||||||
|
"mimeType": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -2,5 +2,142 @@
|
||||||
"name": "ccm-freemarker",
|
"name": "ccm-freemarker",
|
||||||
"type": "freemarker",
|
"type": "freemarker",
|
||||||
|
|
||||||
"default-template": "category-page.html.ftl"
|
"default-template": "category-page.html.ftl",
|
||||||
|
|
||||||
|
"mvc-templates": {
|
||||||
|
"category-page": {
|
||||||
|
"description": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Category Page Template"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Category Page",
|
||||||
|
"path": "categoryPage.html.ftl",
|
||||||
|
"title": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Category Page"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login-form": {
|
||||||
|
"description": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Login Form"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Login Form",
|
||||||
|
"path": "login/login-form.html.ftl",
|
||||||
|
"title": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Login Form"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login-recover-password": {
|
||||||
|
"description": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Recover lost passwords"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "login-recover-password",
|
||||||
|
"path": "login/login-recover-password.html.ftl",
|
||||||
|
"title": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Recover password"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login-password-recovered": {
|
||||||
|
"description": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Password recovered"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "login-password-recovered",
|
||||||
|
"path": "login/login-password-recovered.html.ftl",
|
||||||
|
"title": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Password recovered"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loggedout": {
|
||||||
|
"description": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Logout successful"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "loggedout",
|
||||||
|
"path": "logout/loggedout.html.ftl",
|
||||||
|
"title": {
|
||||||
|
"values": {
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"value": "Logout succesful"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"default": {
|
||||||
|
"default": "category-page"
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"loginForm": "login-form",
|
||||||
|
"passwordRecovered": "login-password-recovered",
|
||||||
|
"recoverPassword": "login-recover-password"
|
||||||
|
},
|
||||||
|
"logout": {
|
||||||
|
"loggedout": "loggedout"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
public class ThemeResourceProviderTest {
|
||||||
|
|
||||||
|
public ThemeResourceProviderTest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setUpClass() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDownClass() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSomeMethod() {
|
||||||
|
// TODO review the generated test code and remove the default call to fail.
|
||||||
|
fail("The test case is a prototype.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue