diff --git a/ccm-cms/src/main/java/org/librecms/pages/PagesController.java b/ccm-cms/src/main/java/org/librecms/pages/PagesController.java new file mode 100644 index 000000000..12323ead4 --- /dev/null +++ b/ccm-cms/src/main/java/org/librecms/pages/PagesController.java @@ -0,0 +1,559 @@ +/* + * 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.librecms.pages; + +import com.arsdigita.kernel.KernelConfig; + +import org.libreccm.categorization.Category; +import org.libreccm.categorization.CategoryManager; +import org.libreccm.categorization.CategoryRepository; +import org.libreccm.configuration.ConfigurationManager; +import org.libreccm.l10n.GlobalizationHelper; +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.mvc.ThemesMvc; +import org.librecms.contentsection.ContentItemVersion; +import org.librecms.pages.models.CategoryModel; +import org.librecms.pages.models.SiteInfoModel; + +import java.net.URI; +import java.util.Collections; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.mvc.Controller; +import javax.transaction.Transactional; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * + * @author Jens Pelzetter + */ +@Controller +@RequestScoped +public class PagesController { + + @Inject + private CategoryManager categoryManager; + + @Inject + private CategoryModel categoryModel; + + @Inject + private CategoryRepository categoryRepo; + + @Inject + private ConfigurationManager confManager; + + @Inject + private GlobalizationHelper globalizationHelper; + + @Inject + private PageManager pageManager; + + @Inject + private PagesRepository pagesRepo; + + @Inject + private SiteInfoModel siteInfoModel; + + @Inject + private SiteRepository siteRepo; + + @Inject + private Themes themes; + + @Inject + private ThemesMvc themesMvc; + + private Locale defaultLocale; + + @PostConstruct + private void init() { + final KernelConfig kernelConfig = confManager + .findConfiguration(KernelConfig.class); + defaultLocale = kernelConfig.getDefaultLocale(); + } + + @GET + @Path("/") + @Transactional(Transactional.TxType.REQUIRED) + public Response redirectToIndexPage(@Context final UriInfo uriInfo) { + + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + final Category category = getCategory(domain, pages, "/"); + final String language = determineLanguage(category); + + final String indexPage = String.format("/index.%s.html", language); + final URI uri = uriInfo.getBaseUriBuilder().path(indexPage).build(); + return Response.temporaryRedirect(uri).build(); + } + + @GET + @Path("/{name:[\\w\\-]+}") + @Transactional(Transactional.TxType.REQUIRED) + public Response getRootPage( + @Context final UriInfo uriInfo, + @PathParam("name") final String itemName) { + + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + final Category category = getCategory(domain, pages, "/"); + final String language = determineLanguage(category); + + final String itemPage = String.format( + "/%s.%s.html", itemName, language + ); + final URI uri = uriInfo.getBaseUriBuilder().path(itemPage).build(); + return Response.temporaryRedirect(uri).build(); + } + + @GET + @Path("/{name:[\\w\\-]+}.html") + @Transactional(Transactional.TxType.REQUIRED) + public Response getRootPageAsHtml( + @Context final UriInfo uriInfo, + @PathParam("name") final String itemName) { + + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + final Category category = getCategory(domain, pages, "/"); + final String language = determineLanguage(category); + + final String itemPage = String.format( + "/%s.%s.html", itemName, language + ); + final String path = uriInfo + .getPath() + .replace(String.format("%s.html", itemName), itemPage); + + final URI uri = uriInfo.getBaseUriBuilder().path(path).build(); + return Response.temporaryRedirect(uri).build(); + } + + @GET + @Path("/{name:[\\w\\-]+}.{lang:\\w+}.html") + @Produces("text/html") + @Transactional(Transactional.TxType.REQUIRED) + public String getRootPageAsHtml( + @Context + final UriInfo uriInfo, + @PathParam("name") + final String itemName, + @PathParam("lang") + final String language, + @QueryParam("theme") + @DefaultValue("--DEFAULT--") + final String theme, + @QueryParam("preview") + @DefaultValue("") + final String preview + ) { + final Versions versions = generateFromPreviewParam(preview); + + final Site site = getSite(uriInfo); + + return themesMvc.getMvcTemplate(uriInfo, "pages", "page"); + } + + /** + * Retrieve the item page for a category and the content item associated + * with the category and identified by {@code itemName}. + * + * Redirects to + * {@link #getPageAsHtml(javax.ws.rs.core.UriInfo, java.lang.String, java.lang.String)}. + * + * @param uriInfo + * @param page + * @param itemName + * + * @return + */ + @GET + @Path("/{page:[\\w\\-/]+}/{name:[\\w\\-]+}") + @Transactional(Transactional.TxType.REQUIRED) + public Response getPage( + @Context final UriInfo uriInfo, + @PathParam("page") final String page, + @PathParam("name") final String itemName + ) { + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + final Category category = getCategory(domain, pages, page); + final String language = determineLanguage(category); + + final String redirectTo; + if (uriInfo.getPath().endsWith("/")) { + redirectTo = String.format( + "%sindex.%s.html", uriInfo.getPath(), language + ); + } else { + final String itemPath = String.format( + "%s.%s.html", itemName, language + ); + redirectTo = uriInfo.getPath().replace(itemName, itemPath); + } + + final URI uri = uriInfo.getBaseUriBuilder().path(redirectTo).build(); + return Response.temporaryRedirect(uri).build(); + } + + /** + * Retrieve the item page for a category and the content item associated + * with the category and identified by {@code itemName}. Redirects to + * {@link #getPageAsHtml(javax.ws.rs.core.UriInfo, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String)}. + * + * @param uriInfo + * @param page + * @param itemName + * + * @return + */ + @GET + @Path("/{page:[\\w\\-/]+}/{name:[\\w\\-]+}.html") + @Transactional(Transactional.TxType.REQUIRED) + public Response getPageAsHtml( + @Context final UriInfo uriInfo, + @PathParam("page") final String page, + @PathParam("name") final String itemName + ) { + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + final Category category = getCategory(domain, pages, page); + final String language = determineLanguage(category); + + final String redirectTo; + if (uriInfo.getPath().endsWith("/")) { + redirectTo = String.format( + "%sindex.%s.html", uriInfo.getPath(), language + ); + } else { + final String itemPath = String.format( + "%s.%s.html", itemName, language + ); + redirectTo = uriInfo.getPath().replace(itemName, itemPath); + } + + final URI uri = uriInfo.getBaseUriBuilder().path(redirectTo).build(); + return Response.temporaryRedirect(uri).build(); + } + + /** + * Retrieve the item page as HTML for a category and the content item + * associated with the category and identified by {@code itemName}. + * + * @param uriInfo + * @param page + * @param itemName + * @param language + * @param theme + * @param preview + * + * @return + */ + @GET + @Path("/{page:[\\w\\-/]+}/{name:[\\w\\-]+}.{lang:\\w+}.html") + @Produces("text/html") + @Transactional(Transactional.TxType.REQUIRED) + public String getPageAsHtml( + @Context + final UriInfo uriInfo, + @PathParam("page") + final String page, + @PathParam("name") + final String itemName, + @PathParam("lang") + final String language, + @QueryParam("theme") + @DefaultValue("--DEFAULT--") + final String theme, + @QueryParam("preview") + @DefaultValue("") + final String preview + ) { + final Versions versions = generateFromPreviewParam(preview); + + final Site site = getSite(uriInfo); + return themesMvc.getMvcTemplate(uriInfo, "pages", "page"); + } + + private Site getSite(final UriInfo uriInfo) { + final String domain = Objects + .requireNonNull(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 Pages getPages(final String domain) { + return pagesRepo + .findPagesForSite(domain) + .orElseThrow( + () -> new NotFoundException( + String.format( + "No Pages for domain \"%s\" available.", + domain + ) + ) + ); + } + + private Category getCategory( + final String domain, final Pages pages, final String pagePath + ) { + return categoryRepo + .findByPath(pages.getCategoryDomain(), pagePath) + .orElseThrow( + () -> new NotFoundException( + String.format( + "No Page for path \"%s\" in site \"%s\"", + pagePath, + domain + ) + ) + ); + } + + private String determineLanguage(final Category category) { + final Locale negoiatedLocale = globalizationHelper + .getNegotiatedLocale(); + + final String language; + if (category.getTitle().hasValue(negoiatedLocale)) { + language = negoiatedLocale.toString(); + } else if (category.getTitle().hasValue(defaultLocale)) { + language = defaultLocale.toString(); + } else { + throw new NotFoundException(); + } + + return language; + } + + 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 Page findPage( + final UriInfo uriInfo, final String pagePath, final String language + ) { + Objects.requireNonNull(uriInfo); + Objects.requireNonNull(pagePath); + + final Site site = getSite(uriInfo); + final String domain = uriInfo.getBaseUri().getHost(); + final Pages pages = getPages(domain); + + final Category category = getCategory(domain, pages, pagePath); + // disabled. Needs to be decided if the available languages of the + // index item or of the category are + // used to decide if a NotFoundException is thrown. +// if (!category.getTitle().hasValue(locale)) { +// throw new NotFoundException(); +// } + final Locale locale = new Locale(language); + globalizationHelper.setSelectedLocale(locale); + + final KernelConfig kernelConfig = confManager + .findConfiguration(KernelConfig.class); + + siteInfoModel.setAvailableLanguages( + kernelConfig + .getSupportedLanguages() + .stream() + .sorted() + .collect(Collectors.toList()) + ); + siteInfoModel.setDomain(site.getDomainOfSite()); + siteInfoModel.setHost(uriInfo.getBaseUri().getHost()); + siteInfoModel.setName(site.getDisplayName()); + + categoryModel.setCategory(category); + categoryModel.setCategoryPath( + categoryManager.getCategoryPath(category) + ); + + return pageManager.findPageForCategory(category); + } + + /** + * Parse the value of the {@code preview} query parameter. + * + * @param value The value of the {@code preview} query parameter to parse. + * + * @return If the provided value is {@code all} a {@link Versions} object + * with all versions set to the draft versions is created and + * returned. If the provided value is {@code null} or empty a + * {@link Versions} object with fields set the the live versions is + * returned. Otherwise the values is split into tokens (separated by + * commas). The values of the returned {@link Versions} depend on + * presence of certain tokens. At the moment to following tokens are + * recognised: + *
+ *
{@code content}
+ *
{@link Versions#contentVersion} is set to + * {@link ContentItemVersion#DRAFT}
+ *
{@code theme}
+ *
{@link Versions#themeVersion} is set to + * {@link ThemeVersion#DRAFT}.
+ *
+ * + */ + private Versions generateFromPreviewParam(final String value) { + if (value == null || value.isEmpty() || value.matches("\\s*")) { + return new Versions(ContentItemVersion.LIVE, + ThemeVersion.LIVE); + } else if ("all".equals(value.toLowerCase(Locale.ROOT))) { + return new Versions(ContentItemVersion.DRAFT, + ThemeVersion.DRAFT); + } else { + final Set values = new HashSet<>(); + Collections.addAll(values, + value.toLowerCase(Locale.ROOT).split(",")); + + final Versions result = new Versions(); + if (values.contains("content")) { + result.setContentVersion(ContentItemVersion.DRAFT); + } + if (values.contains("theme")) { + result.setThemeVersion(ThemeVersion.DRAFT); + } + return result; + } + } + + /** + * Encapsulate the result of converting the value of the {@code preview} + * query parameter. + */ + private class Versions { + + /** + * Version of content to use + */ + private ContentItemVersion contentVersion; + + /** + * Version of theme to use. + */ + private ThemeVersion themeVersion; + + /** + * Creates a new {@code Versions} object with all fields set to + * {@code live} versions. + */ + public Versions() { + this.contentVersion = ContentItemVersion.LIVE; + this.themeVersion = ThemeVersion.LIVE; + } + + /** + * Create a new {@code Versions} object with the provided parameters. + * + * @param contentVersion + * @param themeVersion + */ + public Versions( + final ContentItemVersion contentVersion, + final ThemeVersion themeVersion + ) { + this.contentVersion = contentVersion; + this.themeVersion = themeVersion; + } + + public ContentItemVersion getContentVersion() { + return contentVersion; + } + + public void setContentVersion(final ContentItemVersion contentVersion) { + this.contentVersion = contentVersion; + } + + public ThemeVersion getThemeVersion() { + return themeVersion; + } + + public void setThemeVersion(final ThemeVersion themeVersion) { + this.themeVersion = themeVersion; + } + + } + +} diff --git a/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java b/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java index dd27e25ad..2c2a526de 100644 --- a/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java +++ b/ccm-cms/src/main/java/org/librecms/pages/models/CategoryModel.java @@ -19,7 +19,7 @@ package org.librecms.pages.models; import org.libreccm.categorization.Category; -import org.librecms.pages.PagesRouter; +import org.librecms.pages.PagesController; import java.io.Serializable; @@ -29,7 +29,7 @@ import javax.inject.Named; /** * Model for MVC applications providing access to the current category. This * model MUST be initalized by the calling application (for example - * {@link PagesRouter} with the current category. + * {@link PagesController} with the current category. * * @author Jens Pelzetter */