001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.file.collectors;
029
030import org.opencms.file.CmsDataAccessException;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsProperty;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsResourceFilter;
035import org.opencms.file.I_CmsResource;
036import org.opencms.file.types.I_CmsResourceType;
037import org.opencms.loader.CmsLoaderException;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsIllegalArgumentException;
040import org.opencms.main.CmsRuntimeException;
041import org.opencms.main.OpenCms;
042import org.opencms.util.CmsStringUtil;
043
044import java.text.DateFormat;
045import java.text.ParseException;
046import java.text.SimpleDateFormat;
047import java.util.ArrayList;
048import java.util.Arrays;
049import java.util.Collections;
050import java.util.Iterator;
051import java.util.List;
052
053/**
054 * A collector that allows to collect resources within a time range based upon
055 * a configurable property that contains a time stamp.<p>
056 *
057 * Additionally a property may be specified that contains a comma separated
058 * list of category Strings that have to match the specified list of categories
059 * to allow. <p>
060 *
061 * <b>Demo usage:</b><br/>
062 * <pre>
063 * &lt;cms:contentload collector="timeFrameAndCategories"
064 *   param="
065 *     resource=/de/events/|
066 *     resourceType=xmlcontent|
067 *     resultLimit=10|
068 *     sortDescending=true|
069 *     excludeTimerange=true|
070 *     timeStart=2007-08-01 14:22:12|
071 *     timeEnd=2007-08-01 14:22:12|
072 *     propertyTime=collector.time|
073 *     propertyCategories=collector.categories|
074 *     categories=sports,action,lifestyle"
075 *   &gt;
076 * </pre>
077 * <p>
078 *
079 * <b>The param attribute</b>
080 *
081 * supports a key - value syntax for collector params.<p>
082 *
083 * All parameters are specified as follows:
084 * <pre>
085 * key=value
086 * </pre>
087 * <p>
088 * Many key - value pairs may exist:
089 * <pre>
090 * key=value|key2=value2|key3=value3
091 * </pre>
092 * <p>
093 * The following keys are reserved:
094 * <ul>
095 * <li>
096 * <b>resource</b><br/>
097 * The value defines the folder / single file for collection of results.
098 * </li>
099 * <li>
100 * <b>resourceType</b><br/>
101 * The value defines the name of the type of resource that is required for the result as
102 * defined in opencms-modules.xml, opencms-workplace.xml.
103 * </li>
104 * <li>
105 * <b>resultLimit</b><br/>
106 * The value defines the maximum amount of results to return.
107 * </li>
108 * <li>
109 * <b>sortDescending</b><br/>
110 * The value defines if the result is sorted in descending ("true") or ascending
111 * (anything else than "true") order.
112 * </li>
113 * <li>
114 * <b>excludeTimeRange</b><br/>
115 * The value defines if the result should exclude the time range in an offline project.
116 * </li>
117 * <li>
118 * <b>timeStart</b><br/>
119 * The value defines the start time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
120 * known by the description of <code>{@link SimpleDateFormat}</code>
121 * that will be used for the validity time frame of result candidates.
122 * </li>
123 * <li>
124 * <b>timeEnd</b><br/>
125 * The value defines the end time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
126 * known by the description of <code>{@link SimpleDateFormat}</code>
127 * that will be used for the validity time frame of result candidates.
128 * </li>
129 * <li>
130 * <b>propertyTime</b><br/>
131 * The value defines the name of the property that is inspected for a time stamp
132 * in <code> {@link System#currentTimeMillis()}</code> syntax for the validity time frame
133 * check.
134 * </li>
135 * <li>
136 * <b>propertyCategories</b><br/>
137 * The value defines the name of the property that is inspected for a pipe separated
138 * list of category strings.
139 * </li>
140 * <li>
141 * <b>categories</b><br/>
142 * The value defines a list of comma separated category Strings used to filter
143 * result candidates by. If this parameter is missing completely no category
144 * filtering will be done and also resources with empty category property will
145 * be accepted.
146 * </li>
147 * </ul>
148 * <p>
149 *
150 * All other key - value pairs are ignored.<p>
151 *
152 * @since 7.0.3
153 *
154 */
155public class CmsTimeFrameCategoryCollector extends A_CmsResourceCollector {
156
157    /**
158     * Supports a key - value syntax for collector params.<p>
159     *
160     * All parameters are specified as follows:
161     * <pre>
162     * key=value
163     * </pre>
164     * <p>
165     * Many key - value pairs may exist:
166     * <pre>
167     * key=value|key2=value2|key3=value3
168     * </pre>
169     * <p>
170     * The following keys are reserved:
171     * <ul>
172     * <li>
173     * <b>resource</b><br/>
174     * The value defines the folder / single file for collection of results.
175     * </li>
176     * <li>
177     * <b>resourceType</b><br/>
178     * The value defines the name of the type of resource that is required for the result as
179     * defined in opencms-modules.xml, opencms-workplace.xml.
180     * </li>
181     * <li>
182     * <b>resultLimit</b><br/>
183     * The value defines the maximum amount of results to return.
184     * </li>
185     * <li>
186     * <b>sortDescending</b><br/>
187     * The value defines if the result is sorted in descending ("true") or ascending
188     * (anything else than "true") order.
189     * </li>
190     * <li>
191     * <b>excludeTimeRange</b><br/>
192     * The value defines if the result should exclude the time range in an offline project.
193     * </li>
194     * <li>
195     * <b>timeStart</b><br/>
196     * The value defines the start time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
197     * known by the description of <code>{@link SimpleDateFormat}</code>
198     * that will be used for the validity time frame of result candidates.
199     * </li>
200     * <li>
201     * <b>timeEnd</b><br/>
202     * The value defines the end time in the format <code>yyyy-MM-dd HH:mm:ss</code> as
203     * known by the description of <code>{@link SimpleDateFormat}</code>
204     * that will be used for the validity time frame of result candidates.
205     * </li>
206     * <li>
207     * <b>propertyTime</b><br/>
208     * The value defines the name of the property that is inspected for a time stamp
209     * in <code> {@link System#currentTimeMillis()}</code> syntax for the validity time frame
210     * check.
211     * </li>
212     * <li>
213     * <b>propertyCategories</b><br/>
214     * The value defines the name of the property that is inspected for a pipe separated
215     * list of category strings.
216     * </li>
217     * <li>
218     * <b>categories</b><br/>
219     * The value defines a list of comma separated category Strings used to filter
220     * result candidates by. If this parameter is missing completely no category
221     * filtering will be done and also resources with empty category property will
222     * be accepted.
223     * </li>
224     * </ul>
225     * <p>
226     */
227    private class CollectorDataPropertyBased extends CmsCollectorData {
228
229        /** The collector parameter key for the categories: value is a list of comma - separated Strings. */
230        public static final String PARAM_KEY_CATEGORIES = "categories";
231
232        /** The collector parameter key for the name of the categoires property used to filter resources by. */
233        public static final String PARAM_KEY_PPROPERTY_CATEGORIES = "propertyCategories";
234
235        /** The collector parameter key for the name of the property to use for the validity time frame check. */
236        public static final String PARAM_KEY_PPROPERTY_TIME = "propertyTime";
237
238        /** The collector parameter key for the resource (folder / file). */
239        public static final String PARAM_KEY_RESOURCE = "resource";
240
241        /** The collector parameter key for a result limit. */
242        public static final String PARAM_KEY_RESOURCE_TYPE = "resourceType";
243
244        /** The collector parameter key for a result limit. */
245        public static final String PARAM_KEY_RESULT_LIMIT = "resultLimit";
246
247        /** The collector parameter key for a result limit. */
248        public static final String PARAM_KEY_SORT_DESCENDING = "sortDescending";
249
250        /** The collector parameter key for the start time of the validity time frame. */
251        public static final String PARAM_KEY_TIMEFRAME_END = "timeEnd";
252
253        /** The collector parameter key for the start time of the validity time frame. */
254        public static final String PARAM_KEY_TIMEFRAME_START = "timeStart";
255
256        /** The List &lt;String&gt; containing the categories to allow. */
257        private List<String> m_categories = Collections.emptyList();
258
259        /** The display count. */
260        private int m_count;
261
262        /** The resource path (folder / file). */
263        private String m_fileName;
264
265        /** The property to look for a pipe separated list of category strings in.*/
266        private CmsProperty m_propertyCategories = new CmsProperty();
267
268        /** The property to look up for a time stamp on result candidates for validity time frame check.*/
269        private CmsProperty m_propertyTime = new CmsProperty();
270
271        /** If true results should be sorted in descending order.*/
272        private boolean m_sortDescending;
273
274        /** The end of the validity time frame.*/
275        private long m_timeFrameEnd = Long.MAX_VALUE;
276
277        /** The start of the validity time frame.*/
278        private long m_timeFrameStart;
279
280        /** The resource type to require. */
281        private I_CmsResourceType m_type;
282
283        /**
284         * Constructor with the collector param of the tag.<p>
285         *
286         * @param data the param attribute value of the contentload tag.
287         *
288         * @throws CmsLoaderException if the collector param specifies an illegal resource type.
289         *
290         */
291        public CollectorDataPropertyBased(String data)
292        throws CmsLoaderException {
293
294            try {
295                parseParam(data);
296            } catch (ParseException pe) {
297                CmsRuntimeException ex = new CmsIllegalArgumentException(
298                    Messages.get().container(Messages.ERR_COLLECTOR_PARAM_DATE_FORMAT_SYNTAX_0));
299                ex.initCause(pe);
300                throw ex;
301            }
302
303        }
304
305        /**
306         * Returns The List &lt;String&gt; containing the categories to allow.<p>
307         *
308         * @return The List &lt;String&gt; containing the categories to allow.
309         */
310        public List<String> getCategories() {
311
312            return m_categories;
313        }
314
315        /**
316         * Returns the count.
317         * <p>
318         *
319         * @return the count
320         */
321        @Override
322        public int getCount() {
323
324            return m_count;
325        }
326
327        /**
328         * Returns the file name.<p>
329         *
330         * @return the file name
331         */
332        @Override
333        public String getFileName() {
334
335            return m_fileName;
336        }
337
338        /**
339         * Returns the property to look for a pipe separated list of category strings in.<p>
340         *
341         * Never write this property to VFS as it is "invented in RAM" and not
342         * read from VFS!<p>
343         *
344         * @return the property to look for a pipe separated list of category strings in.
345         */
346        public CmsProperty getPropertyCategories() {
347
348            return m_propertyCategories;
349        }
350
351        /**
352         * Returns The property to look up for a time stamp
353         * on result candidates for validity time frame check.<p>
354         *
355         * Never write this property to VFS as it is "invented in RAM" and not
356         * read from VFS!<p>
357         *
358         * @return The property to look up for a time stamp on result candidates for validity time frame check.
359         */
360        public CmsProperty getPropertyTime() {
361
362            return m_propertyTime;
363        }
364
365        /**
366         * Returns the timeFrameEnd.<p>
367         *
368         * @return the timeFrameEnd
369         *
370         * @see #getPropertyTime()
371         */
372        public long getTimeFrameEnd() {
373
374            return m_timeFrameEnd;
375        }
376
377        /**
378         * Returns the timeFrameStart.<p>
379         *
380         * @return the timeFrameStart
381         */
382        public long getTimeFrameStart() {
383
384            return m_timeFrameStart;
385        }
386
387        /**
388         * Returns the type.
389         * <p>
390         *
391         * @return the type
392         */
393        @Override
394        public int getType() {
395
396            return m_type.getTypeId();
397        }
398
399        /**
400         * If true results should be sorted in descending order.<p>
401         *
402         * Defaults to true.<p>
403         *
404         * @return true if results should be sorted in descending order, false
405         *      if results should be sorted in ascending order.
406         */
407        public boolean isSortDescending() {
408
409            return m_sortDescending;
410        }
411
412        /**
413         * Internally parses the constructor-given param into the data model
414         * of this instance.<p>
415         *
416         * @param param the constructor-given param.
417         *
418         * @throws CmsLoaderException if the collector param specifies an illegal resource type.
419         *
420         * @throws ParseException if date parsing in scope of the param attribute fails.
421         */
422        private void parseParam(final String param) throws CmsLoaderException, ParseException {
423
424            List<String> keyValuePairs = CmsStringUtil.splitAsList(param, '|');
425            String[] keyValuePair;
426            Iterator<String> itKeyValuePairs = keyValuePairs.iterator();
427            String keyValuePairStr;
428            String key;
429            String value;
430            while (itKeyValuePairs.hasNext()) {
431                keyValuePairStr = itKeyValuePairs.next();
432                keyValuePair = CmsStringUtil.splitAsArray(keyValuePairStr, '=');
433                if (keyValuePair.length != 2) {
434                    throw new CmsIllegalArgumentException(
435                        Messages.get().container(
436                            Messages.ERR_COLLECTOR_PARAM_KEY_VALUE_SYNTAX_1,
437                            new Object[] {keyValuePairStr}));
438                }
439                key = String.valueOf(keyValuePair[0]).trim();
440                value = String.valueOf(keyValuePair[1]).trim();
441
442                if (PARAM_KEY_RESOURCE.equals(key)) {
443                    m_fileName = value;
444                } else if (PARAM_KEY_RESOURCE_TYPE.equals(key)) {
445                    m_type = OpenCms.getResourceManager().getResourceType(value);
446                } else if (PARAM_KEY_RESULT_LIMIT.equals(key)) {
447                    m_count = Integer.parseInt(value);
448                } else if (PARAM_KEY_SORT_DESCENDING.equals(key)) {
449                    m_sortDescending = new Boolean(value).booleanValue();
450                } else if (PARAM_KEY_TIMEFRAME_START.equals(key)) {
451                    m_timeFrameStart = DATEFORMAT_SQL.parse(value).getTime();
452                } else if (PARAM_KEY_TIMEFRAME_END.equals(key)) {
453                    m_timeFrameEnd = DATEFORMAT_SQL.parse(value).getTime();
454                } else if (PARAM_KEY_PPROPERTY_TIME.equals(key)) {
455                    m_propertyTime.setName(value);
456                } else if (PARAM_KEY_CATEGORIES.equals(key)) {
457                    m_categories = CmsStringUtil.splitAsList(value, ',');
458                } else if (PARAM_KEY_PPROPERTY_CATEGORIES.equals(key)) {
459                    m_propertyCategories.setName(value);
460                } else if (PARAM_EXCLUDETIMERANGE.equalsIgnoreCase(key)) {
461                    setExcludeTimerange(new Boolean(value).booleanValue());
462                } else {
463                    // nop, one could accept additional filter properties here...
464                }
465
466            }
467
468        }
469
470    }
471
472    /** SQL Standard date format: "yyyy-MM-dd HH:mm:ss".*/
473    public static final DateFormat DATEFORMAT_SQL = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
474
475    /** Static array of the collectors implemented by this class. */
476    private static final String COLLECTOR_NAME = "timeFrameAndCategories";
477
478    /** Sorted set for fast collector name lookup. */
479    private static final List<String> COLLECTORS_LIST = Collections.unmodifiableList(
480        Arrays.asList(new String[] {COLLECTOR_NAME}));
481
482    /**
483     * Public constructor.<p>
484     */
485    public CmsTimeFrameCategoryCollector() {
486
487        // NOOP
488    }
489
490    /**
491     * @see org.opencms.file.collectors.I_CmsResourceCollector#getCollectorNames()
492     */
493    public List<String> getCollectorNames() {
494
495        return new ArrayList<String>(COLLECTORS_LIST);
496    }
497
498    /**
499     * @see org.opencms.file.collectors.I_CmsResourceCollector#getCreateLink(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
500     */
501    public String getCreateLink(CmsObject cms, String collectorName, String param)
502    throws CmsException, CmsDataAccessException {
503
504        // if action is not set, use default action
505        if (collectorName == null) {
506            collectorName = COLLECTOR_NAME;
507        }
508        if (COLLECTOR_NAME.equals(collectorName)) {
509            return getCreateInFolder(cms, new CollectorDataPropertyBased(param));
510        } else {
511            throw new CmsDataAccessException(
512                org.opencms.file.collectors.Messages.get().container(
513                    org.opencms.file.collectors.Messages.ERR_COLLECTOR_NAME_INVALID_1,
514                    collectorName));
515        }
516    }
517
518    /**
519     * @see org.opencms.file.collectors.I_CmsResourceCollector#getCreateParam(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
520     */
521    public String getCreateParam(CmsObject cms, String collectorName, String param) {
522
523        return null;
524    }
525
526    /**
527     * @see org.opencms.file.collectors.A_CmsResourceCollector#getCreateTypeId(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
528     */
529    @Override
530    public int getCreateTypeId(CmsObject cms, String collectorName, String param) throws CmsException {
531
532        int result = -1;
533        if (param != null) {
534            result = new CollectorDataPropertyBased(param).getType();
535        }
536        return result;
537    }
538
539    /**
540     * @see org.opencms.file.collectors.I_CmsResourceCollector#getResults(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
541     */
542    public List<CmsResource> getResults(CmsObject cms, String collectorName, String param)
543    throws CmsDataAccessException, CmsException {
544
545        return getResults(cms, collectorName, param, -1);
546    }
547
548    /**
549     * @see org.opencms.file.collectors.I_CmsResourceCollector#getResults(org.opencms.file.CmsObject, java.lang.String, java.lang.String)
550     */
551    public List<CmsResource> getResults(CmsObject cms, String collectorName, String param, int numResults)
552    throws CmsDataAccessException, CmsException {
553
554        // if action is not set use default
555        if (collectorName == null) {
556            collectorName = COLLECTOR_NAME;
557        }
558
559        if (COLLECTOR_NAME.equals(collectorName)) {
560            // "singleFile"
561            return getTimeFrameAndCategories(cms, param, numResults);
562        } else {
563            throw new CmsDataAccessException(org.opencms.file.collectors.Messages.get().container(
564                org.opencms.file.collectors.Messages.ERR_COLLECTOR_NAME_INVALID_1,
565                collectorName));
566        }
567    }
568
569    /**
570     * Returns a list of resources according to the given parameter.<p>
571     *
572     * @param cms the current cms context
573     * @param param the parameter
574     * @param numResults the number of results
575     *
576     * @return the resulting list of resources
577     *
578     * @throws CmsException if something goes wrong reading the resources
579     */
580    private List<CmsResource> getTimeFrameAndCategories(CmsObject cms, String param, int numResults)
581    throws CmsException {
582
583        List<CmsResource> result = null;
584        CollectorDataPropertyBased data = new CollectorDataPropertyBased(param);
585
586        // Step 1: Read from DB, expiration is respected.
587        String foldername = CmsResource.getFolderPath(data.getFileName());
588        CmsResourceFilter filter = CmsResourceFilter.DEFAULT.addRequireType(data.getType()).addExcludeFlags(
589            CmsResource.FLAG_TEMPFILE);
590        if (data.isExcludeTimerange() && !cms.getRequestContext().getCurrentProject().isOnlineProject()) {
591            // include all not yet released and expired resources in an offline project
592            filter = filter.addExcludeTimerange();
593        }
594        result = cms.readResources(foldername, filter, true);
595
596        // Step 2: Time range filtering
597        String timeProperty = data.getPropertyTime().getName();
598        long start = data.getTimeFrameStart();
599        long end = data.getTimeFrameEnd();
600        long resTime;
601        Iterator<CmsResource> itResults = result.iterator();
602        CmsProperty prop;
603        CmsResource res;
604        while (itResults.hasNext()) {
605            res = itResults.next();
606            prop = cms.readPropertyObject(res, timeProperty, true);
607            if (!prop.isNullProperty()) {
608                resTime = Long.parseLong(prop.getValue());
609                if ((resTime < start) || (resTime > end)) {
610                    itResults.remove();
611                }
612            }
613        }
614
615        // Step 3: Category filtering
616        List<String> categories = data.getCategories();
617        if ((categories != null) && !categories.isEmpty()) {
618            itResults = result.iterator();
619            String categoriesProperty = data.getPropertyCategories().getName();
620            List<String> categoriesFound;
621            while (itResults.hasNext()) {
622                res = itResults.next();
623                prop = cms.readPropertyObject(res, categoriesProperty, true);
624                if (prop.isNullProperty()) {
625                    // disallow contents with empty category property:
626                    itResults.remove();
627                    // accept contents with empty category property:
628                    // continue;
629                } else {
630                    categoriesFound = CmsStringUtil.splitAsList(prop.getValue(), '|');
631
632                    // filter: resource has to be at least in one category
633                    Iterator<String> itCategories = categories.iterator();
634                    String category;
635                    boolean contained = false;
636                    while (itCategories.hasNext()) {
637                        category = itCategories.next();
638                        if (categoriesFound.contains(category)) {
639                            contained = true;
640                            break;
641                        }
642                    }
643                    if (!contained) {
644                        itResults.remove();
645                    }
646                }
647            }
648        }
649
650        // Step 4: Sorting
651        if (data.isSortDescending()) {
652            Collections.sort(result, I_CmsResource.COMPARE_DATE_RELEASED);
653        } else {
654            Collections.sort(result, new ComparatorInverter(I_CmsResource.COMPARE_DATE_RELEASED));
655        }
656
657        // Step 5: result limit
658        return shrinkToFit(result, data.getCount(), numResults);
659    }
660}