From 9bbab62c8acf3833fda245e36bfa59a19b75869d Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Sat, 19 Dec 2020 19:10:12 +0100 Subject: [PATCH 01/12] Integration of Themes with MVC --- .../theming/manifest/ThemeManifest.java | 113 +++++++++-- .../theming/manifest/ThemeManifestUtil.java | 63 +----- .../org/libreccm/theming/mvc/ThemesMvc.java | 179 ++++++++++++++++++ 3 files changed, 281 insertions(+), 74 deletions(-) create mode 100644 ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java index 76133ed7e..e70d61884 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java +++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java @@ -34,6 +34,9 @@ import javax.xml.bind.annotation.XmlRootElement; import static org.libreccm.theming.ThemeConstants.*; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; /** * Each theme contains a Manifest (either in XML or JSON format) which provides @@ -88,8 +91,13 @@ public class ThemeManifest implements Serializable { @XmlElement(name = "default-template", namespace = THEMES_XML_NS) private String defaultTemplate; + @XmlElementWrapper(name = "mvcTemplates", namespace = THEMES_XML_NS) + @XmlElement(name = "template", namespace = THEME_MANIFEST_XML) + private Map> mvcTemplates; + public ThemeManifest() { templates = new ArrayList<>(); + mvcTemplates = new HashMap<>(); } public String getName() { @@ -156,6 +164,65 @@ public class ThemeManifest implements Serializable { this.defaultTemplate = defaultTemplate; } + public Map> getMvcTemplates() { + return Collections.unmodifiableMap(mvcTemplates); + } + + public Optional> getMvcTemplatesOfCategory( + final String category + ) { + return Optional.ofNullable(mvcTemplates.get(category)); + } + + public void addMvcTemplatesCategory(final String category) { + mvcTemplates.put(category, new HashMap<>()); + } + + public void addMvcTemplatesCategory( + final String category, final Map templates + ) { + mvcTemplates.put(category, templates); + } + + public Optional getMvcTemplate( + final String category, final String objectType + ) { + final Optional> templatesInCat + = getMvcTemplatesOfCategory(category); + + if (templatesInCat.isPresent()) { + return Optional.ofNullable(templatesInCat.get().get(objectType)); + } else { + return Optional.empty(); + } + } + + public void addMvcTemplate( + final String category, + final String objectType, + final ThemeTemplate template + ) { + if (!mvcTemplates.containsKey(category)) { + addMvcTemplatesCategory(category); + } + + mvcTemplates + .get(category) + .put( + objectType, + Objects.requireNonNull( + template, + "Template can't be null." + ) + ); + } + + protected void setMvcTemplates( + final Map> mvcTemplates + ) { + this.mvcTemplates = mvcTemplates; + } + @Override public int hashCode() { int hash = 7; @@ -166,6 +233,7 @@ public class ThemeManifest implements Serializable { hash = 83 * hash + Objects.hashCode(description); hash = 83 * hash + Objects.hashCode(templates); hash = 83 * hash + Objects.hashCode(defaultTemplate); + hash = 83 * hash + Objects.hashCode(mvcTemplates); return hash; } @@ -202,7 +270,10 @@ public class ThemeManifest implements Serializable { if (!Objects.equals(templates, other.getTemplates())) { return false; } - return Objects.equals(defaultTemplate, other.getDefaultTemplate()); + if (!Objects.equals(defaultTemplate, other.getDefaultTemplate())) { + return false; + } + return mvcTemplates.equals(other.getMvcTemplates()); } public boolean canEqual(final Object obj) { @@ -216,24 +287,28 @@ public class ThemeManifest implements Serializable { public String toString(final String data) { - return String.format("%s{ " - + "name = \"%s\", " - + "type = \"%s\", " - + "masterTheme = \"%s\", " - + "title = \"%s\", " - + "description = \"%s\", " - + "templates = %s, " - + "defaultTemplate%s" - + " }", - super.toString(), - name, - type, - masterTheme, - Objects.toString(title), - Objects.toString(description), - Objects.toString(templates), - defaultTemplate, - data); + return String.format( + "%s{ " + + "name = \"%s\", " + + "type = \"%s\", " + + "masterTheme = \"%s\", " + + "title = \"%s\", " + + "description = \"%s\", " + + "templates = %s, " + + "defaultTemplate, " + + "mvcTemplates = %s%s" + + " }", + super.toString(), + name, + type, + masterTheme, + Objects.toString(title), + Objects.toString(description), + Objects.toString(templates), + defaultTemplate, + Objects.toString(mvcTemplates), + data + ); } diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java index 9a3ef5c2a..aada91252 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java +++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifestUtil.java @@ -18,7 +18,8 @@ */ package org.libreccm.theming.manifest; -import static org.libreccm.theming.ThemeConstants.*; +import static org.libreccm.theming.ThemeConstants.THEME_MANIFEST_JSON; +import static org.libreccm.theming.ThemeConstants.THEME_MANIFEST_XML; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -60,8 +61,6 @@ public class ThemeManifestUtil implements Serializable { * @return The parsed manifest file. */ public ThemeManifest loadManifest(final Path path) { - -// final String pathStr = path.toString().toLowerCase(Locale.ROOT); final BufferedReader reader; try { reader = Files.newBufferedReader(path, Charset.forName("UTF-8")); @@ -70,34 +69,11 @@ public class ThemeManifestUtil implements Serializable { } return parseManifest(reader, path.toString()); - -// final ObjectMapper mapper; -// if (pathStr.endsWith(THEME_MANIFEST_JSON)) { -// mapper = new ObjectMapper(); -// } else if (pathStr.endsWith(THEME_MANIFEST_XML)) { -// final JacksonXmlModule xmlModule = new JacksonXmlModule(); -// mapper = new XmlMapper(xmlModule); -// } else { -// throw new IllegalArgumentException(String -// .format("The provided path \"%s\" does not point to a theme " -// + "manifest file.", -// path.toString())); -// } -// -// mapper.registerModule(new JaxbAnnotationModule()); -// -// final ThemeManifest manifest; -// try { -// manifest = mapper.readValue(reader, ThemeManifest.class); -// } catch (IOException ex) { -// throw new UnexpectedErrorException(ex); -// } -// return manifest; } - public ThemeManifest loadManifest(final InputStream inputStream, - final String fileName) { - + public ThemeManifest loadManifest( + final InputStream inputStream, final String fileName + ) { final InputStreamReader reader; try { reader = new InputStreamReader(inputStream, "UTF-8"); @@ -106,34 +82,11 @@ public class ThemeManifestUtil implements Serializable { } return parseManifest(reader, fileName); - -// final ObjectMapper mapper; -// if (fileName.endsWith(THEME_MANIFEST_JSON)) { -// mapper = new ObjectMapper(); -// } else if (fileName.endsWith(THEME_MANIFEST_XML)) { -// final JacksonXmlModule xmlModule = new JacksonXmlModule(); -// mapper = new XmlMapper(xmlModule); -// } else { -// throw new IllegalArgumentException(String -// .format("The provided path \"%s\" does not point to a theme " -// + "manifest file.", -// fileName)); -// } -// -// mapper.registerModule(new JaxbAnnotationModule()); -// -// final ThemeManifest manifest; -// try { -// manifest = mapper.readValue(reader, ThemeManifest.class); -// } catch (IOException ex) { -// throw new UnexpectedErrorException(ex); -// } -// return manifest; } - public String serializeManifest(final ThemeManifest manifest, - final String format) { - + public String serializeManifest( + final ThemeManifest manifest, final String format + ) { final ObjectMapper mapper; switch (format) { diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java new file mode 100644 index 000000000..862da79bd --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java @@ -0,0 +1,179 @@ +/* + * 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 java.util.Optional; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +public class ThemesMvc { + + @Inject + private SiteRepository siteRepo; + + @Inject + private Themes themes; + + public String getMvcTemplate( + final UriInfo uriInfo, + final String application + ) { + 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 applicationTemplates = manifest + .getMvcTemplatesOfCategory("applications") + .orElseThrow( + () -> new WebApplicationException( + String.format( + "Manifest of theme %s has no application templates.", + themeInfo.getName() + ), + Response.Status.INTERNAL_SERVER_ERROR + ) + ); + final ThemeTemplate themeTemplate; + if (applicationTemplates.containsKey(application)) { + themeTemplate = applicationTemplates.get(application); + } else { + themeTemplate = Optional.ofNullable( + applicationTemplates.get("@default") + ).orElseThrow( + () -> new WebApplicationException( + String.format( + "Theme %s does not provide a template for application " + + "%s and has not default template for " + + "applications.", + theme, + application + ) + ) + ); + } + + return String.format( + "@themes/%s/%s/%s", + theme, + Objects.toString(themeVersion), + themeTemplate.getPath() + ); + } + + 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; + } + + 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 + ) + ); + } + } + + private String parseThemeParam(final UriInfo uriInfo) { + if (uriInfo.getQueryParameters().containsKey("theme")) { + return uriInfo.getQueryParameters().getFirst("theme"); + } else { + return "--DEFAULT--"; + } + } + + private ThemeVersion parsePreviewParam(final UriInfo uriInfo) { + if (uriInfo.getQueryParameters().containsKey("preview")) { + final List values = uriInfo + .getQueryParameters() + .get("preview"); + if (values.contains("theme") || values.contains("all")) { + return ThemeVersion.DRAFT; + } else { + return ThemeVersion.LIVE; + } + } else { + return ThemeVersion.LIVE; + } + } + +} From 8920bbb7c2381c651793f909057ac23c33caa844 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Sun, 20 Dec 2020 11:41:51 +0100 Subject: [PATCH 02/12] Controller for EE MVC based login app --- .../libreccm/ui/login/LoginApplication.java | 44 ++++++ .../libreccm/ui/login/LoginController.java | 143 ++++++++++++++++++ .../org/libreccm/ui/login/LoginMessages.java | 137 +++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java create mode 100644 ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java create mode 100644 ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java new file mode 100644 index 000000000..10d821a8f --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginApplication.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.ui.login; + +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * + * @author Jens Pelzetter + */ +@ApplicationPath("/@login") +public class LoginApplication extends Application { + + @Override + public Set> getClasses() { + final Set> classes = new HashSet<>(); + classes.add(LoginController.class); + + return classes; + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java new file mode 100644 index 000000000..3767f846b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java @@ -0,0 +1,143 @@ +/* + * 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.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.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.core.Context; +import javax.ws.rs.core.UriInfo; + +/** + * + * @author Jens Pelzetter + */ +@Controller +@Path("/") +@RequestScoped +public class LoginController { + + @Inject + private ChallengeManager challengeManager; + + @Inject + private ConfigurationManager confManager; + + @Inject + private Models models; + + @Inject + private Subject subject; + + @Inject + private ThemesMvc themesMvc; + + @Inject + private UserRepository userRepository; + + @GET + @Path("/") + public String getLoginForm( + @Context final UriInfo uriInfo, + @QueryParam("return_url") final String redirectUrl + + ) { + models.put( + "emailIsPrimaryIdentifier", isEmailPrimaryIdentifier() + ); + models.put("returnUrl", redirectUrl); + return themesMvc.getMvcTemplate(uriInfo, "login-form"); + } + + @POST + @Path("/") + public String processLogin( + @Context final UriInfo uriInfo, + @FormParam("login") final String login, + @FormParam("password") final String password, + @FormParam("rememberMe") final String rememberMeValue, + @FormParam("redirectUrl") @DefaultValue("") final String redirectUrl + ) { + 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, redirectUrl); + } + + return String.format("redirect:%s", redirectUrl); + } + + @GET + @Path("/recover-password") + public String getRecoverPasswordForm(@Context final UriInfo uriInfo) { + return themesMvc.getMvcTemplate(uriInfo, "login-recover-password"); + } + + @POST + @Path("/recover-password") + public String recoverPassword( + @Context final UriInfo uriInfo, + @FormParam("email") final String email + ) { + final Optional user = userRepository.findByEmailAddress(email); + if (user.isPresent()) { + try { + challengeManager.sendPasswordRecover(user.get()); + } catch(MessagingException ex) { + models.put("failedToSendRecoverMessage", true); + return getRecoverPasswordForm(uriInfo); + } + } + + return themesMvc.getMvcTemplate(uriInfo, "login-password-recovered"); + } + + private boolean isEmailPrimaryIdentifier() { + final KernelConfig kernelConfig = confManager.findConfiguration( + KernelConfig.class + ); + return kernelConfig.emailIsPrimaryIdentifier(); + } +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java new file mode 100644 index 000000000..0c8a3e7d3 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2020 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.ui.login; + +import com.arsdigita.ui.login.LoginConstants; + +import org.libreccm.l10n.GlobalizationHelper; + +import java.text.MessageFormat; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; + +/** + * + * @author Jens Pelzetter + */ +@RequestScoped +@Named("LoginMessages") +public class LoginMessages extends AbstractMap{ + + /** + * Provides access to the locale negoiated by LibreCCM. + */ + @Inject + private GlobalizationHelper globalizationHelper; + + /** + * The {@link ResourceBundle} to use. + */ + private ResourceBundle messages; + + /** + * Loads the resource bundle. + */ + @PostConstruct + private void init() { + messages = ResourceBundle.getBundle( + LoginConstants.LOGIN_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 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> entrySet() { + return messages + .keySet() + .stream() + .collect( + Collectors.toMap(key -> key, key -> messages.getString(key)) + ) + .entrySet(); + } + + +} From 7002ed7682c1e1640a4ba6bdc8219f992ddaf8b1 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Wed, 23 Dec 2020 21:48:31 +0100 Subject: [PATCH 03/12] Krazo Theme integration --- .../arsdigita/ui/login/LoginConstants.java | 3 + .../MvcFreemarkerConfigurationProducer.java | 4 + .../theming/manifest/ThemeManifest.java | 3 +- .../org/libreccm/theming/mvc/ThemesMvc.java | 2 +- .../org/libreccm/ui/login/LoginMessages.java | 2 +- .../src/main/resources/META-INF/beans.xml | 4 + .../org/libreccm/ui/LoginBundle.properties | 14 ++++ .../org/libreccm/ui/LoginBundle_de.properties | 14 ++++ .../ccm-freemarker/login/login-form.html.ftl | 35 ++++++++ .../login/login-password-recovered.html.ftl | 15 ++++ .../login/login-recover-password.html.ftl | 27 ++++++ .../themes/ccm-freemarker/theme.json | 82 ++++++++++++++++++- 12 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 ccm-core/src/main/resources/org/libreccm/ui/LoginBundle.properties create mode 100644 ccm-core/src/main/resources/org/libreccm/ui/LoginBundle_de.properties create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/login/login-password-recovered.html.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/login/login-recover-password.html.ftl diff --git a/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java b/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java index 8c179c2d5..cf2b69679 100644 --- a/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java +++ b/ccm-core/src/main/java/com/arsdigita/ui/login/LoginConstants.java @@ -30,6 +30,9 @@ public interface LoginConstants { public static final String LOGIN_BUNDLE = "com.arsdigita.ui.login.LoginResources"; + + public static final String LOGIN_UI_BUNDLE + = "org.libreccm.ui.LoginBundle"; public static final GlobalizedMessage SUBMIT = LoginHelper.getMessage( "login.submit"); diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java index a209a462a..2282db361 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java @@ -29,6 +29,7 @@ import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer; import org.libreccm.theming.Themes; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; import javax.enterprise.inject.Specializes; import javax.inject.Inject; import javax.servlet.ServletContext; @@ -41,6 +42,8 @@ import javax.ws.rs.Produces; * @author Jens Pelzetter */ @ApplicationScoped +@Alternative +@Specializes public class MvcFreemarkerConfigurationProducer extends DefaultConfigurationProducer { @@ -52,6 +55,7 @@ public class MvcFreemarkerConfigurationProducer @Produces @ViewEngineConfig + @Alternative @Specializes @Override public Configuration getConfiguration() { diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java index e70d61884..4c91258b4 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java +++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java @@ -91,8 +91,7 @@ public class ThemeManifest implements Serializable { @XmlElement(name = "default-template", namespace = THEMES_XML_NS) private String defaultTemplate; - @XmlElementWrapper(name = "mvcTemplates", namespace = THEMES_XML_NS) - @XmlElement(name = "template", namespace = THEME_MANIFEST_XML) + @XmlElement(name = "mvc-templates", namespace = THEMES_XML_NS) private Map> mvcTemplates; public ThemeManifest() { diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java index 862da79bd..24620dd21 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java @@ -96,7 +96,7 @@ public class ThemesMvc { return String.format( "@themes/%s/%s/%s", - theme, + themeInfo.getName(), Objects.toString(themeVersion), themeTemplate.getPath() ); diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java index 0c8a3e7d3..5ae2a2795 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginMessages.java @@ -60,7 +60,7 @@ public class LoginMessages extends AbstractMap{ @PostConstruct private void init() { messages = ResourceBundle.getBundle( - LoginConstants.LOGIN_BUNDLE, + LoginConstants.LOGIN_UI_BUNDLE, globalizationHelper.getNegotiatedLocale() ); } diff --git a/ccm-core/src/main/resources/META-INF/beans.xml b/ccm-core/src/main/resources/META-INF/beans.xml index 295fa9e6c..bade5d06d 100644 --- a/ccm-core/src/main/resources/META-INF/beans.xml +++ b/ccm-core/src/main/resources/META-INF/beans.xml @@ -5,6 +5,10 @@ http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all"> + + org.libreccm.mvc.freemarker.MvcFreemarkerConfigurationProducer + + org.libreccm.security.AuthorizationInterceptor diff --git a/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle.properties b/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle.properties new file mode 100644 index 000000000..8959c3abb --- /dev/null +++ b/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle.properties @@ -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. diff --git a/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle_de.properties b/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle_de.properties new file mode 100644 index 000000000..6956e262f --- /dev/null +++ b/ccm-core/src/main/resources/org/libreccm/ui/LoginBundle_de.properties @@ -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. diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl new file mode 100644 index 000000000..ff42eb3c3 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl @@ -0,0 +1,35 @@ + + + + Category page + + + +
+

${LoginMessages['login.title']}

+ <# if (loginFailed)> +
+ ${LoginMessages['login.errors.failed']} +
+ +
+ + + + + + + +
+
+ <#include "footer.html.ftl"> + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-password-recovered.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-password-recovered.html.ftl new file mode 100644 index 000000000..ccee63371 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-password-recovered.html.ftl @@ -0,0 +1,15 @@ + + + + Category page + + + +
+

${LoginMessages['login.password_recovered.title']}

+

${LoginMessages['login.password_recovered.message']}

+
+ <#include "footer.html.ftl"> + + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-recover-password.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-recover-password.html.ftl new file mode 100644 index 000000000..b39124964 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-recover-password.html.ftl @@ -0,0 +1,27 @@ + + + + Category page + + + +
+

${LoginMessages['login.recover_password.title']}

+ <# if (failedToSendRecoverMessage)> +
+ ${LoginMessages['login.errors.failedToSendRecoverMessage']} +
+ +
+ + + + +
+
+ <#include "footer.html.ftl"> + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json index b3e021ea8..a71f3595d 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json @@ -1,6 +1,84 @@ { "name": "ccm-freemarker", "type": "freemarker", - - "default-template": "category-page.html.ftl" + + "default-template": "category-page.html.ftl", + + "mvc-templates": { + "applications": { + "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" + } + ] + } + } + } + } + } } + From 8d3b72efeee999c88f0ca26a51f1ba92beffa452 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Thu, 24 Dec 2020 12:03:33 +0100 Subject: [PATCH 04/12] Some bugfixes --- .../freemarker/FreemarkerViewEngine.java.off | 83 +++++++++++++++++++ .../mvc/freemarker/KrazoTemplateLoader.java | 4 +- .../MvcFreemarkerConfigurationProducer.java | 14 ++-- .../mvc/freemarker/ThemesTemplateLoader.java | 4 +- .../src/main/resources/META-INF/beans.xml | 5 +- 5 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off new file mode 100644 index 000000000..8472dd470 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off @@ -0,0 +1,83 @@ +/* + * 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 Jens Pelzetter + */ +@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 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); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java index 6b31bbc11..d10c970f8 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/KrazoTemplateLoader.java @@ -53,7 +53,9 @@ class KrazoTemplateLoader implements TemplateLoader { @Override public Object findTemplateSource(final String name) throws IOException { - if (name.startsWith("@themes") || name.startsWith("/@themes")) { + if (name.startsWith("@themes") + || name.startsWith("/@themes") + || name.startsWith("WEB-INF/views/@themes")) { return null; } else { // Freemarker drops "/" diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java index 2282db361..97b038240 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java @@ -28,12 +28,11 @@ import org.eclipse.krazo.engine.ViewEngineConfig; import org.eclipse.krazo.ext.freemarker.DefaultConfigurationProducer; import org.libreccm.theming.Themes; -import javax.enterprise.context.ApplicationScoped; -import javax.enterprise.inject.Alternative; +import javax.enterprise.inject.Produces; import javax.enterprise.inject.Specializes; import javax.inject.Inject; import javax.servlet.ServletContext; -import javax.ws.rs.Produces; + /** * Extends the default configuration for Freemarker of Eclipse Krazo to @@ -41,9 +40,10 @@ import javax.ws.rs.Produces; * * @author Jens Pelzetter */ -@ApplicationScoped -@Alternative -@Specializes +//@ApplicationScoped +//@Alternative +//@Specializes +//@Priority(3000) public class MvcFreemarkerConfigurationProducer extends DefaultConfigurationProducer { @@ -55,7 +55,7 @@ public class MvcFreemarkerConfigurationProducer @Produces @ViewEngineConfig - @Alternative +// @Alternative @Specializes @Override public Configuration getConfiguration() { diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java index 478a505a5..e183c6bf8 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java @@ -66,7 +66,9 @@ class ThemesTemplateLoader implements TemplateLoader { */ @Override public Object findTemplateSource(final String path) throws IOException { - if (path.startsWith("@themes") || path.startsWith("/@themes")) { + if (path.startsWith("@themes") + || path.startsWith("/@themes") + || path.startsWith("WEB-INF/views/@themes")) { final String[] tokens; if (path.startsWith("/")) { tokens = path.substring(1).split("/"); diff --git a/ccm-core/src/main/resources/META-INF/beans.xml b/ccm-core/src/main/resources/META-INF/beans.xml index bade5d06d..eecc609e3 100644 --- a/ccm-core/src/main/resources/META-INF/beans.xml +++ b/ccm-core/src/main/resources/META-INF/beans.xml @@ -5,9 +5,10 @@ http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all"> - + org.libreccm.security.AuthorizationInterceptor From 12d779b5d3f4d025d5ddc28ffc52ab8e36aa670f Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Mon, 4 Jan 2021 19:46:13 +0100 Subject: [PATCH 05/12] MVC theme integration --- .../mvc/freemarker/FreemarkerViewEngine.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java new file mode 100644 index 000000000..861ecdac8 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java @@ -0,0 +1,150 @@ +/* + * 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 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; + +/** + * + * @author Jens Pelzetter + */ +@ApplicationScoped +@Priority(ViewEngine.PRIORITY_APPLICATION) +public class FreemarkerViewEngine extends ViewEngineBase { + + @Inject + private BeanManager beanManager; + + @Inject + @ViewEngineConfig + private Configuration configuration; + + @Inject + private MvcContext mvc; + + @Override + public boolean supports(String view) { + return view.endsWith(".ftl"); + } + + @Override + public void processView(final ViewEngineContext context) + throws ViewEngineException { + + final Charset charset = resolveCharsetAndSetContentType(context); + + try (final Writer writer = new OutputStreamWriter( + context.getOutputStream(), charset + )) { + final Template template = configuration.getTemplate( + resolveView(context) + ); + + final Map model = new HashMap<>(); + model.put("mvc", mvc); + model.put("request", context.getRequest(HttpServletRequest.class)); + + final Map namedBeans = beanManager + .getBeans(Object.class) + .stream() + .filter(bean -> bean.getName() != null) + .map(this::findBeanInstance) + .filter(Optional::isPresent) + .map(Optional::get) + .collect( + Collectors.toMap( + NamedBeanInstance::getName, + NamedBeanInstance::getBeanInstance + ) + ); + + model.putAll(namedBeans); + model.putAll(context.getModels().asMap()); + + template.process(model, writer); + } catch (TemplateException | IOException e) { + throw new ViewEngineException(e); + } + } + + @SuppressWarnings("rawtypes") + private Optional findBeanInstance(final Bean bean) { + final Context context = beanManager.getContext(bean.getScope()); + final CreationalContext creationalContext = beanManager + .createCreationalContext(bean); + @SuppressWarnings("unchecked") + final Object instance = context.get(bean, creationalContext); + + if (instance == null) { + return Optional.empty(); + } else { + return Optional.of( + new NamedBeanInstance(bean.getName(), instance) + ); + } + } + + private class NamedBeanInstance { + + private final String name; + + 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; + } + + } + +} From f0e152822df97cb4661dd00e0ee26fe56cb9e2b1 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Tue, 5 Jan 2021 19:21:23 +0100 Subject: [PATCH 06/12] Integration of EE MVC and CCM themes --- .../mvc/freemarker/FreemarkerViewEngine.java | 150 +++++++++++++++++- .../freemarker/FreemarkerViewEngine.java.off | 83 ---------- .../MvcFreemarkerConfigurationProducer.java | 33 ++-- .../libreccm/mvc/freemarker/TemplateInfo.java | 46 ++++++ .../mvc/freemarker/ThemeTemplateUtil.java | 128 +++++++++++++++ .../mvc/freemarker/ThemesTemplateLoader.java | 83 +++------- .../theming/mvc/ThemeResourceProvider.java | 139 ++++++++++++++++ .../libreccm/theming/mvc/ThemeResources.java | 43 +++++ .../org/libreccm/theming/mvc/ThemesMvc.java | 25 ++- .../libreccm/ui/login/LoginController.java | 1 + .../ccm-freemarker/login/login-form.html.ftl | 14 +- .../themes/ccm-freemarker/theme-index.json | 70 ++++++++ .../mvc/ThemeResourceProviderTest.java | 59 +++++++ 13 files changed, 709 insertions(+), 165 deletions(-) delete mode 100644 ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off create mode 100644 ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java create mode 100644 ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java create mode 100644 ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java create mode 100644 ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/theme-index.json create mode 100644 ccm-core/src/test/java/org/libreccm/theming/mvc/ThemeResourceProviderTest.java diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java index 861ecdac8..14e41ce92 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java @@ -19,16 +19,27 @@ 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.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.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; @@ -60,10 +71,22 @@ public class FreemarkerViewEngine extends ViewEngineBase { @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"); @@ -86,6 +109,23 @@ public class FreemarkerViewEngine extends ViewEngineBase { model.put("mvc", mvc); model.put("request", context.getRequest(HttpServletRequest.class)); + final Optional templateInfo = themeTemplateUtil + .getTemplateInfo(context.getView()); + final ThemeProvider themeProvider = themeTemplateUtil + .findThemeProvider(templateInfo.get().getThemeInfo()); + if (templateInfo.isPresent()) { + final ThemeInfo themeInfo = templateInfo.get().getThemeInfo(); + model.put("getSetting", + new GetSettingMethod(themeInfo, themeProvider) + ); + model.put("localize", + new LocalizeMethod(themeInfo, themeProvider) + ); + } + model.put("truncateText", new TruncateTextMethod()); + + + final Map namedBeans = beanManager .getBeans(Object.class) .stream() @@ -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."); + } + } + + } + } diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off deleted file mode 100644 index 8472dd470..000000000 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java.off +++ /dev/null @@ -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 Jens Pelzetter - */ -@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 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); - } - } - -} diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java index 97b038240..83c5c6fd9 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/MvcFreemarkerConfigurationProducer.java @@ -31,38 +31,39 @@ import org.libreccm.theming.Themes; import javax.enterprise.inject.Produces; import javax.enterprise.inject.Specializes; import javax.inject.Inject; +import javax.mvc.Models; import javax.servlet.ServletContext; - /** - * Extends the default configuration for Freemarker of Eclipse Krazo to - * support Freemarker templates in CCM themes. - * + * Extends the default configuration for Freemarker of Eclipse Krazo to support + * Freemarker templates in CCM themes. + * * @author Jens Pelzetter */ -//@ApplicationScoped -//@Alternative -//@Specializes -//@Priority(3000) -public class MvcFreemarkerConfigurationProducer +public class MvcFreemarkerConfigurationProducer extends DefaultConfigurationProducer { - + + @Inject + private Models models; + @Inject private ServletContext servletContext; - + @Inject private Themes themes; + @Inject + private ThemeTemplateUtil themeTemplateUtil; + @Produces @ViewEngineConfig -// @Alternative @Specializes @Override public Configuration getConfiguration() { final Configuration configuration = new Configuration( Configuration.VERSION_2_3_30 ); - + configuration.setDefaultEncoding("UTF-8"); configuration.setTemplateExceptionHandler( TemplateExceptionHandler.RETHROW_HANDLER @@ -74,7 +75,7 @@ public class MvcFreemarkerConfigurationProducer new MultiTemplateLoader( new TemplateLoader[]{ new KrazoTemplateLoader(servletContext), - new ThemesTemplateLoader(themes), + new ThemesTemplateLoader(themes, themeTemplateUtil), // For loading Freemarker macro libraries from WEB-INF // resources new WebappTemplateLoader( @@ -86,8 +87,8 @@ public class MvcFreemarkerConfigurationProducer } ) ); - + return configuration; } - + } diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java new file mode 100644 index 000000000..3eda69687 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java @@ -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 Jens Pelzetter + */ +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; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java new file mode 100644 index 000000000..80b693f6f --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java @@ -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 Jens Pelzetter + */ +@RequestScoped +class ThemeTemplateUtil { + + private static final Logger LOGGER = LogManager.getLogger(ThemeTemplateUtil.class); + + @Inject + private Instance themeProviders; + + @Inject + private Themes themes; + + public boolean isValidTemplatePath(final String templatePath) { + return templatePath.startsWith("@themes") + || templatePath.startsWith("/@themes"); + } + + public Optional getTemplateInfo(final String templatePath) { + if (!isValidTemplatePath(templatePath)) { + throw new IllegalArgumentException( + String.format( + "Provided template \"%s\" path does not start with " + + "\"@theme\" or \"/@theme\".", + templatePath + ) + ); + } + + final String[] tokens; + if (templatePath.startsWith("/")) { + tokens = templatePath.substring(1).split("/"); + } else { + tokens = templatePath.split("/"); + } + + return getTemplateInfo(tokens); + } + + public ThemeProvider findThemeProvider(final ThemeInfo forTheme) { + final Instance 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 getTemplateInfo(final String[] tokens) { + if (tokens.length >= 4) { + final String themeName = tokens[1]; + final ThemeVersion themeVersion = ThemeVersion.valueOf( + tokens[2] + ); + final String filePath = String.join( + "/", + Arrays.copyOfRange( + tokens, 3, tokens.length, String[].class + ) + ); + + final Optional themeInfo = themes.getTheme( + themeName, themeVersion + ); + + if (themeInfo.isPresent()) { + return Optional.of(new TemplateInfo(themeInfo.get(), filePath)); + } else { + return Optional.empty(); + } + } else { + throw new IllegalArgumentException( + String.format( + "Template path has wrong format. Expected at least " + + "four tokens separated by slashes, but found only %d", + tokens.length + ) + ); + } + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java index e183c6bf8..999b90ebb 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java @@ -39,8 +39,13 @@ class ThemesTemplateLoader implements TemplateLoader { private final Themes themes; - public ThemesTemplateLoader(final Themes themes) { + private final ThemeTemplateUtil themeTemplateUtil; + + public ThemesTemplateLoader( + final Themes themes, final ThemeTemplateUtil themeTemplateUtil + ) { this.themes = themes; + this.themeTemplateUtil = themeTemplateUtil; } /** @@ -53,75 +58,37 @@ class ThemesTemplateLoader implements TemplateLoader { * of the theme from which the template is loaded. {@code $version} is the * version of the theme to use. This token is converted to * {@link ThemeVersion}. Valid values are therefore {@code DRAFT} and - * {@code LIVE}. The remainder of the path is the path to the file inside the - * theme. + * {@code LIVE}. The remainder of the path is the path to the file inside + * the theme. * * @param path The path of the file. The path must include the theme and its * version. * * @return An {@link InputStream} for the template if the template was found - * in the theme. Otherwise {@code null} is returned. + * in the theme. Otherwise {@code null} is returned. * * @throws IOException */ @Override public Object findTemplateSource(final String path) throws IOException { - if (path.startsWith("@themes") - || path.startsWith("/@themes") - || path.startsWith("WEB-INF/views/@themes")) { - final String[] tokens; - if (path.startsWith("/")) { - tokens = path.substring(1).split("/"); + if (themeTemplateUtil.isValidTemplatePath(path)) { + final Optional templateInfo = themeTemplateUtil + .getTemplateInfo(path); + + if (templateInfo.isPresent()) { + final Optional source = themes.getFileFromTheme( + templateInfo.get().getThemeInfo(), + templateInfo.get().getFilePath() + ); + + if (source.isPresent()) { + return source.get(); + } else { + return null; + } } else { - tokens = path.split("/"); + return null; } - return findTemplateSource(tokens); - } else { - return null; - } - } - - private InputStream findTemplateSource(final String[] tokens) { - if (tokens.length >= 4) { - final String themeName = tokens[1]; - final ThemeVersion themeVersion = ThemeVersion - .valueOf(tokens[2]); - final String filePath = String.join( - "/", - Arrays.copyOfRange( - tokens, 3, tokens.length, String[].class - ) - ); - - return findTemplateSource(themeName, themeVersion, filePath); - } else { - return null; - } - } - - private InputStream findTemplateSource( - final String themeName, - final ThemeVersion themeVersion, - final String filePath - ) { - final Optional themeInfo = themes.getTheme( - themeName, themeVersion - ); - if (themeInfo.isPresent()) { - return findTemplateSource(themeInfo.get(), filePath); - } else { - return null; - } - } - - private InputStream findTemplateSource( - final ThemeInfo themeInfo, final String filePath - ) { - final Optional source = themes.getFileFromTheme( - themeInfo, filePath - ); - if (source.isPresent()) { - return source.get(); } else { return null; } diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java new file mode 100644 index 000000000..e9dcd7d3b --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java @@ -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 Jens Pelzetter + */ +@RequestScoped +@Path("/") +public class ThemeResourceProvider { + + @Inject + @Any + private Instance 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 provider = findProvider(themeName); + final ThemeVersion themeVersion = ThemeVersion.valueOf( + themeVersionParam + ); + + if (provider.isPresent()) { + final Optional fileInfo = provider + .get() + .getThemeFileInfo(themeName, themeVersion, pathParam); + + if (fileInfo.isPresent()) { + final ThemeFileInfo themeFileInfo = fileInfo.get(); + if (themeFileInfo.isDirectory()) { + return Response.status(Response.Status.FORBIDDEN).build(); + } else { + final Optional inputStream = provider + .get() + .getThemeFileAsStream( + themeName, themeVersion, pathParam + ); + if (inputStream.isPresent()) { + final InputStream inStream = inputStream.get(); + return Response + .ok(inStream) + .type(themeFileInfo.getMimeType()) + .build(); + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity( + String.format( + "File \"%s\" does not exist in version of " + + "theme %s.", + pathParam, + themeVersion, + themeName + ) + ) + .build(); + } + } + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity( + String.format( + "File \"%s\" does not exist in the %s " + + "version of theme %s.", + pathParam, + themeVersion, + themeName + ) + ) + .build(); + } + } else { + return Response + .status(Response.Status.NOT_FOUND) + .entity(String.format("Theme \"%s\" does not exist.", + themeName)) + .build(); + } + } + + private Optional findProvider(final String forTheme) { + + final List providersList = new ArrayList<>(); + providers + .forEach(provider -> providersList.add(provider)); + + return providersList + .stream() + .filter(current -> current.providesTheme(forTheme, + ThemeVersion.DRAFT)) + .findAny(); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java new file mode 100644 index 000000000..bdac41633 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package org.libreccm.theming.mvc; + +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * + * @author Jens Pelzetter + */ +@ApplicationPath("/@themes") +public class ThemeResources extends Application { + + @Override + public Set> getClasses() { + final Set> classes = new HashSet<>(); + classes.add(ThemeResourceProvider.class); + return classes; + } + + + +} diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java index 24620dd21..f999d9b2e 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java @@ -33,6 +33,8 @@ import java.util.Optional; 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; @@ -45,6 +47,12 @@ import javax.ws.rs.core.UriInfo; @RequestScoped public class ThemesMvc { + @Inject + private Models models; + + @Inject + private ServletContext servletContext; + @Inject private SiteRepository siteRepo; @@ -77,7 +85,7 @@ public class ThemesMvc { ); final ThemeTemplate themeTemplate; if (applicationTemplates.containsKey(application)) { - themeTemplate = applicationTemplates.get(application); + themeTemplate = applicationTemplates.get(application); } else { themeTemplate = Optional.ofNullable( 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( - "@themes/%s/%s/%s", + "/@themes/%s/%s/%s", themeInfo.getName(), Objects.toString(themeVersion), themeTemplate.getPath() diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java index 3767f846b..2617c4526 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java @@ -82,6 +82,7 @@ public class LoginController { models.put( "emailIsPrimaryIdentifier", isEmailPrimaryIdentifier() ); + models.put("loginFailed", false); models.put("returnUrl", redirectUrl); return themesMvc.getMvcTemplate(uriInfo, "login-form"); } diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl index ff42eb3c3..9b52ffed1 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl @@ -2,17 +2,21 @@ Category page - - + + +
+            ${themeUrl}/style.css
+        

${LoginMessages['login.title']}

- <# if (loginFailed)> + <#if (loginFailed)>
${LoginMessages['login.errors.failed']}
-
${mvc.uri('LoginController#processLogin')} + @@ -30,6 +34,6 @@
- <#include "footer.html.ftl"> + <#include "../footer.html.ftl"> diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme-index.json b/ccm-core/src/main/resources/themes/ccm-freemarker/theme-index.json new file mode 100644 index 000000000..efed2fefc --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme-index.json @@ -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" + } + ] +} diff --git a/ccm-core/src/test/java/org/libreccm/theming/mvc/ThemeResourceProviderTest.java b/ccm-core/src/test/java/org/libreccm/theming/mvc/ThemeResourceProviderTest.java new file mode 100644 index 000000000..fa1b3dfaf --- /dev/null +++ b/ccm-core/src/test/java/org/libreccm/theming/mvc/ThemeResourceProviderTest.java @@ -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 Jens Pelzetter + */ +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."); + } + +} From 1530cd74c8e86b5a99bc902e7dc5b34544fa672e Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Wed, 6 Jan 2021 14:46:50 +0100 Subject: [PATCH 07/12] Small fixes for login app --- .../libreccm/ui/login/LoginController.java | 83 +++++++++++++------ .../ccm-freemarker/login/login-form.html.ftl | 8 +- 2 files changed, 60 insertions(+), 31 deletions(-) diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java index 2617c4526..01df138c7 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java @@ -29,6 +29,8 @@ 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; @@ -36,13 +38,19 @@ 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; /** @@ -53,48 +61,52 @@ import javax.ws.rs.core.UriInfo; @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("return_url") final String redirectUrl - + @QueryParam("returnUrl") @DefaultValue("") final String returnUrl ) { - models.put( + models.put( "emailIsPrimaryIdentifier", isEmailPrimaryIdentifier() ); - models.put("loginFailed", false); - models.put("returnUrl", redirectUrl); + if (models.get("loginFailed") == null) { + models.put("loginFailed", false); + } + models.put("returnUrl", returnUrl); return themesMvc.getMvcTemplate(uriInfo, "login-form"); } - + @POST @Path("/") - public String processLogin( + public Object processLogin( @Context final UriInfo uriInfo, @FormParam("login") final String login, @FormParam("password") final String password, @FormParam("rememberMe") final String rememberMeValue, - @FormParam("redirectUrl") @DefaultValue("") final String redirectUrl + @FormParam("returnUrl") @DefaultValue("") final String returnUrl ) { final UsernamePasswordToken token = new UsernamePasswordToken( login, password @@ -102,20 +114,36 @@ public class LoginController { token.setRememberMe("on".equals(rememberMeValue)); try { subject.login(token); - } catch(AuthenticationException ex) { + } catch (AuthenticationException ex) { models.put("loginFailed", true); - return getLoginForm(uriInfo, redirectUrl); + 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 + ); } - - return String.format("redirect:%s", redirectUrl); } - + @GET @Path("/recover-password") public String getRecoverPasswordForm(@Context final UriInfo uriInfo) { return themesMvc.getMvcTemplate(uriInfo, "login-recover-password"); } - + @POST @Path("/recover-password") public String recoverPassword( @@ -125,20 +153,21 @@ public class LoginController { final Optional user = userRepository.findByEmailAddress(email); if (user.isPresent()) { try { - challengeManager.sendPasswordRecover(user.get()); - } catch(MessagingException ex) { + challengeManager.sendPasswordRecover(user.get()); + } catch (MessagingException ex) { models.put("failedToSendRecoverMessage", true); return getRecoverPasswordForm(uriInfo); } } - + return themesMvc.getMvcTemplate(uriInfo, "login-password-recovered"); - } - + } + private boolean isEmailPrimaryIdentifier() { - final KernelConfig kernelConfig = confManager.findConfiguration( + final KernelConfig kernelConfig = confManager.findConfiguration( KernelConfig.class ); return kernelConfig.emailIsPrimaryIdentifier(); } + } diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl index 9b52ffed1..d4052f387 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl @@ -5,9 +5,6 @@ -
-            ${themeUrl}/style.css
-        

${LoginMessages['login.title']}

<#if (loginFailed)> @@ -15,7 +12,6 @@ ${LoginMessages['login.errors.failed']} -
${mvc.uri('LoginController#processLogin')}
@@ -29,6 +25,10 @@ required="true" type="password" /> + + From a6b157306ecefa83a43599e8d70b966508bb421e Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Wed, 6 Jan 2021 17:13:40 +0100 Subject: [PATCH 08/12] Logout Application --- .../libreccm/ui/login/LoginController.java | 2 +- .../libreccm/ui/login/LogoutApplication.java | 41 ++++++++++++++ .../libreccm/ui/login/LogoutController.java | 55 +++++++++++++++++++ .../ccm-freemarker/logout/loggedout.html.ftl | 13 +++++ .../themes/ccm-freemarker/theme.json | 24 ++++++++ 5 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 ccm-core/src/main/java/org/libreccm/ui/login/LogoutApplication.java create mode 100644 ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/logout/loggedout.html.ftl diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java index 01df138c7..d75e0c61d 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java @@ -118,7 +118,7 @@ public class LoginController { models.put("loginFailed", true); return getLoginForm(uriInfo, returnUrl); } - + try { return Response.seeOther( new URI( diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LogoutApplication.java b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutApplication.java new file mode 100644 index 000000000..6d942471a --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutApplication.java @@ -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 Jens Pelzetter + */ +@ApplicationPath("/@logout") +public class LogoutApplication extends Application { + + @Override + public Set> getClasses() { + final Set> classes = new HashSet<>(); + classes.add(LogoutController.class); + return classes; + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java new file mode 100644 index 000000000..48f41ccf9 --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java @@ -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 Jens Pelzetter + */ +@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"); + } + +} diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/logout/loggedout.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/logout/loggedout.html.ftl new file mode 100644 index 000000000..c126f5956 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/logout/loggedout.html.ftl @@ -0,0 +1,13 @@ + + + + Category page + + + +
+

Logout successful

+

Logout successful

+
+ + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json index a71f3595d..fce2189d4 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json @@ -77,6 +77,30 @@ ] } } + }, + "logout": { + "description": { + "values": { + "value": [ + { + "lang": "en", + "value": "Logout successful" + } + ] + } + }, + "name": "loggedout", + "path": "logout/loggedout.html.ftl", + "title": { + "values": { + "value": [ + { + "lang": "en", + "value": "Logout succesful" + } + ] + } + } } } } From ae1a3447e3659aadc24957577dc5b80cfa82704f Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Wed, 6 Jan 2021 19:27:44 +0100 Subject: [PATCH 09/12] More user friendly methods for getting template for ThemesMvc --- .../theming/manifest/ThemeManifest.java | 104 +++++---- .../org/libreccm/theming/mvc/ThemesMvc.java | 61 +++-- .../libreccm/ui/login/LoginController.java | 6 +- .../libreccm/ui/login/LogoutController.java | 2 +- .../themes/ccm-freemarker/theme.json | 221 ++++++++++-------- 5 files changed, 225 insertions(+), 169 deletions(-) diff --git a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java index 4c91258b4..27f1a5b76 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java +++ b/ccm-core/src/main/java/org/libreccm/theming/manifest/ThemeManifest.java @@ -92,11 +92,15 @@ public class ThemeManifest implements Serializable { private String defaultTemplate; @XmlElement(name = "mvc-templates", namespace = THEMES_XML_NS) - private Map> mvcTemplates; + private Map mvcTemplates; + + @XmlElement(name = "views", namespace = THEMES_XML_NS) + private Map> views; public ThemeManifest() { templates = new ArrayList<>(); mvcTemplates = new HashMap<>(); + views = new HashMap<>(); } public String getName() { @@ -163,65 +167,62 @@ public class ThemeManifest implements Serializable { this.defaultTemplate = defaultTemplate; } - public Map> getMvcTemplates() { + public Map getMvcTemplates() { return Collections.unmodifiableMap(mvcTemplates); } - public Optional> getMvcTemplatesOfCategory( - final String category - ) { - return Optional.ofNullable(mvcTemplates.get(category)); - } - - public void addMvcTemplatesCategory(final String category) { - mvcTemplates.put(category, new HashMap<>()); - } - - public void addMvcTemplatesCategory( - final String category, final Map templates - ) { - mvcTemplates.put(category, templates); - } - - public Optional getMvcTemplate( - final String category, final String objectType - ) { - final Optional> templatesInCat - = getMvcTemplatesOfCategory(category); - - if (templatesInCat.isPresent()) { - return Optional.ofNullable(templatesInCat.get().get(objectType)); - } else { - return Optional.empty(); - } + public Optional getMvcTemplate(final String name) { + return Optional.ofNullable(mvcTemplates.get(name)); } public void addMvcTemplate( - final String category, - final String objectType, - final ThemeTemplate template + final String name, final ThemeTemplate template ) { - if (!mvcTemplates.containsKey(category)) { - addMvcTemplatesCategory(category); - } - - mvcTemplates - .get(category) - .put( - objectType, - Objects.requireNonNull( - template, - "Template can't be null." - ) - ); + mvcTemplates.put(name, template); } protected void setMvcTemplates( - final Map> mvcTemplates + final Map mvcTemplates ) { this.mvcTemplates = mvcTemplates; } + public Map> getViews() { + return Collections.unmodifiableMap(views); + } + + public Map getViewsOfApplication(final String application) { + if (views.containsKey(application)) { + return views.get(application); + } else { + return Collections.emptyMap(); + } + } + + public void addViewsOfApplication( + final String application, final Map viewsOfApplication + ) { + views.put(application, viewsOfApplication); + } + + public void addViewToApplication( + final String application, final String view, final String templateName + ) { + final Map applicationViews; + if (views.containsKey(application)) { + applicationViews = views.get(application); + } else { + applicationViews = new HashMap<>(); + views.put(application, applicationViews); + } + + applicationViews.put(view, templateName); + } + + protected void setViews(final Map> views) { + this.views = new HashMap<>(views); + } + @Override public int hashCode() { int hash = 7; @@ -233,6 +234,7 @@ public class ThemeManifest implements Serializable { hash = 83 * hash + Objects.hashCode(templates); hash = 83 * hash + Objects.hashCode(defaultTemplate); hash = 83 * hash + Objects.hashCode(mvcTemplates); + hash = 83 * hash + Objects.hashCode(views); return hash; } @@ -272,7 +274,11 @@ public class ThemeManifest implements Serializable { if (!Objects.equals(defaultTemplate, other.getDefaultTemplate())) { return false; } - return mvcTemplates.equals(other.getMvcTemplates()); + if (!Objects.equals(mvcTemplates, other.getMvcTemplates())) { + return false; + } + + return Objects.equals(views, other.getViews()); } public boolean canEqual(final Object obj) { @@ -295,7 +301,8 @@ public class ThemeManifest implements Serializable { + "description = \"%s\", " + "templates = %s, " + "defaultTemplate, " - + "mvcTemplates = %s%s" + + "mvcTemplates = %s," + + "views = %s%s" + " }", super.toString(), name, @@ -306,6 +313,7 @@ public class ThemeManifest implements Serializable { Objects.toString(templates), defaultTemplate, Objects.toString(mvcTemplates), + Objects.toString(views), data ); diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java index f999d9b2e..999bb7630 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java @@ -18,6 +18,7 @@ */ package org.libreccm.theming.mvc; +import org.libreccm.core.UnexpectedErrorException; import org.libreccm.sites.Site; import org.libreccm.sites.SiteRepository; import org.libreccm.theming.ThemeInfo; @@ -61,7 +62,8 @@ public class ThemesMvc { public String getMvcTemplate( final UriInfo uriInfo, - final String application + final String application, + final String view ) { final Site site = getSite(uriInfo); final String theme = parseThemeParam(uriInfo); @@ -72,35 +74,46 @@ public class ThemesMvc { themeVersion ); final ThemeManifest manifest = themeInfo.getManifest(); - final Map applicationTemplates = manifest - .getMvcTemplatesOfCategory("applications") + final Map views = manifest.getViewsOfApplication( + application + ); + final String viewTemplateName; + if (views.containsKey(view)) { + viewTemplateName = views.get(view); + } else { + final Map defaultAppViews = manifest + .getViewsOfApplication(application); + if (defaultAppViews.containsKey("default")) { + viewTemplateName = defaultAppViews.get("default"); + } else { + throw new WebApplicationException( + String.format( + "Theme \"%s\" does not provide a template for view " + + "\"%s\" of application \"%s\", and there is no " + + "default template configured.", + themeInfo.getName(), + view, + application + ) + ); + } + } + + final ThemeTemplate themeTemplate = manifest + .getMvcTemplate(viewTemplateName) .orElseThrow( () -> new WebApplicationException( String.format( - "Manifest of theme %s has no application templates.", - themeInfo.getName() - ), - Response.Status.INTERNAL_SERVER_ERROR - ) - ); - final ThemeTemplate themeTemplate; - if (applicationTemplates.containsKey(application)) { - themeTemplate = applicationTemplates.get(application); - } else { - themeTemplate = Optional.ofNullable( - applicationTemplates.get("@default") - ).orElseThrow( - () -> new WebApplicationException( - String.format( - "Theme %s does not provide a template for application " - + "%s and has not default template for " - + "applications.", - theme, - application + "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()); diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java index d75e0c61d..f006369e5 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LoginController.java @@ -96,7 +96,7 @@ public class LoginController { models.put("loginFailed", false); } models.put("returnUrl", returnUrl); - return themesMvc.getMvcTemplate(uriInfo, "login-form"); + return themesMvc.getMvcTemplate(uriInfo, "login", "loginForm"); } @POST @@ -141,7 +141,7 @@ public class LoginController { @GET @Path("/recover-password") public String getRecoverPasswordForm(@Context final UriInfo uriInfo) { - return themesMvc.getMvcTemplate(uriInfo, "login-recover-password"); + return themesMvc.getMvcTemplate(uriInfo, "login", "recoverPassword"); } @POST @@ -160,7 +160,7 @@ public class LoginController { } } - return themesMvc.getMvcTemplate(uriInfo, "login-password-recovered"); + return themesMvc.getMvcTemplate(uriInfo, "login", "passwordRecovered"); } private boolean isEmailPrimaryIdentifier() { diff --git a/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java index 48f41ccf9..52b5406a7 100644 --- a/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java +++ b/ccm-core/src/main/java/org/libreccm/ui/login/LogoutController.java @@ -49,7 +49,7 @@ public class LogoutController { public String logout(@Context final UriInfo uriInfo) { subject.logout(); - return themesMvc.getMvcTemplate(uriInfo, "logout"); + return themesMvc.getMvcTemplate(uriInfo, "logout", "loggedout"); } } diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json index fce2189d4..935946ed8 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/theme.json @@ -5,103 +5,138 @@ "default-template": "category-page.html.ftl", "mvc-templates": { - "applications": { - "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" - } - ] - } + "category-page": { + "description": { + "values": { + "value": [ + { + "lang": "en", + "value": "Category Page Template" + } + ] } }, - "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" - } - ] - } - } - }, - "logout": { - "description": { - "values": { - "value": [ - { - "lang": "en", - "value": "Logout successful" - } - ] - } - }, - "name": "loggedout", - "path": "logout/loggedout.html.ftl", - "title": { - "values": { - "value": [ - { - "lang": "en", - "value": "Logout succesful" - } - ] - } + "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" } } } From 4f338ad2248d47969ca79e6fb34b26f6ca075d10 Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Thu, 7 Jan 2021 20:18:19 +0100 Subject: [PATCH 10/12] JavaDoc for the integration of EE MVC and the themeing system --- .../mvc/freemarker/FreemarkerViewEngine.java | 43 +++++++++++++++- .../libreccm/mvc/freemarker/TemplateInfo.java | 9 +++- .../mvc/freemarker/ThemeTemplateUtil.java | 39 +++++++++++++- .../mvc/freemarker/ThemesTemplateLoader.java | 2 - .../theming/mvc/ThemeResourceProvider.java | 36 +++++++++++-- .../libreccm/theming/mvc/ThemeResources.java | 6 +-- .../org/libreccm/theming/mvc/ThemesMvc.java | 51 ++++++++++++++++++- .../libreccm/theming/mvc/package-info.java | 22 ++++++++ 8 files changed, 191 insertions(+), 17 deletions(-) create mode 100644 ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java index 14e41ce92..910e74084 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java @@ -58,6 +58,22 @@ import javax.mvc.engine.ViewEngineException; import javax.servlet.http.HttpServletRequest; /** + * Customized version of the Freemarker View Engine. This class is based of the + * View Engine from the Krazo project, but has been extended: + *
    + *
  • Named Beans are supported
  • + *
  • Freemarker template have access to the MvcContext under the name + * {@code mvc}, as in Facelet-based templates
  • + *
  • The current {@link HttpServletRequest} is made avaiable in Freemarker + * templates as {@link request}.
  • + *
  • The following utility functions are made available: + *
      + *
    • {@code getSetting}: retreives the value of a setting from the theme
    • + *
    • {@code localize}: retreives a localized value from the theme
    • + *
    • {@code truncateText}: Truncates text to a specific length.
    • + *
    + *
  • + *
* * @author Jens Pelzetter */ @@ -123,8 +139,6 @@ public class FreemarkerViewEngine extends ViewEngineBase { ); } model.put("truncateText", new TruncateTextMethod()); - - final Map namedBeans = beanManager .getBeans(Object.class) @@ -149,6 +163,13 @@ public class FreemarkerViewEngine extends ViewEngineBase { } } + /** + * Helper method for retrieving a an instance of a named bean using CDI. + * + * @param bean The bean to retrieve. + * + * @return An instance of the bean. + */ @SuppressWarnings("rawtypes") private Optional findBeanInstance(final Bean bean) { final Context context = beanManager.getContext(bean.getScope()); @@ -166,10 +187,19 @@ public class FreemarkerViewEngine extends ViewEngineBase { } } + /** + * 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) { @@ -187,6 +217,9 @@ public class FreemarkerViewEngine extends ViewEngineBase { } + /** + * Retrieves a setting from the theme using the {@link SettingsUtils}. + */ private class GetSettingMethod implements TemplateMethodModelEx { private final ThemeInfo fromTheme; @@ -241,6 +274,9 @@ public class FreemarkerViewEngine extends ViewEngineBase { } + /** + * Retrieves a localized value from the theme using the {@link L10NUtils}. + */ private class LocalizeMethod implements TemplateMethodModelEx { private final ThemeInfo fromTheme; @@ -275,6 +311,9 @@ public class FreemarkerViewEngine extends ViewEngineBase { } + /** + * Truncates text to a specific length. + */ private class TruncateTextMethod implements TemplateMethodModelEx { @Override diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java index 3eda69687..55d6fe3d2 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/TemplateInfo.java @@ -21,13 +21,20 @@ package org.libreccm.mvc.freemarker; import org.libreccm.theming.ThemeInfo; /** - * + * Encapulates the data of a template. + * * @author Jens Pelzetter */ class TemplateInfo { + /** + * Info about the theme providing the template. + */ private final ThemeInfo themeInfo; + /** + * The path of the template, + */ private final String filePath; public TemplateInfo(ThemeInfo themeInfo, String filePath) { diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java index 80b693f6f..dc9721245 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemeTemplateUtil.java @@ -34,13 +34,15 @@ import javax.enterprise.inject.Instance; import javax.inject.Inject; /** + * Utility class for retreving a {@link TemplateInfo} instance for a template. * * @author Jens Pelzetter */ @RequestScoped class ThemeTemplateUtil { - private static final Logger LOGGER = LogManager.getLogger(ThemeTemplateUtil.class); + private static final Logger LOGGER = LogManager.getLogger( + ThemeTemplateUtil.class); @Inject private Instance themeProviders; @@ -48,11 +50,28 @@ class ThemeTemplateUtil { @Inject private Themes themes; + /** + * Checks if the provided path points to a template. + * + * @param templatePath The path of the template. + * + * @return {@code true} if the path points to a template, {@code false} + * otherwise. + */ public boolean isValidTemplatePath(final String templatePath) { return templatePath.startsWith("@themes") || templatePath.startsWith("/@themes"); } + /** + * Get the {@link TemplateInfo} for the template. + * + * @param templatePath The path of the template. + * + * @return An {@link Optional} with a {@link TemplateInfo} for the template. + * If the template is not available, an empty {@link Optional} is + * returned. + */ public Optional getTemplateInfo(final String templatePath) { if (!isValidTemplatePath(templatePath)) { throw new IllegalArgumentException( @@ -74,6 +93,13 @@ class ThemeTemplateUtil { 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 provider = themeProviders .select(forTheme.getProvider()); @@ -92,6 +118,15 @@ class ThemeTemplateUtil { return provider.get(); } + /** + * Retrieves the {@link TemplateInfo} for a template. + * + * @param tokens The tokens of the template path. + * + * @return An {@link Optional} with a {@link TemplateInfo} for the template. + * If the template is not available, an empty {@link Optional} is + * returned. + */ private Optional getTemplateInfo(final String[] tokens) { if (tokens.length >= 4) { final String themeName = tokens[1]; @@ -108,7 +143,7 @@ class ThemeTemplateUtil { final Optional themeInfo = themes.getTheme( themeName, themeVersion ); - + if (themeInfo.isPresent()) { return Optional.of(new TemplateInfo(themeInfo.get(), filePath)); } else { diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java index 999b90ebb..eaf9ede78 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/ThemesTemplateLoader.java @@ -19,7 +19,6 @@ package org.libreccm.mvc.freemarker; import freemarker.cache.TemplateLoader; -import org.libreccm.theming.ThemeInfo; import org.libreccm.theming.ThemeVersion; import org.libreccm.theming.Themes; @@ -27,7 +26,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.util.Arrays; import java.util.Optional; /** diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java index e9dcd7d3b..18315fdd9 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResourceProvider.java @@ -21,7 +21,6 @@ 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; @@ -38,6 +37,9 @@ import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; /** + * Provides access to the resources/assets of themes. + * + * @see ThemeResources * * @author Jens Pelzetter */ @@ -45,13 +47,30 @@ import javax.ws.rs.core.Response; @Path("/") public class ThemeResourceProvider { + /** + * Injection point for the available {@link ThemeProvider}s. + */ @Inject @Any private Instance providers; - @Inject - private Themes themes; - + /** + * 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( @@ -123,8 +142,15 @@ public class ThemeResourceProvider { } } + /** + * Helper method for finding the provider of a theme. + * + * @param forTheme The theme. + * + * @return An {@link Optional} with the provider of the theme. If there is + * no matching provider, an empty {@link Optional} is returned. + */ private Optional findProvider(final String forTheme) { - final List providersList = new ArrayList<>(); providers .forEach(provider -> providersList.add(provider)); diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java index bdac41633..246bf19c6 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemeResources.java @@ -25,6 +25,8 @@ import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; /** + * JAX-RS application providing the resources/assets of a theme (images, CSS + * files, etc) under the {@code /@themes} URL. * * @author Jens Pelzetter */ @@ -37,7 +39,5 @@ public class ThemeResources extends Application { classes.add(ThemeResourceProvider.class); return classes; } - - - + } diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java index 999bb7630..70809320e 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/ThemesMvc.java @@ -18,7 +18,6 @@ */ package org.libreccm.theming.mvc; -import org.libreccm.core.UnexpectedErrorException; import org.libreccm.sites.Site; import org.libreccm.sites.SiteRepository; import org.libreccm.theming.ThemeInfo; @@ -30,7 +29,6 @@ import org.libreccm.theming.manifest.ThemeTemplate; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; @@ -42,6 +40,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; /** + * Main integration point for MVC application with the theme system. * * @author Jens Pelzetter */ @@ -60,6 +59,16 @@ public class ThemesMvc { @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, @@ -136,6 +145,13 @@ public class ThemesMvc { ); } + /** + * 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); @@ -154,6 +170,16 @@ public class ThemesMvc { 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, @@ -187,6 +213,15 @@ public class ThemesMvc { } } + /** + * 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"); @@ -195,6 +230,18 @@ public class ThemesMvc { } } + /** + * Helper method for parsing the {@code preview} query parameter. The + * {@code preview} query parameter allows it to test the draft version of a + * theme. + * + * @param uriInfo Information about the current URI. + * + * @return If the value of the parameter is {@code theme} or {@code all} + * {@link ThemeVersion#DRAFT} is returned. If the query parameter is + * not present or has another value, {@link ThemeVersion#LIVE} is + * returned. + */ private ThemeVersion parsePreviewParam(final UriInfo uriInfo) { if (uriInfo.getQueryParameters().containsKey("preview")) { final List values = uriInfo diff --git a/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java b/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java new file mode 100644 index 000000000..cc845360e --- /dev/null +++ b/ccm-core/src/main/java/org/libreccm/theming/mvc/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +/** + * Integration of the Theming System with Jakarta EE MVC. + */ +package org.libreccm.theming.mvc; From af4873abfe13638b0690d2d47c35a0b1acd9f9ce Mon Sep 17 00:00:00 2001 From: Jens Pelzetter Date: Fri, 8 Jan 2021 19:53:58 +0100 Subject: [PATCH 11/12] =?UTF-8?q?Minimales=20theme=20f=C3=BCr=20login?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mvc/freemarker/FreemarkerViewEngine.java | 3 - .../libreccm/theming/StaticThemeProvider.java | 33 +++-- .../themes/ccm-freemarker/header.html.ftl | 1 + .../themes/ccm-freemarker/images/libreccm.png | Bin 0 -> 58716 bytes .../ccm-freemarker/login/login-form.html.ftl | 43 +++---- .../login/login-form.html_1.ftl | 39 ++++++ .../themes/ccm-freemarker/main.html.ftl | 25 ++++ .../resources/themes/ccm-freemarker/style.css | 117 ++++++------------ .../themes/ccm-freemarker/theme-index.json | 11 ++ 9 files changed, 159 insertions(+), 113 deletions(-) create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/header.html.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/images/libreccm.png create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html_1.ftl create mode 100644 ccm-core/src/main/resources/themes/ccm-freemarker/main.html.ftl diff --git a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java index 910e74084..19d17d176 100644 --- a/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java +++ b/ccm-core/src/main/java/org/libreccm/mvc/freemarker/FreemarkerViewEngine.java @@ -29,7 +29,6 @@ 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.freemarker.FreemarkerThemeProcessor; import org.libreccm.theming.utils.L10NUtils; import org.libreccm.theming.utils.SettingsUtils; import org.libreccm.theming.utils.TextUtils; @@ -291,7 +290,6 @@ public class FreemarkerViewEngine extends ViewEngineBase { @Override public Object exec(final List arguments) throws TemplateModelException { - if (arguments.isEmpty()) { throw new TemplateModelException("No string to localize."); } @@ -318,7 +316,6 @@ public class FreemarkerViewEngine extends ViewEngineBase { @Override public Object exec(final List arguments) throws TemplateModelException { - if (arguments.size() == 2) { final String text = ((TemplateScalarModel) arguments.get(0)) .getAsString(); diff --git a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java index ed89e5afd..0c03cec1f 100644 --- a/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java +++ b/ccm-core/src/main/java/org/libreccm/theming/StaticThemeProvider.java @@ -39,6 +39,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -53,25 +54,31 @@ import javax.json.JsonObject; import javax.json.JsonReader; /** - * Implementation of {@link ThemeProvider} for serves themes stored in the + * Implementation of {@link ThemeProvider} for serves themes stored in the * classpath. - * + * * @author Jens Pelzetter */ @RequestScoped public class StaticThemeProvider implements ThemeProvider { private static final long serialVersionUID = 1L; + private static final Logger LOGGER = LogManager.getLogger( StaticThemeProvider.class); private static final String THEMES_DIR = "/themes"; + private static final String THEMES_PACKAGE = "themes"; + private static final String THEME_MANIFEST_JSON_PATH = THEMES_DIR + "/%s/theme.json"; + private static final String THEME_MANIFEST_XML_PATH = THEMES_DIR + "/%s/theme.xml"; + private static final String THEME_MANIFEST_JSON = "theme.json"; + private static final String THEME_MANIFEST_XML = "theme.xml"; @Inject @@ -84,7 +91,7 @@ public class StaticThemeProvider implements ThemeProvider { public String getName() { return "StaticThemeProvider"; } - + @Override public List getThemes() { @@ -472,18 +479,26 @@ public class StaticThemeProvider implements ThemeProvider { final String fileName = path.get(0); - final Optional fileData = currentDirectory + final Optional fileDataResult = currentDirectory .stream() .map(value -> (JsonObject) value) .filter(value -> filterFileData(value, fileName)) .findAny(); if (path.size() == 1) { - return fileData; + return fileDataResult; } else { - - if (fileData.get().getBoolean("isDirectory")) { - return findFile(path.subList(1, path.size()), - fileData.get().getJsonArray("files")); + final JsonObject fileData = fileDataResult + .orElseThrow( + () -> new NoSuchElementException( + String.format( + "File %s not found.", path + ) + ) + ); + if (fileData.getBoolean("isDirectory")) { + return findFile( + path.subList(1, path.size()), fileData.getJsonArray("files") + ); } else { return Optional.empty(); } diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/header.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/header.html.ftl new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/header.html.ftl @@ -0,0 +1 @@ + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/images/libreccm.png b/ccm-core/src/main/resources/themes/ccm-freemarker/images/libreccm.png new file mode 100644 index 0000000000000000000000000000000000000000..676ec21370fca5626ec16cf7ddf3845c0d897c09 GIT binary patch literal 58716 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 Cd$Vl- literal 0 HcmV?d00001 diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl index d4052f387..40cc805ab 100644 --- a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html.ftl @@ -1,30 +1,28 @@ - - - - Category page - - - -
-

${LoginMessages['login.title']}

+<#import "../main.html.ftl" as main> + +<@main.ccm_main> +

${LoginMessages['login.title']}

<#if (loginFailed)>
${LoginMessages['login.errors.failed']}
- - - - - - +
+ + +
+
+ + +
@@ -33,7 +31,4 @@ ${LoginMessages['login.submit']} -
- <#include "../footer.html.ftl"> - - + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html_1.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html_1.ftl new file mode 100644 index 000000000..d4052f387 --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/login/login-form.html_1.ftl @@ -0,0 +1,39 @@ + + + + Category page + + + +
+

${LoginMessages['login.title']}

+ <#if (loginFailed)> +
+ ${LoginMessages['login.errors.failed']} +
+ +
+ + + + + + + + + +
+
+ <#include "../footer.html.ftl"> + + diff --git a/ccm-core/src/main/resources/themes/ccm-freemarker/main.html.ftl b/ccm-core/src/main/resources/themes/ccm-freemarker/main.html.ftl new file mode 100644 index 000000000..45e8ae70a --- /dev/null +++ b/ccm-core/src/main/resources/themes/ccm-freemarker/main.html.ftl @@ -0,0 +1,25 @@ +<#macro ccm_main scripts=[]> + + + Category page + + <#list scripts as script> +