/*
* File : $Source: /alkacon/cvs/alkacon/com.alkacon.opencms.weboptimization/src/com/alkacon/opencms/weboptimization/CmsOptimizationBean.java,v $
* Date : $Date: 2010/01/08 09:46:05 $
* Version: $Revision: 1.3 $
*
* This file is part of the Alkacon OpenCms Add-On Module Package
*
* Copyright (c) 2007 Alkacon Software GmbH (http://www.alkacon.com)
*
* The Alkacon OpenCms Add-On Module Package is free software:
* you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The Alkacon OpenCms Add-On Module Package 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the Alkacon OpenCms Add-On Module Package.
* If not, see http://www.gnu.org/licenses/.
*
* For further information about Alkacon Software GmbH, please see the
* company website: http://www.alkacon.com.
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org.
*/
package com.alkacon.opencms.weboptimization;
import org.opencms.file.CmsFile;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsPropertyDefinition;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.i18n.CmsLocaleManager;
import org.opencms.jsp.CmsJspActionElement;
import org.opencms.loader.CmsTemplateLoaderFacade;
import org.opencms.loader.CmsXmlContentLoader;
import org.opencms.loader.I_CmsResourceLoader;
import org.opencms.main.CmsException;
import org.opencms.main.CmsLog;
import org.opencms.main.OpenCms;
import org.opencms.xml.CmsXmlUtils;
import org.opencms.xml.content.CmsXmlContent;
import org.opencms.xml.content.CmsXmlContentFactory;
import org.opencms.xml.types.I_CmsXmlContentValue;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.PageContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
/**
* Base bean for optimization.<p>
*
* @author Michael Moossen
*
* @version $Revision: 1.3 $
*
* @since 7.0.6
*/
public abstract class CmsOptimizationBean extends CmsJspActionElement {
/**
* The inclusion mode.<p>
*/
protected enum IncludeMode {
/** Optimized mode. */
OPTIMIZED,
/** Original mode.*/
ORIGINAL,
/** Automatic mode. */
AUTO;
}
/**
* The parsing results.<p>
*/
protected static class Resolution {
/** List of original resources. */
private List<CmsResource> m_resources = new ArrayList<CmsResource>();
/** Are there any optimized resource left?. */
private boolean m_hasOptimizedLeft;
/**
* Adds the given resource to the list.<p>
*
* @param resource the resource to add
*/
public void addResource(CmsResource resource) {
if (!m_resources.contains(resource)) {
m_resources.add(resource);
}
}
/**
* Returns the list of resources.<p>
*
* @return the list of resources
*/
public List<CmsResource> getResources() {
return m_resources;
}
/**
* Checks if there is at least one optimized resource left.<p>
*
* @return <code>true</code> if there is at least one optimized resource left
*/
public boolean hasOptimizedLeft() {
return m_hasOptimizedLeft;
}
/**
* Merges the data with the given resolution.<p>
*
* @param resolution the resolution to merge with
*/
public void merge(Resolution resolution) {
if(resolution.hasOptimizedLeft()) {
setOptimizedLeft();
}
m_resources.addAll(resolution.getResources());
}
/**
* Sets the flag indicating that there is at least one optimized
* resource left.<p>
*/
public void setOptimizedLeft() {
m_hasOptimizedLeft = true;
}
}
/** type attribute constant. */
protected static final String ATTR_TYPE = "type";
/** Node name constant. */
protected static final String N_LINE_BREAK_POS = "LineBreakPos";
/** Node name constant. */
protected static final String N_OPTIONS = "Options";
/** Node name constant. */
protected static final String N_PATH = "Path";
/** Node name constant. */
protected static final String N_RESOURCE = "Resource";
/** Node name constant. */
protected static final String N_BEHAVIOUR = "Behaviour";
/** Behaviour value constant. */
protected static final String BEHAVIOUR_OPTIMIZED = "optimized";
/** Node name constant. */
protected static final String N_ONLINE = "Online";
/** Node name constant. */
protected static final String N_OFFLINE = "Offline";
/** The log object for this class. */
private static final Log LOG = CmsLog.getLog(CmsOptimizationBean.class);
/**
* Default constructor.
*
* @param context the JSP page context object
* @param req the JSP request
* @param res the JSP response
*/
public CmsOptimizationBean(PageContext context, HttpServletRequest req, HttpServletResponse res) {
super(context, req, res);
}
/**
* Creates a new HTML tag with the given name and attributes.<p>
*
* @param tag the tag name
* @param attr the attributes, can be <code>null</code>
* @param xmlStyle if <code>true</code>,the tag will be closed in the xml style
*
* @return the HTML code
*/
protected String createTag(String tag, Map<String,String> attr, boolean xmlStyle) {
StringBuffer sb = new StringBuffer();
sb.append("<");
sb.append(tag);
if (attr != null) {
Iterator<Map.Entry<String,String>> it = attr.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String,String> entry = it.next();
sb.append(" ");
sb.append(entry.getKey());
sb.append("=\"");
sb.append(entry.getValue());
sb.append("\"");
}
}
sb.append(" ");
if (xmlStyle) {
sb.append("/");
}
sb.append(">");
if (!xmlStyle) {
sb.append("</");
sb.append(tag);
sb.append(">");
}
return sb.toString();
}
/**
* Returns the content of all files of the given path with the 'right' extension.<p>
*
* @param cms the cms context
* @param path the path to process, can be a folder
*
* @return the content of all files
*
* @throws Exception if something goes wrong
*/
protected String getAllContent(CmsObject cms, String path) throws Exception {
// get the extension (should be .css or .js)
String ext = cms.getRequestContext().getUri().substring(cms.getRequestContext().getUri().lastIndexOf('.'));
// retrieve the actual files to process
List<CmsResource> resources = resolveResource(cms, path, ext);
if (resources.isEmpty()) {
LOG.warn(Messages.get().getBundle().key(Messages.LOG_WARN_NOTHING_TO_PROCESS_1, path));
return "";
}
// merge file contents
StringBuffer sb = new StringBuffer();
Iterator<CmsResource> itRes = resources.iterator();
while (itRes.hasNext()) {
CmsResource res = itRes.next();
byte[] data = getBinaryContent(cms, res);
sb.append(new String(data, getRequestContext().getEncoding()));
}
return sb.toString();
}
/**
* Returns the processed output of an OpenCms resource.<p>
*
* @param cms the cms context
* @param resource the resource to process
*
* @return the processed output
*
* @throws Exception if something goes wrong
*/
protected byte[] getBinaryContent(CmsObject cms, CmsResource resource) throws Exception {
I_CmsResourceLoader loader = OpenCms.getResourceManager().getLoader(resource);
// We need to handle separately xmlcontent files since we want the rendered content and not the xml data
if (loader.getLoaderId() == CmsXmlContentLoader.RESOURCE_LOADER_ID) {
// HACK: the A_CmsXmlDocumentLoader.getTemplatePropertyDefinition() method is not accessible :(
String templatePropertyDef = CmsPropertyDefinition.PROPERTY_TEMPLATE_ELEMENTS;
CmsTemplateLoaderFacade loaderFacade = OpenCms.getResourceManager().getTemplateLoaderFacade(
cms,
resource,
templatePropertyDef);
loader = loaderFacade.getLoader();
String oldUri = cms.getRequestContext().getUri();
try {
cms.getRequestContext().setUri(cms.getSitePath(resource));
resource = loaderFacade.getLoaderStartResource();
return loader.dump(cms, resource, null, null, getRequest(), getResponse());
} finally {
cms.getRequestContext().setUri(oldUri);
}
}
return loader.dump(cms, resource, null, null, getRequest(), getResponse());
}
/**
* Creates code for the given optimized resource,
* by default it will use the original code in the
* offline project for debugging purposes, and
* optimized code in the online project for optimal
* performance.<p>
*
* @param path the uri of the file to be included
*
* @throws Exception if something goes wrong
*/
public void includeDefault(String path) throws Exception {
includeDefault(path, IncludeMode.AUTO);
}
/**
* Creates optimized code for the optimized resource, depending on the mode.<p>
*
* @param path the optimized resource uri
* @param mode the mode to use
*
* @throws Exception if something goes wrong
*/
protected abstract void includeDefault(String path, IncludeMode mode) throws Exception;
/**
* Creates optimized code for the optimized resource.<p>
*
* @param path the optimized resource uri
*
* @throws Exception if something goes wrong
*/
public void includeOptimized(String path) throws Exception {
includeDefault(path, IncludeMode.OPTIMIZED);
}
/**
* Creates original code for the optimized resource.<p>
*
* @param path the optimized resource uri
*
* @throws Exception if something goes wrong
*/
public void includeOriginal(String path) throws Exception {
includeDefault(path, IncludeMode.ORIGINAL);
}
/**
* Checks if the given resource node has to be optimized.<p>
*
* @param cms the current cms context
* @param xml the xml content to use
* @param value the resource xml node
* @param locale the locale to use
* @param online if online or offline
*
* @return <code>true</code> if the given resource node has to be optimized
*/
protected boolean isOptimized(CmsObject cms, CmsXmlContent xml, I_CmsXmlContentValue value, Locale locale, boolean online) {
boolean optimized = false;
String xpath = CmsXmlUtils.concatXpath(value.getPath(), N_BEHAVIOUR);
I_CmsXmlContentValue behaviour = xml.getValue(xpath, locale);
if (behaviour == null) {
if (online) {
optimized = true;
}
} else {
if (online) {
xpath = CmsXmlUtils.concatXpath(behaviour.getPath(), N_ONLINE);
} else {
xpath = CmsXmlUtils.concatXpath(behaviour.getPath(), N_OFFLINE);
}
I_CmsXmlContentValue p = xml.getValue(xpath, locale);
optimized = p.getStringValue(cms).equals(BEHAVIOUR_OPTIMIZED);
}
return optimized;
}
/**
* Resolves the given file.<p>
*
* @param cms the current context
* @param file the file to resolve
* @param mode the mode to use
* @param extension the file extension to restrict the inclusion
* @param type the resource type id for recursion
*
* @return the resolution
*
* @throws CmsException if something goes wrong
*/
protected Resolution resolveInclude(CmsObject cms, CmsFile file, IncludeMode mode, String extension, int type) throws CmsException {
Resolution resolution = new Resolution();
// read the XML content
CmsXmlContent xml = CmsXmlContentFactory.unmarshal(cms, file);
// resolve the locale
Locale locale = resolveLocale(cms, xml);
// cache the current project
boolean online = cms.getRequestContext().currentProject().isOnlineProject();
// iterate the resources
Iterator<I_CmsXmlContentValue> itPath = xml.getValues(N_RESOURCE, locale).iterator();
while (itPath.hasNext()) {
I_CmsXmlContentValue value = itPath.next();
// get the uri
String xpath = CmsXmlUtils.concatXpath(value.getPath(), N_PATH);
String uri = xml.getValue(xpath, locale).getStringValue(cms);
// retrieve the actual files to process
List<CmsResource> resorces = resolveResource(cms, uri, extension);
if (resorces.isEmpty()) {
LOG.warn(Messages.get().getBundle().key(
Messages.LOG_WARN_NOTHING_TO_PROCESS_1, uri));
continue;
}
if (mode == IncludeMode.AUTO) {
// compute the mode to use
boolean optimized = isOptimized(cms, xml, value, locale, online);
if (optimized) {
resolution.setOptimizedLeft();
// if optimized it will be handled while rendering
continue;
}
}
Iterator<CmsResource> itRes = resorces.iterator();
while (itRes.hasNext()) {
CmsResource res = itRes.next();
if (res.getTypeId() == type) {
// recurse in case of nested optimized resources
resolution.merge(resolveInclude(cms, cms.readFile(res), mode,extension, type));
} else {
// handle this resource
resolution.addResource(res);
}
}
}
return resolution;
}
/**
* Resolves the right locale to use.<p>
*
* @param cms the cms context
* @param xml the xmlcontent
*
* @return the locale to use
*/
protected Locale resolveLocale(CmsObject cms, CmsXmlContent xml) {
Locale locale = cms.getRequestContext().getLocale();
if (!xml.hasLocale(locale)) {
locale = OpenCms.getLocaleManager().getDefaultLocale(cms, cms.getRequestContext().getUri());
if (!xml.hasLocale(locale)) {
locale = CmsLocaleManager.getDefaultLocale();
if (!xml.hasLocale(locale)) {
locale = (Locale)xml.getLocales().get(0);
}
}
}
return locale;
}
/**
* Will check the extension, and if the path is a folder retrieve
* all files with the same extension in the folder.<p>
*
* @param cms the cms context
* @param path the resource path, can be a folder!
* @param ext the file extension to filter
*
* @return the list of files
*
* @throws CmsException if something goes wrong
*/
protected List<CmsResource> resolveResource(CmsObject cms, String path, String ext) throws CmsException {
List<CmsResource> resorces = new ArrayList<CmsResource>();
// An empty path is most probably a bug/unintentionally included. Ignoring it.
if (StringUtils.isBlank(path)) {
LOG.warn(Messages.get().getBundle().key(Messages.LOG_WARN_RESOLVE_EMPTY_PATH_1, cms.getRequestContext().getUri()));
return resorces;
}
CmsResource res = cms.readResource(path);
if (res.isFolder()) {
// if folder, get all files with the given extension in the folder
List<CmsResource> files = cms.readResources(path, CmsResourceFilter.DEFAULT_FILES);
Iterator<CmsResource> itFiles = files.iterator();
while (itFiles.hasNext()) {
CmsResource file = itFiles.next();
if (file.getRootPath().endsWith(ext)) {
resorces.add(file);
}
}
} else {
if (res.getRootPath().endsWith(ext)) {
resorces.add(res);
}
}
return resorces;
}
}