001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://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 015 * GNU Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://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.configuration; 029 030import java.time.Duration; 031 032import org.apache.commons.codec.digest.DigestUtils; 033 034import org.dom4j.Element; 035 036/** 037 * Central configuration for image cache retention and maintenance.<p> 038 * 039 * The configuration is optional for the classic RFS cache. If it is not present in {@code opencms-system.xml}, 040 * {@link #isConfigured()} returns {@code false} and the legacy image cache behavior applies. External FS and S3 041 * image caches require an explicit configuration.<p> 042 */ 043public class CmsImageCacheConfiguration { 044 045 /** Retention modes. */ 046 public enum RetentionMode { 047 048 /** Cache retention is managed outside OpenCms. */ 049 external("external"), 050 051 /** Entries expire based on their creation or replacement timestamp. */ 052 fixed("fixed"), 053 054 /** Entries are renewed when successfully used within the renewal window. */ 055 renewOnUse("renew-on-use"); 056 057 /** The XML value. */ 058 private String m_xmlValue; 059 060 /** 061 * Creates a retention mode.<p> 062 * 063 * @param xmlValue the XML value 064 */ 065 RetentionMode(String xmlValue) { 066 067 m_xmlValue = xmlValue; 068 } 069 070 /** 071 * Parses a retention mode.<p> 072 * 073 * @param value the XML value 074 * @return the retention mode 075 */ 076 public static RetentionMode fromXmlValue(String value) { 077 078 for (RetentionMode mode : values()) { 079 if (mode.getXmlValue().equals(value)) { 080 return mode; 081 } 082 } 083 throw new IllegalArgumentException("Unsupported image cache retention mode: " + value); 084 } 085 086 /** 087 * Returns the XML value.<p> 088 * 089 * @return the XML value 090 */ 091 public String getXmlValue() { 092 093 return m_xmlValue; 094 } 095 } 096 097 /** Default maximum number of deletes per maintenance run. */ 098 public static final int DEFAULT_CLEANUP_MAX_DELETES_PER_RUN = 10000; 099 100 /** Default maximum runtime per maintenance run. */ 101 public static final String DEFAULT_CLEANUP_MAX_RUNTIME = "PT5M"; 102 103 /** Default FS touch concurrency. */ 104 public static final int DEFAULT_FS_TOUCH_CONCURRENCY = 1; 105 106 /** Default RFS minimum touch interval. */ 107 public static final String DEFAULT_RFS_TOUCH_MINIMUM_INTERVAL = "PT1H"; 108 109 /** Default S3 copy concurrency. */ 110 public static final int DEFAULT_S3_COPY_CONCURRENCY = 2; 111 112 /** Default S3 delete batch size. */ 113 public static final int DEFAULT_S3_DELETE_BATCH_SIZE = 1000; 114 115 /** Default S3 delete concurrency. */ 116 public static final int DEFAULT_S3_DELETE_CONCURRENCY = 1; 117 118 /** Default maximum number of S3 copies per second. */ 119 public static final int DEFAULT_S3_MAX_COPIES_PER_SECOND = 20; 120 121 /** Default S3 renewal queue capacity. */ 122 public static final int DEFAULT_S3_RENEWAL_QUEUE_CAPACITY = 10000; 123 124 /** The maximum number of deletes per maintenance run. */ 125 private int m_cleanupMaxDeletesPerRun = DEFAULT_CLEANUP_MAX_DELETES_PER_RUN; 126 127 /** Indicates whether cleanup settings were explicitly configured. */ 128 private boolean m_cleanupConfigured; 129 130 /** The maximum runtime per maintenance run. */ 131 private Duration m_cleanupMaxRuntime = Duration.parse(DEFAULT_CLEANUP_MAX_RUNTIME); 132 133 /** The configured maximum runtime per maintenance run. */ 134 private String m_cleanupMaxRuntimeValue = DEFAULT_CLEANUP_MAX_RUNTIME; 135 136 /** Indicates whether the image cache configuration element was present. */ 137 private boolean m_configured; 138 139 /** The FS touch concurrency. */ 140 private int m_fsTouchConcurrency = DEFAULT_FS_TOUCH_CONCURRENCY; 141 142 /** Indicates whether FS settings were explicitly configured. */ 143 private boolean m_fsConfigured; 144 145 /** The maximum cache entry age. */ 146 private Duration m_maxAge; 147 148 /** The configured maximum cache entry age. */ 149 private String m_maxAgeValue; 150 151 /** The renewal jitter. */ 152 private Duration m_renewalJitter; 153 154 /** The configured renewal jitter value. */ 155 private String m_renewalJitterValue; 156 157 /** The renewal window. */ 158 private Duration m_renewalWindow; 159 160 /** The configured renewal window value. */ 161 private String m_renewalWindowValue; 162 163 /** The retention mode. */ 164 private RetentionMode m_retentionMode; 165 166 /** The RFS minimum touch interval. */ 167 private Duration m_rfsTouchMinimumInterval = Duration.parse(DEFAULT_RFS_TOUCH_MINIMUM_INTERVAL); 168 169 /** Indicates whether RFS settings were explicitly configured. */ 170 private boolean m_rfsConfigured; 171 172 /** The configured RFS minimum touch interval. */ 173 private String m_rfsTouchMinimumIntervalValue = DEFAULT_RFS_TOUCH_MINIMUM_INTERVAL; 174 175 /** The S3 copy concurrency. */ 176 private int m_s3CopyConcurrency = DEFAULT_S3_COPY_CONCURRENCY; 177 178 /** Indicates whether S3 settings were explicitly configured. */ 179 private boolean m_s3Configured; 180 181 /** The S3 delete batch size. */ 182 private int m_s3DeleteBatchSize = DEFAULT_S3_DELETE_BATCH_SIZE; 183 184 /** The S3 delete concurrency. */ 185 private int m_s3DeleteConcurrency = DEFAULT_S3_DELETE_CONCURRENCY; 186 187 /** The maximum number of S3 copies per second. */ 188 private int m_s3MaxCopiesPerSecond = DEFAULT_S3_MAX_COPIES_PER_SECOND; 189 190 /** The S3 renewal queue capacity. */ 191 private int m_s3RenewalQueueCapacity = DEFAULT_S3_RENEWAL_QUEUE_CAPACITY; 192 193 /** 194 * Creates a configuration populated from an explicit XML element.<p> 195 */ 196 public CmsImageCacheConfiguration() { 197 198 m_configured = true; 199 } 200 201 /** 202 * Creates a configuration.<p> 203 * 204 * @param configured whether an explicit XML element is present 205 */ 206 private CmsImageCacheConfiguration(boolean configured) { 207 208 m_configured = configured; 209 } 210 211 /** 212 * Creates the configuration representing the absence of an {@code imagecache} element.<p> 213 * 214 * @return the legacy configuration 215 */ 216 public static CmsImageCacheConfiguration createLegacyConfiguration() { 217 218 return new CmsImageCacheConfiguration(false); 219 } 220 221 /** 222 * Parses a duration.<p> 223 * 224 * @param name the value name 225 * @param value the value 226 * @param zeroAllowed whether zero is allowed 227 * @return the parsed duration 228 */ 229 private static Duration parsePositiveDuration(String name, String value, boolean zeroAllowed) { 230 231 if (value == null) { 232 throw new IllegalArgumentException(name + " must be configured."); 233 } 234 Duration result; 235 try { 236 result = Duration.parse(value.trim()); 237 } catch (RuntimeException e) { 238 throw new IllegalArgumentException(name + " is not a valid ISO-8601 duration: " + value, e); 239 } 240 if (result.isNegative() || (!zeroAllowed && result.isZero())) { 241 throw new IllegalArgumentException(name + " must be " + (zeroAllowed ? "non-negative" : "positive")); 242 } 243 return result; 244 } 245 246 /** 247 * Parses a positive integer.<p> 248 * 249 * @param name the value name 250 * @param value the value 251 * @return the parsed integer 252 */ 253 private static int parsePositiveInt(String name, String value) { 254 255 int result; 256 try { 257 result = Integer.parseInt(value.trim()); 258 } catch (RuntimeException e) { 259 throw new IllegalArgumentException(name + " is not a valid integer: " + value, e); 260 } 261 if (result <= 0) { 262 throw new IllegalArgumentException(name + " must be positive: " + value); 263 } 264 return result; 265 } 266 267 /** 268 * Returns a stable string representation for a nullable value.<p> 269 * 270 * @param value the value 271 * @return the string representation 272 */ 273 private static String valueOf(Object value) { 274 275 return value == null ? "" : value.toString(); 276 } 277 278 /** 279 * Appends this configuration to the given system configuration element.<p> 280 * 281 * Nothing is appended for the internal legacy configuration.<p> 282 * 283 * @param parent the system configuration element 284 * @return the appended image cache element, or {@code null} 285 */ 286 public Element appendToXml(Element parent) { 287 288 if (!isConfigured()) { 289 return null; 290 } 291 Element imageCacheElement = parent.addElement(CmsSystemConfiguration.N_IMAGECACHE); 292 Element retentionElement = imageCacheElement.addElement(CmsSystemConfiguration.N_RETENTION); 293 retentionElement.addAttribute(CmsSystemConfiguration.A_MODE, getRetentionMode().getXmlValue()); 294 if (getRetentionMode() != RetentionMode.external) { 295 retentionElement.addAttribute(CmsSystemConfiguration.A_MAX_AGE, getMaxAgeValue()); 296 } 297 if (getRetentionMode() == RetentionMode.renewOnUse) { 298 retentionElement.addAttribute(CmsSystemConfiguration.A_RENEWAL_WINDOW, getRenewalWindowValue()); 299 retentionElement.addAttribute(CmsSystemConfiguration.A_RENEWAL_JITTER, getRenewalJitterValue()); 300 } 301 if (m_cleanupConfigured) { 302 imageCacheElement.addElement(CmsSystemConfiguration.N_CLEANUP).addAttribute( 303 CmsSystemConfiguration.A_MAX_DELETES_PER_RUN, 304 Integer.toString(getCleanupMaxDeletesPerRun())).addAttribute( 305 CmsSystemConfiguration.A_MAX_RUNTIME, 306 getCleanupMaxRuntimeValue()); 307 } 308 if (m_rfsConfigured) { 309 imageCacheElement.addElement(CmsSystemConfiguration.N_RFS).addAttribute( 310 CmsSystemConfiguration.A_TOUCH_MINIMUM_INTERVAL, 311 getRfsTouchMinimumIntervalValue()); 312 } 313 if (m_fsConfigured) { 314 imageCacheElement.addElement(CmsSystemConfiguration.N_FS).addAttribute( 315 CmsSystemConfiguration.A_TOUCH_CONCURRENCY, 316 Integer.toString(getFsTouchConcurrency())); 317 } 318 if (m_s3Configured) { 319 imageCacheElement.addElement(CmsSystemConfiguration.N_S3).addAttribute( 320 CmsSystemConfiguration.A_DELETE_BATCH_SIZE, 321 Integer.toString(getS3DeleteBatchSize())).addAttribute( 322 CmsSystemConfiguration.A_DELETE_CONCURRENCY, 323 Integer.toString(getS3DeleteConcurrency())).addAttribute( 324 CmsSystemConfiguration.A_COPY_CONCURRENCY, 325 Integer.toString(getS3CopyConcurrency())).addAttribute( 326 CmsSystemConfiguration.A_MAX_COPIES_PER_SECOND, 327 Integer.toString(getS3MaxCopiesPerSecond())).addAttribute( 328 CmsSystemConfiguration.A_RENEWAL_QUEUE_CAPACITY, 329 Integer.toString(getS3RenewalQueueCapacity())); 330 } 331 return imageCacheElement; 332 } 333 334 /** 335 * Returns the maximum number of deletes per maintenance run.<p> 336 * 337 * @return the maximum number of deletes per maintenance run 338 */ 339 public int getCleanupMaxDeletesPerRun() { 340 341 return m_cleanupMaxDeletesPerRun; 342 } 343 344 /** 345 * Returns the maximum runtime per maintenance run.<p> 346 * 347 * @return the maximum runtime per maintenance run 348 */ 349 public Duration getCleanupMaxRuntime() { 350 351 return m_cleanupMaxRuntime; 352 } 353 354 /** 355 * Returns the configured maximum runtime value.<p> 356 * 357 * @return the configured maximum runtime value 358 */ 359 public String getCleanupMaxRuntimeValue() { 360 361 return m_cleanupMaxRuntimeValue; 362 } 363 364 /** 365 * Returns the FS touch concurrency.<p> 366 * 367 * @return the FS touch concurrency 368 */ 369 public int getFsTouchConcurrency() { 370 371 return m_fsTouchConcurrency; 372 } 373 374 /** 375 * Returns the maximum cache entry age.<p> 376 * 377 * @return the maximum cache entry age, or {@code null} for externally managed retention 378 */ 379 public Duration getMaxAge() { 380 381 return m_maxAge; 382 } 383 384 /** 385 * Returns the configured maximum cache entry age.<p> 386 * 387 * @return the configured maximum cache entry age, or {@code null} for externally managed retention 388 */ 389 public String getMaxAgeValue() { 390 391 return m_maxAgeValue; 392 } 393 394 /** 395 * Returns a stable fingerprint of the configured policy and maintenance settings.<p> 396 * 397 * @return the configuration fingerprint 398 */ 399 public String getPolicyFingerprint() { 400 401 return DigestUtils.sha256Hex( 402 isConfigured() 403 + "|" 404 + valueOf(m_retentionMode) 405 + "|" 406 + valueOf(m_maxAgeValue) 407 + "|" 408 + valueOf(m_retentionMode == RetentionMode.renewOnUse ? m_renewalWindowValue : null) 409 + "|" 410 + valueOf(m_retentionMode == RetentionMode.renewOnUse ? m_renewalJitterValue : null) 411 + "|" 412 + m_cleanupMaxDeletesPerRun 413 + "|" 414 + m_cleanupMaxRuntimeValue 415 + "|" 416 + m_rfsTouchMinimumIntervalValue 417 + "|" 418 + m_fsTouchConcurrency 419 + "|" 420 + m_s3DeleteBatchSize 421 + "|" 422 + m_s3DeleteConcurrency 423 + "|" 424 + m_s3CopyConcurrency 425 + "|" 426 + m_s3MaxCopiesPerSecond 427 + "|" 428 + m_s3RenewalQueueCapacity); 429 } 430 431 /** 432 * Returns the renewal jitter.<p> 433 * 434 * @return the renewal jitter 435 */ 436 public Duration getRenewalJitter() { 437 438 return m_renewalJitter; 439 } 440 441 /** 442 * Returns the effective renewal jitter value.<p> 443 * 444 * @return the effective renewal jitter value 445 */ 446 public String getRenewalJitterValue() { 447 448 return m_renewalJitterValue; 449 } 450 451 /** 452 * Returns the renewal window.<p> 453 * 454 * @return the renewal window 455 */ 456 public Duration getRenewalWindow() { 457 458 return m_renewalWindow; 459 } 460 461 /** 462 * Returns the effective renewal window value.<p> 463 * 464 * @return the effective renewal window value 465 */ 466 public String getRenewalWindowValue() { 467 468 return m_renewalWindowValue; 469 } 470 471 /** 472 * Returns the retention mode.<p> 473 * 474 * @return the retention mode, or {@code null} for the legacy configuration 475 */ 476 public RetentionMode getRetentionMode() { 477 478 return m_retentionMode; 479 } 480 481 /** 482 * Returns the RFS minimum touch interval.<p> 483 * 484 * @return the RFS minimum touch interval 485 */ 486 public Duration getRfsTouchMinimumInterval() { 487 488 return m_rfsTouchMinimumInterval; 489 } 490 491 /** 492 * Returns the configured RFS minimum touch interval.<p> 493 * 494 * @return the configured RFS minimum touch interval 495 */ 496 public String getRfsTouchMinimumIntervalValue() { 497 498 return m_rfsTouchMinimumIntervalValue; 499 } 500 501 /** 502 * Returns the S3 copy concurrency.<p> 503 * 504 * @return the S3 copy concurrency 505 */ 506 public int getS3CopyConcurrency() { 507 508 return m_s3CopyConcurrency; 509 } 510 511 /** 512 * Returns the S3 delete batch size.<p> 513 * 514 * @return the S3 delete batch size 515 */ 516 public int getS3DeleteBatchSize() { 517 518 return m_s3DeleteBatchSize; 519 } 520 521 /** 522 * Returns the S3 delete concurrency.<p> 523 * 524 * @return the S3 delete concurrency 525 */ 526 public int getS3DeleteConcurrency() { 527 528 return m_s3DeleteConcurrency; 529 } 530 531 /** 532 * Returns the maximum number of S3 copies per second.<p> 533 * 534 * @return the maximum number of S3 copies per second 535 */ 536 public int getS3MaxCopiesPerSecond() { 537 538 return m_s3MaxCopiesPerSecond; 539 } 540 541 /** 542 * Returns the S3 renewal queue capacity.<p> 543 * 544 * @return the S3 renewal queue capacity 545 */ 546 public int getS3RenewalQueueCapacity() { 547 548 return m_s3RenewalQueueCapacity; 549 } 550 551 /** 552 * Returns whether the configuration was explicitly configured.<p> 553 * 554 * @return whether the configuration was explicitly configured 555 */ 556 public boolean isConfigured() { 557 558 return m_configured; 559 } 560 561 /** 562 * Sets the cleanup configuration.<p> 563 * 564 * @param maxDeletesPerRun the maximum number of deletes per maintenance run 565 * @param maxRuntime the maximum runtime per maintenance run 566 */ 567 public void setCleanup(String maxDeletesPerRun, String maxRuntime) { 568 569 m_cleanupConfigured = true; 570 if (maxDeletesPerRun != null) { 571 m_cleanupMaxDeletesPerRun = parsePositiveInt("cleanup max-deletes-per-run", maxDeletesPerRun); 572 } 573 if (maxRuntime != null) { 574 m_cleanupMaxRuntime = parsePositiveDuration("cleanup max-runtime", maxRuntime, false); 575 m_cleanupMaxRuntimeValue = maxRuntime.trim(); 576 } 577 } 578 579 /** 580 * Sets the FS configuration.<p> 581 * 582 * @param touchConcurrency the touch concurrency 583 */ 584 public void setFs(String touchConcurrency) { 585 586 m_fsConfigured = true; 587 if (touchConcurrency != null) { 588 m_fsTouchConcurrency = parsePositiveInt("fs touch-concurrency", touchConcurrency); 589 } 590 } 591 592 /** 593 * Sets the retention policy.<p> 594 * 595 * @param mode the retention mode 596 * @param maxAge the maximum cache entry age 597 * @param renewalWindow the renewal window 598 * @param renewalJitter the renewal jitter 599 */ 600 public void setRetention(String mode, String maxAge, String renewalWindow, String renewalJitter) { 601 602 if (mode == null) { 603 throw new IllegalArgumentException("Image cache retention mode must be configured."); 604 } 605 m_retentionMode = RetentionMode.fromXmlValue(mode); 606 m_maxAge = null; 607 m_maxAgeValue = null; 608 m_renewalWindow = null; 609 m_renewalWindowValue = null; 610 m_renewalJitter = null; 611 m_renewalJitterValue = null; 612 if (maxAge != null) { 613 m_maxAge = parsePositiveDuration("retention max-age", maxAge, false); 614 m_maxAgeValue = maxAge.trim(); 615 } 616 if (renewalWindow != null) { 617 m_renewalWindow = parsePositiveDuration("retention renewal-window", renewalWindow, false); 618 m_renewalWindowValue = renewalWindow.trim(); 619 } 620 if (renewalJitter != null) { 621 m_renewalJitter = parsePositiveDuration("retention renewal-jitter", renewalJitter, true); 622 m_renewalJitterValue = renewalJitter.trim(); 623 } 624 } 625 626 /** 627 * Sets the RFS configuration.<p> 628 * 629 * @param touchMinimumInterval the minimum touch interval 630 */ 631 public void setRfs(String touchMinimumInterval) { 632 633 m_rfsConfigured = true; 634 if (touchMinimumInterval != null) { 635 m_rfsTouchMinimumInterval = parsePositiveDuration( 636 "rfs touch-minimum-interval", 637 touchMinimumInterval, 638 false); 639 m_rfsTouchMinimumIntervalValue = touchMinimumInterval.trim(); 640 } 641 } 642 643 /** 644 * Sets the S3 configuration.<p> 645 * 646 * @param deleteBatchSize the delete batch size 647 * @param deleteConcurrency the delete concurrency 648 * @param copyConcurrency the copy concurrency 649 * @param maxCopiesPerSecond the maximum number of copies per second 650 * @param renewalQueueCapacity the renewal queue capacity 651 */ 652 public void setS3( 653 String deleteBatchSize, 654 String deleteConcurrency, 655 String copyConcurrency, 656 String maxCopiesPerSecond, 657 String renewalQueueCapacity) { 658 659 m_s3Configured = true; 660 if (deleteBatchSize != null) { 661 m_s3DeleteBatchSize = parsePositiveInt("s3 delete-batch-size", deleteBatchSize); 662 if (m_s3DeleteBatchSize > 1000) { 663 throw new IllegalArgumentException("s3 delete-batch-size must not exceed 1000"); 664 } 665 } 666 if (deleteConcurrency != null) { 667 m_s3DeleteConcurrency = parsePositiveInt("s3 delete-concurrency", deleteConcurrency); 668 } 669 if (copyConcurrency != null) { 670 m_s3CopyConcurrency = parsePositiveInt("s3 copy-concurrency", copyConcurrency); 671 } 672 if (maxCopiesPerSecond != null) { 673 m_s3MaxCopiesPerSecond = parsePositiveInt("s3 max-copies-per-second", maxCopiesPerSecond); 674 } 675 if (renewalQueueCapacity != null) { 676 m_s3RenewalQueueCapacity = parsePositiveInt("s3 renewal-queue-capacity", renewalQueueCapacity); 677 } 678 } 679 680 /** 681 * Validates the complete configuration.<p> 682 */ 683 public void validate() { 684 685 if (!m_configured) { 686 return; 687 } 688 if (m_retentionMode == null) { 689 throw new IllegalArgumentException("The image cache retention mode must be configured."); 690 } 691 if (m_retentionMode == RetentionMode.external) { 692 if ((m_maxAge != null) || (m_renewalWindow != null) || (m_renewalJitter != null)) { 693 throw new IllegalArgumentException( 694 "Retention time settings must be absent when image cache retention is managed externally."); 695 } 696 return; 697 } 698 if (m_maxAge == null) { 699 throw new IllegalArgumentException("The image cache retention max-age must be configured."); 700 } 701 if (m_retentionMode == RetentionMode.renewOnUse) { 702 if (m_renewalWindow == null) { 703 throw new IllegalArgumentException( 704 "The image cache retention renewal-window must be configured for renew-on-use."); 705 } 706 if (m_renewalJitter == null) { 707 throw new IllegalArgumentException( 708 "The image cache retention renewal-jitter must be configured for renew-on-use."); 709 } 710 if (m_renewalWindow.compareTo(m_maxAge) >= 0) { 711 throw new IllegalArgumentException("The image cache renewal-window must be smaller than max-age."); 712 } 713 if (m_renewalJitter.compareTo(m_renewalWindow) > 0) { 714 throw new IllegalArgumentException( 715 "The image cache renewal-jitter must not exceed the renewal-window."); 716 } 717 } else if ((m_renewalWindow != null) || (m_renewalJitter != null)) { 718 throw new IllegalArgumentException("Renewal settings require image cache mode renew-on-use."); 719 } 720 } 721}