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 * <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 * > 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 <String> 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 <String> containing the categories to allow.<p> 307 * 308 * @return The List <String> 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}