/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
*
* 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.
*
* 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
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.file.collectors;
import org.opencms.file.CmsDataAccessException;
import org.opencms.file.CmsObject;
import org.opencms.file.CmsProperty;
import org.opencms.file.CmsResource;
import org.opencms.file.CmsResourceFilter;
import org.opencms.file.I_CmsResource;
import org.opencms.file.types.I_CmsResourceType;
import org.opencms.loader.CmsLoaderException;
import org.opencms.main.CmsException;
import org.opencms.main.CmsIllegalArgumentException;
import org.opencms.main.CmsRuntimeException;
import org.opencms.main.OpenCms;
import org.opencms.util.CmsStringUtil;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* A collector that allows to collect resources within a time range based upon
* a configurable property that contains a time stamp.<p>
*
* Additionally a property may be specified that contains a comma separated
* list of category Strings that have to match the specified list of categories
* to allow. <p>
*
* <b>Demo usage:</b><br/>
* <pre>
* <cms:contentload collector="timeFrameAndCategories"
* param="
* resource=/de/events/|
* resourceType=xmlcontent|
* resultLimit=10|
* sortDescending=true|
* timeStart=2007-08-01 14:22:12|
* timeEnd=2007-08-01 14:22:12|
* propertyTime=collector.time|
* propertyCategories=collector.categories|
* categories=sports,action,lifestyle"
* >
* </pre>
* <p>
*
* <b>The param attribute</b>
*
* supports a key - value syntax for collector params.<p>
*
* All parameters are specified as follows:
* <pre>
* key=value
* </pre>
* <p>
* Many key - value pairs may exist:
* <pre>
* key=value|key2=value2|key3=value3
* </pre>
* <p>
* The following keys are reserved:
* <ul>
* <li>
* <b>resource</b><br/>
* The value defines the folder / single file for collection of results.
* </li>
* <li>
* <b>resourceType</b><br/>
* The value defines the name of the type of resource that is required for the result as
* defined in opencms-modules.xml, opencms-workplace.xml.
* </li>
* <li>
* <b>resultLimit</b><br/>
* The value defines the maximum amount of results to return.
* </li>
* <li>
* <b>sortDescending</b><br/>
* The value defines if the result is sorted in descending ("true") or ascending
* (anything else than "true") order.
* </li>
* <li>
* <b>timeStart</b><br/>
* The value defines the start time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
* known by the description of <code>{@link SimpleDateFormat}</code>
* that will be used for the validity time frame of result candidates.
* </li>
* <li>
* <b>timeEnd</b><br/>
* The value defines the end time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
* known by the description of <code>{@link SimpleDateFormat}</code>
* that will be used for the validity time frame of result candidates.
* </li>
* <li>
* <b>propertyTime</b><br/>
* The value defines the name of the property that is inspected for a time stamp
* in <code> {@link System#currentTimeMillis()}</code> syntax for the validity time frame
* check.
* </li>
* <li>
* <b>propertyCategories</b><br/>
* The value defines the name of the property that is inspected for a pipe separated
* list of category strings.
* </li>
* <li>
* <b>categories</b><br/>
* The value defines a list of comma separated category Strings used to filter
* result candidates by. If this parameter is missing completely no category
* filtering will be done and also resources with empty category property will
* be accepted.
* </li>
* </ul>
* <p>
*
* All other key - value pairs are ignored.<p>
*
* @since 7.0.3
*
*/
public class CmsTimeFrameCategoryCollector extends A_CmsResourceCollector {
/**
* Supports a key - value syntax for collector params.<p>
*
* All parameters are specified as follows:
* <pre>
* key=value
* </pre>
* <p>
* Many key - value pairs may exist:
* <pre>
* key=value|key2=value2|key3=value3
* </pre>
* <p>
* The following keys are reserved:
* <ul>
* <li>
* <b>resource</b><br/>
* The value defines the folder / single file for collection of results.
* </li>
* <li>
* <b>resourceType</b><br/>
* The value defines the name of the type of resource that is required for the result as
* defined in opencms-modules.xml, opencms-workplace.xml.
* </li>
* <li>
* <b>resultLimit</b><br/>
* The value defines the maximum amount of results to return.
* </li>
* <li>
* <b>sortDescending</b><br/>
* The value defines if the result is sorted in descending ("true") or ascending
* (anything else than "true") order.
* </li>
* <li>
* <b>timeStart</b><br/>
* The value defines the start time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
* known by the description of <code>{@link SimpleDateFormat}</code>
* that will be used for the validity time frame of result candidates.
* </li>
* <li>
* <b>timeEnd</b><br/>
* The value defines the end time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
* known by the description of <code>{@link SimpleDateFormat}</code>
* that will be used for the validity time frame of result candidates.
* </li>
* <li>
* <b>propertyTime</b><br/>
* The value defines the name of the property that is inspected for a time stamp
* in <code> {@link System#currentTimeMillis()}</code> syntax for the validity time frame
* check.
* </li>
* <li>
* <b>propertyCategories</b><br/>
* The value defines the name of the property that is inspected for a pipe separated
* list of category strings.
* </li>
* <li>
* <b>categories</b><br/>
* The value defines a list of comma separated category Strings used to filter
* result candidates by. If this parameter is missing completely no category
* filtering will be done and also resources with empty category property will
* be accepted.
* </li>
* </ul>
* <p>
*/
private class CollectorDataPropertyBased extends CmsCollectorData {
/** The collector parameter key for the categories: value is a list of comma - separated Strings. */
public static final String PARAM_KEY_CATEGORIES = "categories";
/** The collector parameter key for the name of the categoires property used to filter resources by. */
public static final String PARAM_KEY_PPROPERTY_CATEGORIES = "propertyCategories";
/** The collector parameter key for the name of the property to use for the validity time frame check. */
public static final String PARAM_KEY_PPROPERTY_TIME = "propertyTime";
/** The collector parameter key for the resource (folder / file). */
public static final String PARAM_KEY_RESOURCE = "resource";
/** The collector parameter key for a result limit. */
public static final String PARAM_KEY_RESOURCE_TYPE = "resourceType";
/** The collector parameter key for a result limit. */
public static final String PARAM_KEY_RESULT_LIMIT = "resultLimit";
/** The collector parameter key for a result limit. */
public static final String PARAM_KEY_SORT_DESCENDING = "sortDescending";
/** The collector parameter key for the start time of the validity time frame. */
public static final String PARAM_KEY_TIMEFRAME_END = "timeEnd";
/** The collector parameter key for the start time of the validity time frame. */
public static final String PARAM_KEY_TIMEFRAME_START = "timeStart";
/** The List <String> containing the categories to allow. */
private List<String> m_categories = Collections.emptyList();
/** The display count. */
private int m_count;
/** The resource path (folder / file). */
private String m_fileName;
/** The property to look for a pipe separated list of category strings in.*/
private CmsProperty m_propertyCategories = new CmsProperty();
/** The property to look up for a time stamp on result candidates for validity time frame check.*/
private CmsProperty m_propertyTime = new CmsProperty();
/** If true results should be sorted in descending order.*/
private boolean m_sortDescending;
/** The end of the validity time frame.*/
private long m_timeFrameEnd = Long.MAX_VALUE;
/** The start of the validity time frame.*/
private long m_timeFrameStart;
/** The resource type to require. */
private I_CmsResourceType m_type;
/**
* Constructor with the collector param of the tag.<p>
*
* @param data the param attribute value of the contentload tag.
*
* @throws CmsLoaderException if the collector param specifies an illegal resource type.
*
*/
public CollectorDataPropertyBased(String data)
throws CmsLoaderException {
try {
parseParam(data);
} catch (ParseException pe) {
CmsRuntimeException ex = new CmsIllegalArgumentException(Messages.get().container(
Messages.ERR_COLLECTOR_PARAM_DATE_FORMAT_SYNTAX_0));
ex.initCause(pe);
throw ex;
}
}
/**
* Returns The List <String> containing the categories to allow.<p>
*
* @return The List <String> containing the categories to allow.
*/
public List<String> getCategories() {
return m_categories;
}
/**
* Returns the count.
* <p>
*
* @return the count
*/
@Override
public int getCount() {
return m_count;
}
/**
* Returns the file name.<p>
*
* @return the file name
*/
@Override
public String getFileName() {
return m_fileName;
}
/**
* Returns the property to look for a pipe separated list of category strings in.<p>
*
* Never write this property to VFS as it is "invented in RAM" and not
* read from VFS!<p>
*
* @return the property to look for a pipe separated list of category strings in.
*/
public CmsProperty getPropertyCategories() {
return m_propertyCategories;
}
/**
* Returns The property to look up for a time stamp
* on result candidates for validity time frame check.<p>
*
* Never write this property to VFS as it is "invented in RAM" and not
* read from VFS!<p>
*
* @return The property to look up for a time stamp on result candidates for validity time frame check.
*/
public CmsProperty getPropertyTime() {
return m_propertyTime;
}
/**
* Returns the timeFrameEnd.<p>
*
* @return the timeFrameEnd
*
* @see #getPropertyTime()
*/
public long getTimeFrameEnd() {
return m_timeFrameEnd;
}
/**
* Returns the timeFrameStart.<p>
*
* @return the timeFrameStart
*/
public long getTimeFrameStart() {
return m_timeFrameStart;
}
/**
* Returns the type.
* <p>
*
* @return the type
*/
@Override
public int getType() {
return m_type.getTypeId();
}
/**
* If true results should be sorted in descending order.<p>
*
* Defaults to true.<p>
*
* @return true if results should be sorted in descending order, false
* if results should be sorted in ascending order.
*/
public boolean isSortDescending() {
return m_sortDescending;
}
/**
* Internally parses the constructor-given param into the data model
* of this instance.<p>
*
* @param param the constructor-given param.
*
* @throws CmsLoaderException if the collector param specifies an illegal resource type.
*
* @throws ParseException if date parsing in scope of the param attribute fails.
*/
private void parseParam(final String param) throws CmsLoaderException, ParseException {
List<String> keyValuePairs = CmsStringUtil.splitAsList(param, '|');
String[] keyValuePair;
Iterator<String> itKeyValuePairs = keyValuePairs.iterator();
String keyValuePairStr;
String key;
String value;
while (itKeyValuePairs.hasNext()) {
keyValuePairStr = itKeyValuePairs.next();
keyValuePair = CmsStringUtil.splitAsArray(keyValuePairStr, '=');
if (keyValuePair.length != 2) {
throw new CmsIllegalArgumentException(Messages.get().container(
Messages.ERR_COLLECTOR_PARAM_KEY_VALUE_SYNTAX_1,
new Object[] {keyValuePairStr}));
}
key = String.valueOf(keyValuePair[0]).trim();
value = String.valueOf(keyValuePair[1]).trim();
if (PARAM_KEY_RESOURCE.equals(key)) {
m_fileName = value;
} else if (PARAM_KEY_RESOURCE_TYPE.equals(key)) {
m_type = OpenCms.getResourceManager().getResourceType(value);
} else if (PARAM_KEY_RESULT_LIMIT.equals(key)) {
m_count = Integer.parseInt(value);
} else if (PARAM_KEY_SORT_DESCENDING.equals(key)) {
m_sortDescending = new Boolean(value).booleanValue();
} else if (PARAM_KEY_TIMEFRAME_START.equals(key)) {
m_timeFrameStart = DATEFORMAT_SQL.parse(value).getTime();
} else if (PARAM_KEY_TIMEFRAME_END.equals(key)) {
m_timeFrameEnd = DATEFORMAT_SQL.parse(value).getTime();
} else if (PARAM_KEY_PPROPERTY_TIME.equals(key)) {
m_propertyTime.setName(value);
} else if (PARAM_KEY_CATEGORIES.equals(key)) {
m_categories = CmsStringUtil.splitAsList(value, ',');
} else if (PARAM_KEY_PPROPERTY_CATEGORIES.equals(key)) {
m_propertyCategories.setName(value);
} else {
// nop, one could accept additional filter properties here...
}
}
}
}
/** Static array of the collectors implemented by this class. */
private static final String COLLECTOR_NAME = "timeFrameAndCategories";
/** Sorted set for fast collector name lookup. */
private static final List<String> COLLECTORS_LIST = Collections.unmodifiableList(Arrays.asList(new String[] {COLLECTOR_NAME}));
/** SQL Standard date format: "yyyy-MM-dd HH:mm:ss".*/
public static final DateFormat DATEFORMAT_SQL = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* Public constructor.<p>
*/
public CmsTimeFrameCategoryCollector() {
// NOOP
}
/**
* @see org.opencms.file.collectors.I_CmsResourceCollector#getCollectorNames()
*/
public List<String> getCollectorNames() {
return new ArrayList<String>(COLLECTORS_LIST);
}
/**
* @see org.opencms.file.collectors.I_CmsResourceCollector#getCreateLink(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
*/
public String getCreateLink(CmsObject cms, String collectorName, String param)
throws CmsException, CmsDataAccessException {
// if action is not set, use default action
if (collectorName == null) {
collectorName = COLLECTOR_NAME;
}
if (COLLECTOR_NAME.equals(collectorName)) {
return getCreateInFolder(cms, new CollectorDataPropertyBased(param));
} else {
throw new CmsDataAccessException(org.opencms.file.collectors.Messages.get().container(
org.opencms.file.collectors.Messages.ERR_COLLECTOR_NAME_INVALID_1,
collectorName));
}
}
/**
* @see org.opencms.file.collectors.I_CmsResourceCollector#getCreateParam(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
*/
public String getCreateParam(CmsObject cms, String collectorName, String param) {
return null;
}
/**
* @see org.opencms.file.collectors.I_CmsResourceCollector#getResults(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
*/
public List<CmsResource> getResults(CmsObject cms, String collectorName, String param)
throws CmsDataAccessException, CmsException {
// if action is not set use default
if (collectorName == null) {
collectorName = COLLECTOR_NAME;
}
if (COLLECTOR_NAME.equals(collectorName)) {
// "singleFile"
return getTimeFrameAndCategories(cms, param);
} else {
throw new CmsDataAccessException(org.opencms.file.collectors.Messages.get().container(
org.opencms.file.collectors.Messages.ERR_COLLECTOR_NAME_INVALID_1,
collectorName));
}
}
private List<CmsResource> getTimeFrameAndCategories(CmsObject cms, String param) throws CmsException {
List<CmsResource> result = null;
CollectorDataPropertyBased data = new CollectorDataPropertyBased(param);
// Step 1: Read from DB, expiration is respected.
String foldername = CmsResource.getFolderPath(data.getFileName());
CmsResourceFilter filter = CmsResourceFilter.DEFAULT.addRequireType(data.getType()).addExcludeFlags(
CmsResource.FLAG_TEMPFILE);
result = cms.readResources(foldername, filter, true);
// Step 2: Time range filtering
String timeProperty = data.getPropertyTime().getName();
long start = data.getTimeFrameStart();
long end = data.getTimeFrameEnd();
long resTime;
Iterator<CmsResource> itResults = result.iterator();
CmsProperty prop;
CmsResource res;
while (itResults.hasNext()) {
res = itResults.next();
prop = cms.readPropertyObject(res, timeProperty, true);
if (!prop.isNullProperty()) {
resTime = Long.parseLong(prop.getValue());
if ((resTime < start) || (resTime > end)) {
itResults.remove();
}
}
}
// Step 3: Category filtering
List<String> categories = data.getCategories();
if ((categories != null) && !categories.isEmpty()) {
itResults = result.iterator();
String categoriesProperty = data.getPropertyCategories().getName();
List<String> categoriesFound;
while (itResults.hasNext()) {
res = itResults.next();
prop = cms.readPropertyObject(res, categoriesProperty, true);
if (prop.isNullProperty()) {
// disallow contents with empty category property:
itResults.remove();
// accept contents with empty category property:
// continue;
} else {
categoriesFound = CmsStringUtil.splitAsList(prop.getValue(), '|');
// filter: resource has to be at least in one category
Iterator<String> itCategories = categories.iterator();
String category;
boolean contained = false;
while (itCategories.hasNext()) {
category = itCategories.next();
if (categoriesFound.contains(category)) {
contained = true;
break;
}
}
if (!contained) {
itResults.remove();
}
}
}
}
// Step 4: Sorting
if (data.isSortDescending()) {
Collections.sort(result, I_CmsResource.COMPARE_DATE_RELEASED);
} else {
Collections.sort(result, new ComparatorInverter(I_CmsResource.COMPARE_DATE_RELEASED));
}
// Step 5: result limit
return shrinkToFit(result, data.getCount());
}
}