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 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: 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.loader;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.configuration.CmsParameterConfiguration;
032import org.opencms.db.storage.CmsFsStorage;
033import org.opencms.db.storage.CmsS3Storage;
034import org.opencms.db.storage.CmsStorageManager;
035import org.opencms.db.storage.s3.CmsS3ClientConfiguration;
036import org.opencms.util.CmsStringUtil;
037
038import java.net.URI;
039import java.nio.file.Path;
040import java.nio.file.Paths;
041
042/**
043 * Creates the external image cache selected in {@code opencms.properties}.<p>
044 */
045public final class CmsImageCacheFactory {
046
047    /** Prefix for the direct image cache configuration. */
048    public static final String PARAM_IMAGE_CACHE_PREFIX = "storage.imagecache.";
049
050    /** Property selecting the image cache type. */
051    public static final String PARAM_IMAGE_CACHE_TYPE = PARAM_IMAGE_CACHE_PREFIX + "type";
052
053    /** Hidden constructor. */
054    private CmsImageCacheFactory() {
055
056        // utility class
057    }
058
059    /**
060     * Creates the configured external image cache.<p>
061     *
062     * @param configuration the runtime property configuration
063     * @return the configured cache, or <code>null</code> for the classic RFS image cache
064     * @throws CmsConfigurationException if the configuration is invalid or the cache is unavailable
065     */
066    public static I_CmsImageCache create(CmsParameterConfiguration configuration) throws CmsConfigurationException {
067
068        validateConfiguration(configuration);
069        String type = getType(configuration);
070        try {
071            if (CmsS3Storage.STORAGE_TYPE.equals(type)) {
072                return new CmsS3ImageCache(
073                    CmsStorageManager.createS3ClientConfiguration(configuration, PARAM_IMAGE_CACHE_PREFIX));
074            }
075            if (CmsFsStorage.STORAGE_TYPE.equals(type)) {
076                return new CmsFsImageCache(getFileSystemPath(configuration).toString());
077            }
078            return null;
079        } catch (Exception e) {
080            throw new CmsConfigurationException(Messages.get().container(Messages.ERR_IMAGE_CACHE_INIT_1, type), e);
081        }
082    }
083
084    /**
085     * Validates the image cache settings and separation from active and legacy data storage.<p>
086     *
087     * This does not open a cache or access S3, so offline maintenance tools can use the same validation.
088     *
089     * @param configuration the runtime property configuration
090     * @throws CmsConfigurationException if the configuration is invalid
091     */
092    public static void validateConfiguration(CmsParameterConfiguration configuration) throws CmsConfigurationException {
093
094        String type = getType(configuration);
095        if (type == null) {
096            return;
097        }
098        try {
099            if (CmsS3Storage.STORAGE_TYPE.equals(type)) {
100                validateS3Configuration(configuration);
101            } else if (CmsFsStorage.STORAGE_TYPE.equals(type)) {
102                validateFileSystemConfiguration(configuration);
103            } else {
104                throw new CmsConfigurationException(
105                    Messages.get().container(Messages.ERR_IMAGE_CACHE_CONFIG_TYPE_2, type, PARAM_IMAGE_CACHE_TYPE));
106            }
107        } catch (CmsConfigurationException e) {
108            throw e;
109        } catch (Exception e) {
110            throw new CmsConfigurationException(Messages.get().container(Messages.ERR_IMAGE_CACHE_INIT_1, type), e);
111        }
112    }
113
114    /** Returns the required image cache root path. */
115    private static Path getFileSystemPath(CmsParameterConfiguration configuration) {
116
117        String path = configuration.getString(PARAM_IMAGE_CACHE_PREFIX + "path", null);
118        if (CmsStringUtil.isEmptyOrWhitespaceOnly(path)) {
119            throw new IllegalArgumentException("Missing required configuration property: storage.imagecache.path");
120        }
121        return normalizePath(path.trim());
122    }
123
124    /** Returns the external cache type, or null for the classic local cache. */
125    private static String getType(CmsParameterConfiguration configuration) {
126
127        String type = configuration.getString(PARAM_IMAGE_CACHE_TYPE, null);
128        return CmsStringUtil.isEmptyOrWhitespaceOnly(type) ? null : type.trim();
129    }
130
131    /** Normalizes an S3 endpoint for comparisons. */
132    private static String normalizeEndpoint(String endpoint) {
133
134        String result = URI.create(endpoint.trim()).normalize().toString();
135        while (result.endsWith("/")) {
136            result = result.substring(0, result.length() - 1);
137        }
138        return result.toLowerCase(java.util.Locale.ROOT);
139    }
140
141    /** Normalizes a file system path. */
142    private static Path normalizePath(String path) {
143
144        return Paths.get(path).toAbsolutePath().normalize();
145    }
146
147    /** Ensures the FS cache does not overlap any data storage repository. */
148    private static void validateFileSystemConfiguration(CmsParameterConfiguration configuration)
149    throws CmsConfigurationException {
150
151        Path imageCachePath = getFileSystemPath(configuration);
152        for (String dataStorageId : CmsStorageManager.getDataStorageIds(configuration)) {
153            if (!CmsFsStorage.STORAGE_TYPE.equals(CmsStorageManager.getStorageType(configuration, dataStorageId))) {
154                continue;
155            }
156            Path dataStoragePath = normalizePath(
157                CmsStorageManager.getFileSystemStoragePath(configuration, dataStorageId));
158            if (imageCachePath.startsWith(dataStoragePath) || dataStoragePath.startsWith(imageCachePath)) {
159                throw new CmsConfigurationException(
160                    Messages.get().container(
161                        Messages.ERR_IMAGE_CACHE_CONFIG_FS_PATH_2,
162                        imageCachePath,
163                        dataStoragePath));
164            }
165        }
166    }
167
168    /** Ensures the S3 cache has its own bucket, separate from all data storage buckets. */
169    private static void validateS3Configuration(CmsParameterConfiguration configuration)
170    throws CmsConfigurationException {
171
172        CmsS3ClientConfiguration imageCache = CmsStorageManager.createS3ClientConfiguration(
173            configuration,
174            PARAM_IMAGE_CACHE_PREFIX);
175        for (String dataStorageId : CmsStorageManager.getDataStorageIds(configuration)) {
176            if (!CmsS3Storage.STORAGE_TYPE.equals(CmsStorageManager.getStorageType(configuration, dataStorageId))) {
177                continue;
178            }
179            CmsS3ClientConfiguration data = CmsStorageManager.createS3ClientConfiguration(
180                configuration,
181                dataStorageId,
182                null);
183            if (normalizeEndpoint(imageCache.getEndpoint()).equals(normalizeEndpoint(data.getEndpoint()))
184                && imageCache.getBucketName().equals(data.getBucketName())) {
185                throw new CmsConfigurationException(
186                    Messages.get().container(Messages.ERR_IMAGE_CACHE_CONFIG_S3_BUCKET_1, imageCache.getBucketName()));
187            }
188        }
189    }
190}