/*
* Copyright (C) 2017 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 com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.libreccm.categorization.Category;
import org.libreccm.categorization.CategoryRepository;
import org.libreccm.configuration.ConfigurationManager;
import org.libreccm.l10n.GlobalizationHelper;
import org.libreccm.pagemodel.PageModel;
import org.libreccm.pagemodel.PageModelManager;
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 java.net.URI;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
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;
import static org.librecms.pages.PagesConstants.*;
/**
* JAX-RS class providing access to the pages.
*
* @author Jens Pelzetter
*/
@RequestScoped
@Path("/")
public class PagesRouter {
@Inject
private CategoryRepository categoryRepo;
@Inject
private ConfigurationManager confManager;
@Inject
private GlobalizationHelper globalizationHelper;
@Inject
private CmsPageRenderer pageBuilder;
@Inject
private PagesRepository pagesRepo;
@Inject
private PageManager pageManager;
@Inject
private PageModelManager pageModelManager;
@Inject
private SiteRepository siteRepo;
@Inject
private Themes themes;
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("theme-version")
@DefaultValue("LIVE")
final String themeVersion,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
"/",
language,
pageModelVersion);
} else {
result = getCategoryItemPage(
uriInfo, "/", itemName, language, pageModelVersion);
}
final Site site = getSite(uriInfo);
final ThemeInfo themeInfo = getTheme(site,
theme,
themeVersion);
return themes.process(result, themeInfo);
}
@GET
@Path("/{name:[\\w\\-]+}.{lang:\\w+}.json")
@Produces("text/json")
@Transactional(Transactional.TxType.REQUIRED)
public String getRootPageAsJson(
@Context
final UriInfo uriInfo,
@PathParam("name")
final String itemName,
@PathParam("lang")
final String language,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final ObjectMapper mapper = new ObjectMapper();
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
"/",
language,
pageModelVersion);
} else {
result = getCategoryItemPage(uriInfo,
"/",
itemName,
language,
pageModelVersion);
}
try {
final String json = mapper.writeValueAsString(result);
return json;
} catch (JsonProcessingException ex) {
throw new WebApplicationException(ex);
}
}
@GET
@Path("/{name:[\\w\\-]+}.{lang:\\w+}.xml")
@Produces("text/xml")
@Transactional(Transactional.TxType.REQUIRED)
public String getRootPageAsXml(
@Context
final UriInfo uriInfo,
@PathParam("name")
final String itemName,
@PathParam("lang")
final String language,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final JacksonXmlModule xmlModule = new JacksonXmlModule();
final ObjectMapper mapper = new XmlMapper(xmlModule);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
"/",
language,
pageModelVersion);
} else {
result = getCategoryItemPage(uriInfo,
"/",
itemName,
language,
pageModelVersion);
}
try {
final String html = mapper
.writer()
.withRootName("page")
.writeValueAsString(result);
return html;
} catch (JsonProcessingException ex) {
throw new WebApplicationException(ex);
}
}
/**
* 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 themeVersion
* @param pageModelVersion
*
* @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("theme-version")
@DefaultValue("LIVE")
final String themeVersion,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
page,
language,
pageModelVersion);
} else {
result = getCategoryItemPage(uriInfo,
page,
itemName,
language,
pageModelVersion);
}
final Site site = getSite(uriInfo);
final ThemeInfo themeInfo = getTheme(site, theme, themeVersion);
return themes.process(result, themeInfo);
}
/**
* Retrieve the item page as JSON 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 pageModelVersion
*
* @return
*/
@GET
@Path("/{page:[\\w/]+}/{name:[\\w\\-]+}.{lang:\\w+}.json")
@Produces("text/json")
@Transactional(Transactional.TxType.REQUIRED)
public String getPageAsJson(
@Context
final UriInfo uriInfo,
@PathParam("page")
final String page,
@PathParam("name")
final String itemName,
@PathParam("lang")
final String language,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final ObjectMapper mapper = new ObjectMapper();
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
page,
language,
pageModelVersion);
} else {
result = getCategoryItemPage(uriInfo,
page,
itemName,
language,
pageModelVersion);
}
try {
final String json = mapper.writeValueAsString(result);
return json;
} catch (JsonProcessingException ex) {
throw new WebApplicationException(ex);
}
}
/**
* Retrieve the item page as XML 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 pageModelVersion
*
* @return
*/
@GET
@Path("/{page:[\\w/]+}/{name:[\\w\\-]+}.{lang:\\w+}.xml")
@Produces("text/xml")
@Transactional(Transactional.TxType.REQUIRED)
public String getPageAsXml(
@Context
final UriInfo uriInfo,
@PathParam("page")
final String page,
@PathParam("name")
final String itemName,
@PathParam("lang")
final String language,
@QueryParam("pagemodel-version")
@DefaultValue("LIVE")
final String pageModelVersion) {
final JacksonXmlModule xmlModule = new JacksonXmlModule();
final ObjectMapper mapper = new XmlMapper(xmlModule);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
final Map result;
if ("index".equals(itemName)) {
result = getCategoryIndexPage(uriInfo,
page,
language,
pageModelVersion);
} else {
result = getCategoryItemPage(uriInfo,
page,
itemName,
language,
pageModelVersion);
}
try {
final String html = mapper
.writer()
.withRootName("page")
.writeValueAsString(result);
return html;
} catch (JsonProcessingException ex) {
throw new WebApplicationException(ex);
}
}
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 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 String themeVersion) {
if ("--DEFAULT--".equals(theme)) {
return themes
.getTheme(site.getDefaultTheme(),
ThemeVersion.valueOf(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.valueOf(themeVersion))
.orElseThrow(() -> new WebApplicationException(
String.format("The theme \"%s\" is not available.",
theme),
Response.Status.BAD_REQUEST));
}
}
private Page getPage(final UriInfo uriInfo,
final String pagePath,
final String language,
final Map parameters) {
Objects.requireNonNull(uriInfo);
Objects.requireNonNull(pagePath);
Objects.requireNonNull(parameters);
final String domain = uriInfo.getBaseUri().getHost();
final Pages pages = getPages(domain);
final Category category = getCategory(domain, pages, pagePath);
final Locale locale = new Locale(language);
// 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();
// }
globalizationHelper.setSelectedLocale(locale);
parameters.put(PARAMETER_CATEGORY, category);
return pageManager.findPageForCategory(category);
}
private Map buildPage(
final PageModel pageModel,
final Map parameters) {
Objects.requireNonNull(pageModel);
Objects.requireNonNull(parameters);
final Map result;
if (pageModel == null) {
result = pageBuilder.renderPage(parameters);
} else {
result = pageBuilder.renderPage(pageModel, parameters);
}
return result;
}
private Map getCategoryIndexPage(
final UriInfo uriInfo,
final String pagePath,
final String language,
final String pageModelVersion) {
final Map parameters = new HashMap<>();
final Page page = getPage(uriInfo,
pagePath,
language,
parameters);
final PageModel pageModel;
if ("DRAFT".equals(pageModelVersion)) {
pageModel = pageModelManager
.getDraftVersion(page.getIndexPageModel());
} else {
pageModel = pageModelManager
.getLiveVersion(page.getIndexPageModel())
.orElseThrow(() -> new NotFoundException(String
.format("The PageModel for the index page of the category"
+ "\"%s\" is not available as live version.",
pagePath)));
}
parameters.put(PARAMETER_LANGUAGE, language);
return buildPage(pageModel, parameters);
}
private Map getCategoryItemPage(
final UriInfo uriInfo,
final String pagePath,
final String itemName,
final String language,
final String pageModelVersion) {
final Map parameters = new HashMap<>();
final Page page = PagesRouter.this.getPage(uriInfo, pagePath, language,
parameters);
final PageModel pageModel;
if ("DRAFT".equals(pageModelVersion)) {
pageModel = pageModelManager.getDraftVersion(page
.getItemPageModel());
} else {
pageModel = pageModelManager
.getLiveVersion(page.getItemPageModel())
.orElseThrow(() -> new NotFoundException(String
.format("The PageModel for the index page of the category"
+ "\"%s\" is not available as live version.",
pagePath)));
}
parameters.put(PARAMETER_ITEMNAME, itemName);
parameters.put(PARAMETER_LANGUAGE, language);
return buildPage(pageModel, parameters);
}
}