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}