
com.semanticcms.core.servlet.SemanticCMS Maven / Gradle / Ivy
/*
* semanticcms-core-servlet - Java API for modeling web page content and relationships in a Servlet environment.
* Copyright (C) 2014, 2015, 2016 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of semanticcms-core-servlet.
*
* semanticcms-core-servlet 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 3 of the License, or
* (at your option) any later version.
*
* semanticcms-core-servlet 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 semanticcms-core-servlet. If not, see .
*/
package com.semanticcms.core.servlet;
import com.aoindustries.servlet.PropertiesUtils;
import com.aoindustries.servlet.http.Dispatcher;
import com.aoindustries.util.WrappedException;
import com.aoindustries.xml.XmlUtils;
import com.semanticcms.core.model.Book;
import com.semanticcms.core.model.PageRef;
import com.semanticcms.core.model.ParentRef;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* The SemanticCMS application context.
*
* TODO: Consider custom EL resolver for this variable: http://stackoverflow.com/questions/5016965/how-to-add-a-custom-variableresolver-in-pure-jsp
*/
public class SemanticCMS {
//
static final String ATTRIBUTE_NAME = "semanticCMS";
private static class InstanceLock {}
private static final InstanceLock instanceLock = new InstanceLock();
/**
* Gets the SemanticCMS instance, creating it if necessary.
*/
public static SemanticCMS getInstance(ServletContext servletContext) {
try {
synchronized(instanceLock) {
SemanticCMS semanticCMS = (SemanticCMS)servletContext.getAttribute(SemanticCMS.ATTRIBUTE_NAME);
if(semanticCMS == null) {
semanticCMS = new SemanticCMS(servletContext);
servletContext.setAttribute(SemanticCMS.ATTRIBUTE_NAME, semanticCMS);
}
return semanticCMS;
}
} catch(IOException e) {
throw new WrappedException(e);
} catch(SAXException e) {
throw new WrappedException(e);
} catch(ParserConfigurationException e) {
throw new WrappedException(e);
} catch(XPathExpressionException e) {
throw new WrappedException(e);
}
}
private final ServletContext servletContext;
private SemanticCMS(ServletContext servletContext) throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
this.servletContext = servletContext;
this.demoMode = Boolean.parseBoolean(servletContext.getInitParameter(DEMO_MODE_INIT_PARAM));
int numProcessors = Runtime.getRuntime().availableProcessors();
this.concurrentSubrequests =
numProcessors > 1
&& Boolean.parseBoolean(servletContext.getInitParameter(CONCURRENT_SUBREQUESTS_INIT_PARAM))
;
this.rootBook = initBooks();
this.executors = new Executors();
}
/**
* Called when the context is shutting down.
*/
void destroy() {
synchronized(instanceLock) {
servletContext.removeAttribute(SemanticCMS.ATTRIBUTE_NAME);
}
}
//
//
private static final String DEMO_MODE_INIT_PARAM = "com.semanticcms.core.servlet.SemanticCMS.demoMode";
private final boolean demoMode;
/**
* When true, a cursory attempt will be made to hide sensitive information for demo mode.
*/
public boolean getDemoMode() {
return demoMode;
}
//
//
// See https://docs.oracle.com/javase/tutorial/jaxp/dom/validating.html
private static final String BOOKS_XML_RESOURCE = "/WEB-INF/books.xml";
private static final String BOOKS_XML_SCHEMA_RESOURCE = "books-1.0.xsd";
private static final String MISSING_BOOK_TAG_NAME = "missingBook";
private static final String BOOK_TAG_NAME = "book";
private static final String PARENT_TAG_NAME = "parent";
private static final String ROOT_BOOK_ATTRIBUTE_NAME = "rootBook";
private final Map books = new LinkedHashMap();
private final Set missingBooks = new LinkedHashSet();
private final Book rootBook;
private Book initBooks() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
Document booksXml;
{
InputStream schemaIn = SemanticCMS.class.getResourceAsStream(BOOKS_XML_SCHEMA_RESOURCE);
if(schemaIn == null) throw new IOException("Schema not found: " + BOOKS_XML_SCHEMA_RESOURCE);
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setValidating(true);
dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaLanguage", XMLConstants.W3C_XML_SCHEMA_NS_URI);
dbf.setAttribute("http://java.sun.com/xml/jaxp/properties/schemaSource", schemaIn);
DocumentBuilder db = dbf.newDocumentBuilder();
InputStream booksXmlIn = servletContext.getResource(BOOKS_XML_RESOURCE).openStream();
if(booksXmlIn == null) throw new IOException(BOOKS_XML_RESOURCE + " not found");
try {
booksXml = db.parse(booksXmlIn);
} finally {
booksXmlIn.close();
}
} finally {
schemaIn.close();
}
}
org.w3c.dom.Element booksElem = booksXml.getDocumentElement();
// Load missingBooks
for(org.w3c.dom.Element missingBookElem : XmlUtils.iterableChildElementsByTagName(booksElem, MISSING_BOOK_TAG_NAME)) {
String name = missingBookElem.getAttribute("name");
if(!missingBooks.add(name)) throw new IllegalStateException(BOOKS_XML_RESOURCE+ ": Duplicate value for \"" + MISSING_BOOK_TAG_NAME + "\": " + name);
}
// Load books
String rootBookName = booksElem.getAttribute(ROOT_BOOK_ATTRIBUTE_NAME);
if(rootBookName == null || rootBookName.isEmpty()) throw new IllegalStateException(BOOKS_XML_RESOURCE + ": \"" + ROOT_BOOK_ATTRIBUTE_NAME + "\" not found");
for(org.w3c.dom.Element bookElem : XmlUtils.iterableChildElementsByTagName(booksElem, BOOK_TAG_NAME)) {
String name = bookElem.getAttribute("name");
if(missingBooks.contains(name)) throw new IllegalStateException(BOOKS_XML_RESOURCE + ": Book also listed in \"" + MISSING_BOOK_TAG_NAME+ "\": " + name);
Set parentRefs = new LinkedHashSet();
for(org.w3c.dom.Element parentElem : XmlUtils.iterableChildElementsByTagName(bookElem, PARENT_TAG_NAME)) {
String parentBookName = parentElem.getAttribute("book");
String parentPage = parentElem.getAttribute("page");
String parentShortTitle = parentElem.hasAttribute("shortTitle") ? parentElem.getAttribute("shortTitle") : null;
Book parentBook = books.get(parentBookName);
if(parentBook == null) {
throw new IllegalStateException(BOOKS_XML_RESOURCE + ": parent book not found (loading order currently matters): " + parentBookName);
}
parentRefs.add(new ParentRef(new PageRef(parentBook, parentPage), parentShortTitle));
}
if(name.equals(rootBookName)) {
if(!parentRefs.isEmpty()) {
throw new IllegalStateException(BOOKS_XML_RESOURCE + ": \"" + ROOT_BOOK_ATTRIBUTE_NAME + "\" may not have any parents: " + rootBookName);
}
} else {
if(parentRefs.isEmpty()) {
throw new IllegalStateException(BOOKS_XML_RESOURCE + ": Non-root books must have at least one parent: " + name);
}
}
books.put(
name,
new Book(
name,
bookElem.getAttribute("cvsworkDirectory"),
Boolean.valueOf(bookElem.getAttribute("allowRobots")),
parentRefs,
PropertiesUtils.loadFromResource(servletContext, ("/".equals(name) ? "" : name) + "/book.properties")
)
);
}
// Load rootBook
Book newRootBook = books.get(rootBookName);
if(newRootBook == null) throw new AssertionError();
// Successful book load
return newRootBook;
}
public Map getBooks() {
return Collections.unmodifiableMap(books);
}
public Set getMissingBooks() {
return Collections.unmodifiableSet(missingBooks);
}
/**
* Gets the root book as configured in /WEB-INF/books.properties
*/
public Book getRootBook() {
return rootBook;
}
/**
* Gets the book for the provided context-relative servlet path or null
if no book configured at that path.
* The book with the longest prefix match is used.
* The servlet path must begin with a slash (/).
*/
public Book getBook(String servletPath) {
if(servletPath.charAt(0) != '/') throw new IllegalArgumentException("Invalid servletPath: " + servletPath);
Book longestPrefixBook = null;
int longestPrefixLen = -1;
for(Book book : getBooks().values()) {
String prefix = book.getPathPrefix();
int prefixLen = prefix.length();
if(
prefixLen > longestPrefixLen
&& servletPath.startsWith(prefix)
) {
longestPrefixBook = book;
longestPrefixLen = prefixLen;
}
}
return longestPrefixBook;
}
/**
* Gets the book for the provided request or null
if no book configured at the current request path.
*/
public Book getBook(HttpServletRequest request) {
return getBook(Dispatcher.getCurrentPagePath(request));
}
//
//
/**
* The parameter name used for views.
*/
public static final String VIEW_PARAM = "view";
/**
* The default view is the content view and will have the empty view name.
*/
public static final String DEFAULT_VIEW_NAME = "content";
private static class ViewsLock {}
private final ViewsLock viewsLock = new ViewsLock();
/**
* The views by name in order added.
*/
private final Map viewsByName = new LinkedHashMap();
private static final Set viewGroups = Collections.unmodifiableSet(EnumSet.allOf(View.Group.class));
/**
* Gets all view groups.
*/
public Set getViewGroups() {
return viewGroups;
}
/**
* Gets the views in order added.
*/
public Map getViewsByName() {
return Collections.unmodifiableMap(viewsByName);
}
/**
* The views in order.
*/
private final SortedSet views = new TreeSet();
/**
* Gets the views, ordered by view group then display.
*
* @see View#compareTo(com.semanticcms.core.servlet.View)
*/
public SortedSet getViews() {
return Collections.unmodifiableSortedSet(views);
}
/**
* Registers a new view.
*
* @throws IllegalStateException if a view is already registered with the name.
*/
public void addView(View view) throws IllegalStateException {
String name = view.getName();
synchronized(viewsLock) {
if(viewsByName.containsKey(name)) throw new IllegalStateException("View already registered: " + name);
if(viewsByName.put(name, view) != null) throw new AssertionError();
if(!views.add(view)) throw new AssertionError();
}
}
//
//
/**
* The components that are currently registered.
*/
private final List components = new CopyOnWriteArrayList();
/**
* Gets all components in an undefined, but consistent (within a single run) ordering.
*/
public List getComponents() {
return components;
}
/**
* Registers a new component.
*/
public void addComponent(Component component) {
components.add(component);
// Order the components by classname, just to have a consistent output
// independent of the order components happened to be registered.
Collections.sort(
components,
new Comparator() {
@Override
public int compare(Component o1, Component o2) {
return o1.getClass().getName().compareTo(o2.getClass().getName());
}
}
);
}
//
//
/**
* The default theme is used when no other theme is registered.
*/
public static final String DEFAULT_THEME_NAME = "base";
/**
* The themes in order added.
*/
private final Map themes = new LinkedHashMap();
/**
* Gets the themes, in the order added.
*/
public Map getThemes() {
synchronized(themes) {
// Not returning a copy since themes are normally only registered on app start-up.
return Collections.unmodifiableMap(themes);
}
}
/**
* Registers a new theme.
*
* @throws IllegalStateException if a theme is already registered with the name.
*/
public void addTheme(Theme theme) throws IllegalStateException {
String name = theme.getName();
synchronized(themes) {
if(themes.containsKey(name)) throw new IllegalStateException("Theme already registered: " + name);
if(themes.put(name, theme) != null) throw new AssertionError();
}
}
//
//
/**
* The CSS links in the order added.
*/
private final Set cssLinks = new LinkedHashSet();
/**
* Gets the CSS links, in the order added.
*/
public Set getCssLinks() {
synchronized(cssLinks) {
// Not returning a copy since CSS links are normally only registered on app start-up.
return Collections.unmodifiableSet(cssLinks);
}
}
/**
* Registers a new CSS link.
*
* @throws IllegalStateException if the link is already registered.
*/
public void addCssLink(String cssLink) throws IllegalStateException {
synchronized(cssLinks) {
if(!cssLinks.add(cssLink)) throw new IllegalStateException("CSS link already registered: " + cssLink);
}
}
//
//
/**
* The scripts in the order added.
*/
private final Map scripts = new LinkedHashMap();
/**
* Gets the scripts, in the order added.
*/
public Map getScripts() {
synchronized(scripts) {
// Not returning a copy since scripts are normally only registered on app start-up.
return Collections.unmodifiableMap(scripts);
}
}
/**
* Registers a new script. When a script is added multiple times,
* the src must be consistent between adds. Also, a src may not be
* added under different names.
*
* @param name the name of the script, independent of version and src
* @param src the src of the script.
*
* @throws IllegalStateException if the script already registered but with a different src.
*/
public void addScript(String name, String src) throws IllegalStateException {
synchronized(scripts) {
String existingSrc = scripts.get(name);
if(existingSrc != null) {
if(!src.equals(existingSrc)) {
throw new IllegalStateException(
"Script already registered but with a different src:"
+ " name=" + name
+ " src=" + src
+ " existingSrc=" + existingSrc
);
}
} else {
// Make sure src not provided by another script
if(scripts.values().contains(src)) {
throw new IllegalArgumentException("Non-unique global script src: " + src);
}
if(scripts.put(name, src) != null) throw new AssertionError();
}
}
}
//
//
/**
* The head includes in the order added.
*/
private final Set headIncludes = new LinkedHashSet();
/**
* Gets the head includes, in the order added.
*/
public Set getHeadIncludes() {
synchronized(headIncludes) {
// Not returning a copy since head includes are normally only registered on app start-up.
return Collections.unmodifiableSet(headIncludes);
}
}
/**
* Registers a new head include.
*
* @throws IllegalStateException if the link is already registered.
*/
public void addHeadInclude(String headInclude) throws IllegalStateException {
synchronized(headIncludes) {
if(!headIncludes.add(headInclude)) throw new IllegalStateException("headInclude already registered: " + headInclude);
}
}
//
//
/**
* Resolves the link CSS class for the given types of elements.
*/
public static interface LinkCssClassResolver {
/**
* Gets the CSS class to use in links to the given element.
* When null is returned, any resolvers for super classes will also be invoked.
*
* @return The CSS class name or {@code null} when none configured for the provided element.
*/
String getCssLinkClass(E element);
}
/**
* The CSS classes used in links.
*/
private final Map,LinkCssClassResolver>> linkCssClassResolverByElementType = new LinkedHashMap,LinkCssClassResolver>>();
/**
* Gets the CSS class to use in links to the given element.
* Also looks for match on parent classes up to and including Element itself.
*
* @return The CSS class or {@code null} when element is null or no class registered for it or any super class.
*
* @see #getLinkCssClass(java.lang.Class)
*/
public String getLinkCssClass(E element) {
if(element == null) return null;
Class extends com.semanticcms.core.model.Element> elementType = element.getClass();
synchronized(linkCssClassResolverByElementType) {
while(true) {
@SuppressWarnings("unchecked")
LinkCssClassResolver super E> linkCssClassResolver = (LinkCssClassResolver super E>)linkCssClassResolverByElementType.get(elementType);
if(linkCssClassResolver != null) {
String linkCssClass = linkCssClassResolver.getCssLinkClass(element);
if(linkCssClass != null) return linkCssClass;
}
if(elementType == com.semanticcms.core.model.Element.class) return null;
elementType = elementType.getSuperclass().asSubclass(com.semanticcms.core.model.Element.class);
}
}
}
/**
* Registers a new CSS resolver to use in link to the given type of element.
*
* @throws IllegalStateException if the element type is already registered.
*/
public void addLinkCssClassResolver(
Class elementType,
LinkCssClassResolver super E> cssLinkClassResolver
) throws IllegalStateException {
synchronized(linkCssClassResolverByElementType) {
if(linkCssClassResolverByElementType.containsKey(elementType)) throw new IllegalStateException("Link CSS class already registered: " + elementType);
if(linkCssClassResolverByElementType.put(elementType, cssLinkClassResolver) != null) throw new AssertionError();
}
}
/**
* Registers a new CSS class to use in link to the given type of element.
*
* @throws IllegalStateException if the element type is already registered.
*/
public void addLinkCssClass(
Class elementType,
final String cssLinkClass
) throws IllegalStateException {
addLinkCssClassResolver(
elementType,
new LinkCssClassResolver() {
@Override
public String getCssLinkClass(E element) {
return cssLinkClass;
}
}
);
}
//
//
/**
* Resolves the list item CSS class for the given types of nodes.
*/
public static interface ListItemCssClassResolver {
/**
* Gets the CSS class to use in list items to the given node.
* When null is returned, any resolvers for super classes will also be invoked.
*
* @return The CSS class name or {@code null} when none configured for the provided node.
*/
String getListItemCssClass(N node);
}
/**
* The CSS classes used in list items.
*/
private final Map,ListItemCssClassResolver>> listItemCssClassResolverByNodeType = new LinkedHashMap,ListItemCssClassResolver>>();
/**
* Gets the CSS class to use in list items to the given node.
* Also looks for match on parent classes up to and including Node itself.
*
* @return The CSS class or {@code null} when node is null or no class registered for it or any super class.
*
* @see #getListItemCssClass(java.lang.Class)
*/
public String getListItemCssClass(N node) {
if(node == null) return null;
Class extends com.semanticcms.core.model.Node> nodeType = node.getClass();
synchronized(listItemCssClassResolverByNodeType) {
while(true) {
@SuppressWarnings("unchecked")
ListItemCssClassResolver super N> listItemCssClassResolver = (ListItemCssClassResolver super N>)listItemCssClassResolverByNodeType.get(nodeType);
if(listItemCssClassResolver != null) {
String listItemCssClass = listItemCssClassResolver.getListItemCssClass(node);
if(listItemCssClass != null) return listItemCssClass;
}
if(nodeType == com.semanticcms.core.model.Node.class) return null;
nodeType = nodeType.getSuperclass().asSubclass(com.semanticcms.core.model.Node.class);
}
}
}
/**
* Registers a new CSS resolver to use in list items to the given type of node.
*
* @throws IllegalStateException if the node type is already registered.
*/
public void addListItemCssClassResolver(
Class nodeType,
ListItemCssClassResolver super N> listItemCssClassResolver
) throws IllegalStateException {
synchronized(listItemCssClassResolverByNodeType) {
if(listItemCssClassResolverByNodeType.containsKey(nodeType)) throw new IllegalStateException("List item CSS class already registered: " + nodeType);
if(listItemCssClassResolverByNodeType.put(nodeType, listItemCssClassResolver) != null) throw new AssertionError();
}
}
/**
* Registers a new CSS class to use in list items to the given type of node.
*
* @throws IllegalStateException if the node type is already registered.
*/
public void addListItemCssClass(
Class nodeType,
final String listItemCssClass
) throws IllegalStateException {
addListItemCssClassResolver(
nodeType,
new ListItemCssClassResolver() {
@Override
public String getListItemCssClass(N node) {
return listItemCssClass;
}
}
);
}
//
//
/**
* Initialization parameter, that when set to "true" will enable the
* concurrent subrequest processing feature. This is still experimental
* and is off by default.
*/
private static final String CONCURRENT_SUBREQUESTS_INIT_PARAM = SemanticCMS.class.getName() + ".concurrentSubrequests";
private final boolean concurrentSubrequests;
/**
* Checks if concurrent subrequests are allowed.
*/
boolean getConcurrentSubrequests() {
return concurrentSubrequests;
}
private final Executors executors;
/**
* A shared executor available to all components.
*
* @see CountConcurrencyFilter#isConcurrentProcessingRecommended(javax.servlet.ServletRequest)
* Consider selecting concurrent or sequential implementations based on overall system load.
*/
public Executors getExecutors() {
return executors;
}
//
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy