/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program 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
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.content.page;
import static ch.entwine.weblounge.common.site.Environment.Any;
import ch.entwine.weblounge.common.content.RenderException;
import ch.entwine.weblounge.common.content.page.HTMLHeadElement;
import ch.entwine.weblounge.common.content.page.Link;
import ch.entwine.weblounge.common.content.page.PagePreviewMode;
import ch.entwine.weblounge.common.content.page.PageletRenderer;
import ch.entwine.weblounge.common.content.page.Script;
import ch.entwine.weblounge.common.impl.util.config.ConfigurationUtils;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.request.CacheTag;
import ch.entwine.weblounge.common.request.RequestFlavor;
import ch.entwine.weblounge.common.request.WebloungeRequest;
import ch.entwine.weblounge.common.request.WebloungeResponse;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
/**
* This renderer implements a pagelet renderer that is backed by a Java Server
* Page.
*/
public class PageletRendererImpl extends AbstractRenderer implements PageletRenderer {
/** The logging facility */
private final Logger logger = LoggerFactory.getLogger(PageletRendererImpl.class);
/** The editor url */
protected URL editor = null;
/** The defining module */
protected Module module = null;
/** The site */
protected Site site = null;
/** The preview mode */
protected PagePreviewMode previewMode = PagePreviewMode.None;
/**
* Creates a new page template.
*/
public PageletRendererImpl() {
addFlavor(RequestFlavor.HTML);
}
/**
* Creates a new page template with the given identifier.
*
* @param identifier
* the template identifier
*/
public PageletRendererImpl(String identifier) {
this(identifier, null);
}
/**
* Creates a new page template that is backed by a Java Server Page located at
* <code>url</code>.
*
* @param identifier
* the template identifier
* @param url
* the renderer url
*/
public PageletRendererImpl(String identifier, URL url) {
super(identifier, url);
addFlavor(RequestFlavor.HTML);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#setModule(ch.entwine.weblounge.common.site.Module)
*/
public void setModule(Module module) {
if (module == null)
throw new IllegalArgumentException("Module must not be null");
this.module = module;
this.site = module.getSite();
for (HTMLHeadElement htmlHead : headers) {
htmlHead.setSite(site);
htmlHead.setModule(module);
}
if (!Any.equals(environment)) {
processURLTemplates(environment);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#setEnvironment(ch.entwine.weblounge.common.site.Environment)
*/
@Override
public void setEnvironment(Environment environment) {
if (environment == null)
throw new IllegalArgumentException("Environment must not be null");
// Is there anything we need to be doing?
if (!environment.equals(this.environment) && module != null) {
logger.debug("Processing url templates of {} with environment {}", this, environment);
processURLTemplates(environment);
}
super.setEnvironment(environment);
}
/**
* Processes both renderer and editor url by replacing templates in their
* paths with real values from the actual module.
*
* @param environment
* the environment
*
* @return <code>false</code> if the paths don't end up being real urls,
* <code>true</code> otherwise
*/
private boolean processURLTemplates(Environment environment) {
if (module == null)
throw new IllegalStateException("Module is null");
if (module.getSite() == null)
throw new IllegalArgumentException("Site is null");
// Process the renderer URL
for (Map.Entry<String, URL> entry : renderers.entrySet()) {
URL renderer = entry.getValue();
String rendererURL = ConfigurationUtils.processTemplate(renderer.toExternalForm(), module, environment);
try {
renderer = new URL(rendererURL);
renderers.put(entry.getKey(), renderer);
} catch (MalformedURLException e) {
logger.warn("Renderer url {} of pagelet {} is malformed", rendererURL, this);
return false;
}
}
// Process the editor URL
if (editor != null) {
String editorURL = ConfigurationUtils.processTemplate(editor.toExternalForm(), module, environment);
try {
editor = new URL(editorURL);
} catch (MalformedURLException e) {
logger.warn("Editor url {} of pagelet {} is malformed", editorURL, this);
return false;
}
}
// Process the head elements (scripts and stylesheet includes)
for (HTMLHeadElement headElement : headers) {
headElement.setEnvironment(environment);
}
return true;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#addHTMLHeader(ch.entwine.weblounge.common.content.page.HTMLHeadElement)
*/
@Override
public void addHTMLHeader(HTMLHeadElement header) {
if (module != null) {
if (module != null)
header.setModule(module);
if (site != null)
header.setSite(site);
if (!Any.equals(environment))
header.setEnvironment(environment);
}
super.addHTMLHeader(header);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#getModule()
*/
public Module getModule() {
return module;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#setPreviewMode(ch.entwine.weblounge.common.content.Pagelet.PagePreviewMode)
*/
public void setPreviewMode(PagePreviewMode mode) {
this.previewMode = mode;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#getPreviewMode()
*/
public PagePreviewMode getPreviewMode() {
return previewMode;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.page.AbstractRenderer#setRenderer(java.net.URL)
*/
@Override
public void setRenderer(URL renderer) {
super.setRenderer(renderer);
if (!Any.equals(environment) && module != null)
processURLTemplates(environment);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.page.AbstractRenderer#getRenderer()
*/
@Override
public URL getRenderer() {
return getRenderer(RendererType.Page.toString());
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.page.AbstractRenderer#getRenderer(java.lang.String)
*/
@Override
public URL getRenderer(String type) {
return super.getRenderer(type);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#setEditor(java.net.URL)
*/
public void setEditor(URL editor) {
this.editor = editor;
if (!Any.equals(environment) && module != null)
processURLTemplates(environment);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#getEditor()
*/
public URL getEditor() {
return editor;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.Renderer#render(ch.entwine.weblounge.common.request.WebloungeRequest,
* ch.entwine.weblounge.common.request.WebloungeResponse)
*/
public void render(WebloungeRequest request, WebloungeResponse response)
throws RenderException {
// Adjust revalidation and expiration time
response.setClientRevalidationTime(getClientRevalidationTime());
response.setCacheExpirationTime(getCacheExpirationTime());
// Add cache support
response.addTag(CacheTag.Module, getModule().getIdentifier());
response.addTag(CacheTag.Renderer, getIdentifier());
URL renderer = renderers.get(RendererType.Page.toString().toLowerCase());
includeJSP(request, response, renderer);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.content.page.PageletRenderer#renderAsEditor(ch.entwine.weblounge.common.request.WebloungeRequest,
* ch.entwine.weblounge.common.request.WebloungeResponse)
*/
public void renderAsEditor(WebloungeRequest request,
WebloungeResponse response) throws RenderException {
includeJSP(request, response, editor);
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#hashCode()
*/
@Override
public int hashCode() {
return super.hashCode();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof PageletRenderer))
return false;
PageletRenderer r = (PageletRenderer) o;
if (module != null && !module.equals(r.getModule()))
return false;
return super.equals(o);
}
/**
* Initializes this pagelet renderer from an XML node that was generated using
* {@link #toXml()}.
* <p>
* To speed things up, you might consider using the second signature that uses
* an existing <code>XPath</code> instance instead of creating a new one.
*
* @param node
* the pagelet renderer node
* @throws IllegalStateException
* if the pagelet renderer cannot be parsed
* @see #fromXml(Node, XPath)
* @see #toXml()
*/
public static PageletRenderer fromXml(Node node) throws IllegalStateException {
XPath xpath = XPathFactory.newInstance().newXPath();
return fromXml(node, xpath);
}
/**
* Initializes this pagelet renderer from an XML node that was generated using
* {@link #toXml()}.
*
* @param node
* the pagelet renderer node
* @param xpath
* the xpath processor
* @throws IllegalStateException
* if the pagelet renderer cannot be parsed
* @see #fromXml(Node)
* @see #toXml()
*/
public static PageletRenderer fromXml(Node node, XPath xpath)
throws IllegalStateException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Identifier
String id = XPathHelper.valueOf(node, "@id", xpath);
if (id == null)
throw new IllegalStateException("Missing id in page template definition");
// Class
String className = XPathHelper.valueOf(node, "m:class", xpath);
// Create the pagelet renderer
PageletRenderer renderer = null;
if (className != null) {
Class<? extends PageletRenderer> c = null;
try {
c = (Class<? extends PageletRenderer>) classLoader.loadClass(className);
renderer = c.newInstance();
renderer.setIdentifier(id);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Implementation " + className + " for pagelet renderer '" + id + "' not found", e);
} catch (InstantiationException e) {
throw new IllegalStateException("Error instantiating impelementation " + className + " for pagelet renderer '" + id + "'", e);
} catch (IllegalAccessException e) {
throw new IllegalStateException("Access violation instantiating implementation " + className + " for pagelet renderer '" + id + "'", e);
} catch (Throwable t) {
throw new IllegalStateException("Error loading implementation " + className + " for pagelet renderer '" + id + "'", t);
}
} else {
renderer = new PageletRendererImpl();
renderer.setIdentifier(id);
}
// Renderer url
NodeList rendererUrlNodes = XPathHelper.selectList(node, "m:renderer", xpath);
if (rendererUrlNodes.getLength() == 0)
throw new IllegalStateException("Missing renderer in page template definition");
for (int i = 0; i < rendererUrlNodes.getLength(); i++) {
Node rendererUrlNode = rendererUrlNodes.item(i);
URL rendererUrl = null;
Node typeNode = rendererUrlNode.getAttributes().getNamedItem("type");
String type = (typeNode != null) ? typeNode.getNodeValue() : RendererType.Page.toString();
try {
rendererUrl = new URL(rendererUrlNode.getFirstChild().getNodeValue());
renderer.addRenderer(rendererUrl, type);
} catch (MalformedURLException e) {
throw new IllegalStateException("Malformed renderer url in page template definition: " + rendererUrlNode);
}
}
// Composeable
renderer.setComposeable("true".equals(XPathHelper.valueOf(node, "@composeable", xpath)));
// Preview mode
String previewMode = XPathHelper.valueOf(node, "m:preview", xpath);
if (previewMode != null)
renderer.setPreviewMode(PagePreviewMode.parse(previewMode));
// Editor url
String editorUrlNode = XPathHelper.valueOf(node, "m:editor", xpath);
try {
if (editorUrlNode != null) {
URL editorUrl = new URL(editorUrlNode);
renderer.setEditor(editorUrl);
}
} catch (MalformedURLException e) {
throw new IllegalStateException("Malformed editor url in page template definition: " + editorUrlNode);
}
// client revalidation time
String recheck = XPathHelper.valueOf(node, "m:recheck", xpath);
if (recheck != null) {
try {
renderer.setClientRevalidationTime(ConfigurationUtils.parseDuration(recheck));
} catch (NumberFormatException e) {
throw new IllegalStateException("The pagelet renderer revalidation time is malformed: '" + recheck + "'");
} catch (IllegalArgumentException e) {
throw new IllegalStateException("The pagelet renderer revalidation time is malformed: '" + recheck + "'");
}
}
// cache expiration time
String valid = XPathHelper.valueOf(node, "m:valid", xpath);
if (valid != null) {
try {
renderer.setCacheExpirationTime(ConfigurationUtils.parseDuration(valid));
} catch (NumberFormatException e) {
throw new IllegalStateException("The pagelet renderer valid time is malformed: '" + valid + "'", e);
} catch (IllegalArgumentException e) {
throw new IllegalStateException("The pagelet renderer valid time is malformed: '" + valid + "'", e);
}
}
// name
String name = XPathHelper.valueOf(node, "m:name", xpath);
renderer.setName(name);
// scripts
NodeList scripts = XPathHelper.selectList(node, "m:includes/m:script", xpath);
for (int i = 0; i < scripts.getLength(); i++) {
renderer.addHTMLHeader(ScriptImpl.fromXml(scripts.item(i)));
}
// links
NodeList includes = XPathHelper.selectList(node, "m:includes/m:link", xpath);
for (int i = 0; i < includes.getLength(); i++) {
renderer.addHTMLHeader(LinkImpl.fromXml(includes.item(i)));
}
return renderer;
}
/**
* Returns an XML representation of this renderer.
*
* @return the xml representation
*/
public String toXml() {
StringBuffer buf = new StringBuffer();
buf.append("<pagelet");
buf.append(" id=\"").append(identifier).append("\"");
buf.append(" composeable=\"").append(composeable).append("\"");
buf.append(">");
// Names
if (StringUtils.isNotBlank(name)) {
buf.append("<name><![CDATA[");
buf.append(name);
buf.append("]]></name>");
}
// Renderer class
if (!this.getClass().equals(PageletRendererImpl.class))
buf.append("<class>").append(getClass().getName()).append("</class>");
// Renderer url
for (Map.Entry<String, URL> entry : renderers.entrySet()) {
if (renderers.size() > 1)
buf.append("<renderer type=\"").append(entry.getKey()).append("\">");
else
buf.append("<renderer>");
buf.append(entry.getValue().toExternalForm()).append("</renderer>");
}
// Editor url
if (editor != null)
buf.append("<editor>").append(editor.toExternalForm()).append("</editor>");
// Recheck time
if (clientRevalidationTime >= 0) {
buf.append("<recheck>");
buf.append(ConfigurationUtils.toDuration(clientRevalidationTime));
buf.append("</recheck>");
}
// Valid time
if (cacheExpirationTime >= 0) {
buf.append("<valid>");
buf.append(ConfigurationUtils.toDuration(cacheExpirationTime));
buf.append("</valid>");
}
// Preview
if (!previewMode.equals(PagePreviewMode.None)) {
buf.append("<preview>");
buf.append(previewMode.toString().toLowerCase());
buf.append("</preview>");
}
// Includes
if (getHTMLHeaders().length > 0) {
buf.append("<includes>");
for (HTMLHeadElement header : getHTMLHeaders()) {
if (header instanceof Link)
buf.append(header.toXml());
}
for (HTMLHeadElement header : getHTMLHeaders()) {
if (header instanceof Script)
buf.append(header.toXml());
}
buf.append("</includes>");
}
buf.append("</pagelet>");
return buf.toString();
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.content.GeneralComposeable#toString()
*/
@Override
public String toString() {
return module.getIdentifier() + "/" + identifier;
}
}