Integration of EE MVC and CCM themes
parent
5c2c4e583f
commit
5f155a754f
|
|
@ -19,16 +19,27 @@
|
||||||
package org.libreccm.mvc.freemarker;
|
package org.libreccm.mvc.freemarker;
|
||||||
|
|
||||||
import freemarker.template.Configuration;
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.SimpleNumber;
|
||||||
import freemarker.template.Template;
|
import freemarker.template.Template;
|
||||||
import freemarker.template.TemplateException;
|
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.ViewEngineBase;
|
||||||
import org.eclipse.krazo.engine.ViewEngineConfig;
|
import org.eclipse.krazo.engine.ViewEngineConfig;
|
||||||
|
import org.libreccm.theming.ThemeInfo;
|
||||||
|
import org.libreccm.theming.ThemeProvider;
|
||||||
|
import org.libreccm.theming.freemarker.FreemarkerThemeProcessor;
|
||||||
|
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.IOException;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -64,6 +75,18 @@ public class FreemarkerViewEngine extends ViewEngineBase {
|
||||||
@Inject
|
@Inject
|
||||||
private MvcContext mvc;
|
private MvcContext mvc;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private L10NUtils l10nUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SettingsUtils settingsUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TextUtils textUtils;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ThemeTemplateUtil themeTemplateUtil;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supports(String view) {
|
public boolean supports(String view) {
|
||||||
return view.endsWith(".ftl");
|
return view.endsWith(".ftl");
|
||||||
|
|
@ -86,6 +109,23 @@ public class FreemarkerViewEngine extends ViewEngineBase {
|
||||||
model.put("mvc", mvc);
|
model.put("mvc", mvc);
|
||||||
model.put("request", context.getRequest(HttpServletRequest.class));
|
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
|
final Map<String, Object> namedBeans = beanManager
|
||||||
.getBeans(Object.class)
|
.getBeans(Object.class)
|
||||||
.stream()
|
.stream()
|
||||||
|
|
@ -147,4 +187,112 @@ public class FreemarkerViewEngine extends ViewEngineBase {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class GetSettingMethod implements TemplateMethodModelEx {
|
||||||
|
|
||||||
|
private final ThemeInfo fromTheme;
|
||||||
|
|
||||||
|
private final ThemeProvider themeProvider;
|
||||||
|
|
||||||
|
public GetSettingMethod(final ThemeInfo fromTheme,
|
||||||
|
final ThemeProvider themeProvider) {
|
||||||
|
this.fromTheme = fromTheme;
|
||||||
|
this.themeProvider = themeProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object exec(final List arguments) throws TemplateModelException {
|
||||||
|
|
||||||
|
switch (arguments.size()) {
|
||||||
|
case 2: {
|
||||||
|
final String filePath = ((TemplateScalarModel) arguments
|
||||||
|
.get(0))
|
||||||
|
.getAsString();
|
||||||
|
final String settingName = ((TemplateScalarModel) arguments
|
||||||
|
.get(0))
|
||||||
|
.getAsString();
|
||||||
|
|
||||||
|
return settingsUtils.getSetting(fromTheme,
|
||||||
|
themeProvider,
|
||||||
|
filePath,
|
||||||
|
settingName);
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
final String filePath
|
||||||
|
= ((TemplateScalarModel) arguments.get(0))
|
||||||
|
.getAsString();
|
||||||
|
final String settingName
|
||||||
|
= ((TemplateScalarModel) arguments.get(1))
|
||||||
|
.getAsString();
|
||||||
|
final String defaultValue
|
||||||
|
= ((TemplateScalarModel) arguments.get(2))
|
||||||
|
.getAsString();
|
||||||
|
|
||||||
|
return settingsUtils.getSetting(fromTheme,
|
||||||
|
themeProvider,
|
||||||
|
filePath,
|
||||||
|
settingName,
|
||||||
|
defaultValue);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new TemplateModelException(
|
||||||
|
"Illegal number of arguments.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LocalizeMethod implements TemplateMethodModelEx {
|
||||||
|
|
||||||
|
private final ThemeInfo fromTheme;
|
||||||
|
|
||||||
|
private final ThemeProvider themeProvider;
|
||||||
|
|
||||||
|
public LocalizeMethod(final ThemeInfo fromTheme,
|
||||||
|
final ThemeProvider themeProvider) {
|
||||||
|
this.fromTheme = fromTheme;
|
||||||
|
this.themeProvider = themeProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object exec(final List arguments) throws TemplateModelException {
|
||||||
|
|
||||||
|
if (arguments.isEmpty()) {
|
||||||
|
throw new TemplateModelException("No string to localize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final String bundle;
|
||||||
|
if (arguments.size() > 1) {
|
||||||
|
bundle = ((TemplateScalarModel) arguments.get(1)).getAsString();
|
||||||
|
} else {
|
||||||
|
bundle = "theme-bundle";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String key = ((TemplateScalarModel) arguments.get(0))
|
||||||
|
.getAsString();
|
||||||
|
|
||||||
|
return l10nUtils.getText(fromTheme, themeProvider, bundle, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TruncateTextMethod implements TemplateMethodModelEx {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object exec(final List arguments) throws TemplateModelException {
|
||||||
|
|
||||||
|
if (arguments.size() == 2) {
|
||||||
|
final String text = ((TemplateScalarModel) arguments.get(0))
|
||||||
|
.getAsString();
|
||||||
|
final int length = ((SimpleNumber) arguments.get(1))
|
||||||
|
.getAsNumber()
|
||||||
|
.intValue();
|
||||||
|
|
||||||
|
return textUtils.truncateText(text, length);
|
||||||
|
} else {
|
||||||
|
throw new TemplateModelException("Illegal number of arguments.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.Template;
|
|
||||||
import freemarker.template.TemplateException;
|
|
||||||
import org.eclipse.krazo.engine.ViewEngineBase;
|
|
||||||
import org.eclipse.krazo.engine.ViewEngineConfig;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.annotation.Priority;
|
|
||||||
import javax.enterprise.context.ApplicationScoped;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.mvc.engine.ViewEngine;
|
|
||||||
import javax.mvc.engine.ViewEngineContext;
|
|
||||||
import javax.mvc.engine.ViewEngineException;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
|
||||||
*/
|
|
||||||
@ApplicationScoped
|
|
||||||
@Priority(ViewEngine.PRIORITY_APPLICATION)
|
|
||||||
public class FreemarkerViewEngine extends ViewEngineBase {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@ViewEngineConfig
|
|
||||||
private Configuration configuration;
|
|
||||||
|
|
||||||
@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<>(
|
|
||||||
context.getModels().asMap()
|
|
||||||
);
|
|
||||||
model.put("request", context.getRequest(HttpServletRequest.class));
|
|
||||||
|
|
||||||
template.process(model, writer);
|
|
||||||
} catch (TemplateException | IOException e) {
|
|
||||||
throw new ViewEngineException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -31,31 +31,32 @@ import org.libreccm.theming.Themes;
|
||||||
import javax.enterprise.inject.Produces;
|
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;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
|
||||||
//@Alternative
|
|
||||||
//@Specializes
|
|
||||||
//@Priority(3000)
|
|
||||||
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
|
||||||
// @Alternative
|
|
||||||
@Specializes
|
@Specializes
|
||||||
@Override
|
@Override
|
||||||
public Configuration getConfiguration() {
|
public Configuration getConfiguration() {
|
||||||
|
|
@ -74,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,46 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
class TemplateInfo {
|
||||||
|
|
||||||
|
private final ThemeInfo themeInfo;
|
||||||
|
|
||||||
|
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,128 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
public boolean isValidTemplatePath(final String templatePath) {
|
||||||
|
return templatePath.startsWith("@themes")
|
||||||
|
|| templatePath.startsWith("/@themes");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -39,8 +39,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,75 +58,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")
|
if (themeTemplateUtil.isValidTemplatePath(path)) {
|
||||||
|| path.startsWith("/@themes")
|
final Optional<TemplateInfo> templateInfo = themeTemplateUtil
|
||||||
|| path.startsWith("WEB-INF/views/@themes")) {
|
.getTemplateInfo(path);
|
||||||
final String[] tokens;
|
|
||||||
if (path.startsWith("/")) {
|
if (templateInfo.isPresent()) {
|
||||||
tokens = path.substring(1).split("/");
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* 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 org.libreccm.theming.manager.Themes;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
|
||||||
|
*/
|
||||||
|
@RequestScoped
|
||||||
|
@Path("/")
|
||||||
|
public class ThemeResourceProvider {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Any
|
||||||
|
private Instance<ThemeProvider> providers;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Themes themes;
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -33,6 +33,8 @@ import java.util.Optional;
|
||||||
|
|
||||||
import javax.enterprise.context.RequestScoped;
|
import javax.enterprise.context.RequestScoped;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.mvc.Models;
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
@ -45,6 +47,12 @@ import javax.ws.rs.core.UriInfo;
|
||||||
@RequestScoped
|
@RequestScoped
|
||||||
public class ThemesMvc {
|
public class ThemesMvc {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private Models models;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private ServletContext servletContext;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private SiteRepository siteRepo;
|
private SiteRepository siteRepo;
|
||||||
|
|
||||||
|
|
@ -77,7 +85,7 @@ public class ThemesMvc {
|
||||||
);
|
);
|
||||||
final ThemeTemplate themeTemplate;
|
final ThemeTemplate themeTemplate;
|
||||||
if (applicationTemplates.containsKey(application)) {
|
if (applicationTemplates.containsKey(application)) {
|
||||||
themeTemplate = applicationTemplates.get(application);
|
themeTemplate = applicationTemplates.get(application);
|
||||||
} else {
|
} else {
|
||||||
themeTemplate = Optional.ofNullable(
|
themeTemplate = Optional.ofNullable(
|
||||||
applicationTemplates.get("@default")
|
applicationTemplates.get("@default")
|
||||||
|
|
@ -94,8 +102,21 @@ public class ThemesMvc {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
return String.format(
|
||||||
"@themes/%s/%s/%s",
|
"/@themes/%s/%s/%s",
|
||||||
themeInfo.getName(),
|
themeInfo.getName(),
|
||||||
Objects.toString(themeVersion),
|
Objects.toString(themeVersion),
|
||||||
themeTemplate.getPath()
|
themeTemplate.getPath()
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ public class LoginController {
|
||||||
models.put(
|
models.put(
|
||||||
"emailIsPrimaryIdentifier", isEmailPrimaryIdentifier()
|
"emailIsPrimaryIdentifier", isEmailPrimaryIdentifier()
|
||||||
);
|
);
|
||||||
|
models.put("loginFailed", false);
|
||||||
models.put("returnUrl", redirectUrl);
|
models.put("returnUrl", redirectUrl);
|
||||||
return themesMvc.getMvcTemplate(uriInfo, "login-form");
|
return themesMvc.getMvcTemplate(uriInfo, "login-form");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Category page</title>
|
<title>Category page</title>
|
||||||
<link rel="stylesheet" href="${getContextPath()}/theming/ccm/style.css" />
|
<link rel="stylesheet" href="${themeUrl}/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<pre>
|
||||||
|
${themeUrl}/style.css
|
||||||
|
</pre>
|
||||||
<main>
|
<main>
|
||||||
<h1>${LoginMessages['login.title']}</h1>
|
<h1>${LoginMessages['login.title']}</h1>
|
||||||
<# if (loginFailed)>
|
<#if (loginFailed)>
|
||||||
<div class="alert-error">
|
<div class="alert-error">
|
||||||
${LoginMessages['login.errors.failed']}
|
${LoginMessages['login.errors.failed']}
|
||||||
</div>
|
</div>
|
||||||
</#if>
|
</#if>
|
||||||
<form action="${mvc.url('LoginController#processLogin')}"
|
<pre>${mvc.uri('LoginController#processLogin')}</pre>
|
||||||
|
<form action="${mvc.uri('LoginController#processLogin')}"
|
||||||
method="post">
|
method="post">
|
||||||
<label for="login">${LoginMessages['login.screenname.label']}</label>
|
<label for="login">${LoginMessages['login.screenname.label']}</label>
|
||||||
<input id="login" name="login" required="true" type="text" />
|
<input id="login" name="login" required="true" type="text" />
|
||||||
|
|
@ -30,6 +34,6 @@
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
<#include "footer.html.ftl">
|
<#include "../footer.html.ftl">
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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