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, 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.db.storage;
029
030import org.opencms.configuration.CmsConfigurationException;
031import org.opencms.configuration.CmsParameterConfiguration;
032import org.opencms.configuration.CmsStoragePolicyConfiguration;
033import org.opencms.configuration.I_CmsConfigurationParameterHandler;
034import org.opencms.db.CmsDbContext;
035import org.opencms.db.CmsDbSqlException;
036import org.opencms.db.generic.CmsSqlManager;
037import org.opencms.db.storage.policy.CmsNoExternalStoragePolicy;
038import org.opencms.db.storage.policy.CmsStoragePolicyContext;
039import org.opencms.db.storage.policy.I_CmsStoragePolicy;
040import org.opencms.db.storage.s3.CmsS3ClientConfiguration;
041import org.opencms.file.CmsDataAccessException;
042import org.opencms.file.I_CmsFileContentStreamHandler;
043import org.opencms.main.CmsLog;
044import org.opencms.util.CmsStringUtil;
045
046import java.io.ByteArrayInputStream;
047import java.io.OutputStream;
048import java.lang.reflect.Constructor;
049import java.security.MessageDigest;
050import java.security.NoSuchAlgorithmException;
051import java.sql.Connection;
052import java.sql.PreparedStatement;
053import java.sql.ResultSet;
054import java.sql.SQLException;
055import java.util.ArrayList;
056import java.util.LinkedHashMap;
057import java.util.List;
058import java.util.Map;
059
060import org.apache.commons.logging.Log;
061
062/**
063 * Central coordinator of the OpenCms deduplicating media storage engine (DMSE).<p>
064 *
065 * The DMSE stores binary resource content in database, file system or
066 * S3-compatible backends, using SHA-512 content hashes for deduplication.
067 * Storage policies determine which contents remain in the regular VFS
068 * content columns and which are stored in a backend.<p>
069 *
070 * This manager evaluates policies, coordinates backend access and checks
071 * content references before deleting stored blobs.<p>
072 */
073public class CmsStorageManager implements AutoCloseable {
074
075    /**
076     * Container for storage evaluation results, used by VFS drivers to determine
077     * which database columns to update.<p>
078     *
079     * Note: This result follows a "mutual exclusivity" principle:
080     * <ul>
081     * <li>If the content is stored locally, {@link #getFileContent()} contains the
082     * data, while {@link #getHash()} and {@link #getStorage()} are
083     * <code>null</code>.</li>
084     * <li>If the content is offloaded, {@link #getFileContent()} is empty
085     * (zero-length array), while {@link #getHash()} and {@link #getStorage()}
086     * contain the reference details.</li>
087     * </ul>
088     */
089    public static class StorageResult {
090
091        /** Value for the FILE_CONTENT column. */
092        private final byte[] m_fileContent;
093
094        /** Value for the STORAGE column. */
095        private final String m_storage;
096
097        /** Value for the HASH column. */
098        private final String m_hash;
099
100        /**
101         * Constructor for local storage.<p>
102         *
103         * @param fileContent the raw bytes to be stored in the content table
104         */
105        public StorageResult(byte[] fileContent) {
106
107            m_fileContent = fileContent;
108            m_storage = null;
109            m_hash = null;
110        }
111
112        /**
113         * Constructor for external storage.<p>
114         *
115         * @param storage the stable storage identifier stored in the STORAGE column
116         * @param hash the SHA-512 content hash stored in the HASH column
117         */
118        public StorageResult(String storage, String hash) {
119
120            m_fileContent = new byte[0];
121            m_storage = storage;
122            m_hash = hash;
123        }
124
125        /**
126         * Returns the content for the local FILE_CONTENT column.
127         * <p>
128         *
129         * @return the bytes
130         */
131        public byte[] getFileContent() {
132
133            return m_fileContent;
134        }
135
136        /**
137         * Returns the hash for the HASH column.<p>
138         *
139         * @return the hash string
140         */
141        public String getHash() {
142
143            return m_hash;
144        }
145
146        /**
147         * Returns the stable storage identifier for the STORAGE column.<p>
148         *
149         * @return the storage identifier
150         */
151        public String getStorage() {
152
153            return m_storage;
154        }
155    }
156
157    /** The log object for this class. */
158    private static final Log LOG = CmsLog.getLog(CmsStorageManager.class);
159
160    /** Prefix for storage properties. */
161    public static final String PARAM_STORAGE_ACTIVE = "storage.active";
162
163    /** Prefix for the legacy storage list. */
164    public static final String PARAM_STORAGE_LEGACY = "storage.legacy";
165
166    /** Prefix for backend-specific configuration. */
167    public static final String PARAM_STORAGE_BACKEND_PREFIX = "storage.backend.";
168
169    /** Property name for backend type. */
170    private static final String PARAM_TYPE = "type";
171
172    /** Property name for backend implementation class. */
173    private static final String PARAM_CLASS = "class";
174
175    /** Property name for the S3 bucket. */
176    private static final String PARAM_BUCKET = "bucket";
177
178    /** Property name for the S3 endpoint. */
179    private static final String PARAM_ENDPOINT = "endpoint";
180
181    /** Property name for the S3 access key. */
182    private static final String PARAM_ACCESS_KEY = "accessKey";
183
184    /** Property name for the S3 secret key. */
185    private static final String PARAM_SECRET_KEY = "secretKey";
186
187    /** Property name for path-style access. */
188    private static final String PARAM_PATH_STYLE = "pathStyle";
189
190    /** Property name for the S3 region. */
191    private static final String PARAM_REGION = "region";
192
193    /** Property name for the S3 complete API call timeout. */
194    private static final String PARAM_API_CALL_TIMEOUT = "apiCallTimeout";
195
196    /** Property name for the S3 single API call attempt timeout. */
197    private static final String PARAM_API_CALL_ATTEMPT_TIMEOUT = "apiCallAttemptTimeout";
198
199    /** Property name for the S3 connection timeout. */
200    private static final String PARAM_CONNECTION_TIMEOUT = "connectionTimeout";
201
202    /** Property name for the S3 connection acquisition timeout. */
203    private static final String PARAM_CONNECTION_ACQUISITION_TIMEOUT = "connectionAcquisitionTimeout";
204
205    /** Property name for the S3 maximum number of pooled connections. */
206    private static final String PARAM_MAX_CONNECTIONS = "maxConnections";
207
208    /** Property name for the S3 maximum number of retries. */
209    private static final String PARAM_MAX_RETRIES = "maxRetries";
210
211    /** Property name for the S3 socket timeout. */
212    private static final String PARAM_SOCKET_TIMEOUT = "socketTimeout";
213
214    /** Property name for the FS repository path. */
215    private static final String PARAM_PATH = "path";
216
217    /** The active write storage implementation. */
218    private I_CmsStorage m_activeStorage;
219
220    /** The active write storage identifier. */
221    private String m_activeStorageId;
222
223    /** The configured storage policy. */
224    private I_CmsStoragePolicy m_storagePolicy;
225
226    /** The configured storages keyed by their stable identifier. */
227    private Map<String, I_CmsStorage> m_storages;
228
229    /** The configured backend id for each storage identifier. */
230    private Map<String, String> m_storageBackendIds;
231
232    /** The SQL manager. */
233    private final CmsSqlManager m_sqlManager;
234
235    /**
236     * Creates a new storage manager.<p>
237     *
238     * @param sqlManager the SQL manager
239     * @param configuration the runtime property configuration
240     */
241    public CmsStorageManager(CmsSqlManager sqlManager, CmsParameterConfiguration configuration) {
242
243        this(sqlManager, configuration, null);
244    }
245
246    /**
247     * Creates a new storage manager.<p>
248     *
249     * @param sqlManager the SQL manager
250     * @param configuration the runtime property configuration
251     * @param storagePolicyConfiguration the storage policy configuration
252     */
253    public CmsStorageManager(
254        CmsSqlManager sqlManager,
255        CmsParameterConfiguration configuration,
256        CmsStoragePolicyConfiguration storagePolicyConfiguration) {
257
258        m_sqlManager = sqlManager;
259        m_storagePolicy = createStoragePolicy(storagePolicyConfiguration);
260        initStorages(configuration);
261    }
262
263    /**
264     * Creates an S3 client configuration from properties below the given prefix.<p>
265     *
266     * @param configuration the runtime property configuration
267     * @param prefix the property prefix, including its trailing dot
268     * @return the S3 client configuration
269     */
270    public static CmsS3ClientConfiguration createS3ClientConfiguration(
271        CmsParameterConfiguration configuration,
272        String prefix) {
273
274        return parseS3ClientConfiguration(configuration, prefix, null);
275    }
276
277    /**
278     * Creates an S3 client configuration from a configured storage backend.<p>
279     *
280     * @param configuration the runtime property configuration
281     * @param backendId the configured storage backend id, or <code>null</code> to use the active backend
282     * @param bucketOverride the bucket override, or <code>null</code> to use the backend bucket
283     * @return the S3 client configuration
284     */
285    public static CmsS3ClientConfiguration createS3ClientConfiguration(
286        CmsParameterConfiguration configuration,
287        String backendId,
288        String bucketOverride) {
289
290        String storageId = backendId;
291        if (CmsStringUtil.isEmptyOrWhitespaceOnly(storageId)) {
292            storageId = getActiveStorageId(configuration);
293        }
294        String prefix = PARAM_STORAGE_BACKEND_PREFIX + storageId + ".";
295        String type = getStorageType(configuration, storageId);
296        if (!CmsS3Storage.STORAGE_TYPE.equals(type)) {
297            throw new IllegalArgumentException(
298                Messages.get().getBundle().key(Messages.ERR_STORAGE_UNSUPPORTED_TYPE_2, type, storageId));
299        }
300        return parseS3ClientConfiguration(configuration, prefix, bucketOverride);
301    }
302
303    /**
304     * Returns the active storage backend id from the runtime property configuration.<p>
305     *
306     * @param configuration the runtime property configuration
307     * @return the active storage backend id
308     */
309    public static String getActiveStorageId(CmsParameterConfiguration configuration) {
310
311        return configuration.getString(PARAM_STORAGE_ACTIVE, I_CmsDbStorage.STORAGE_TYPE).trim();
312    }
313
314    /**
315     * Returns the active storage backend type from the runtime property configuration.<p>
316     *
317     * @param configuration the runtime property configuration
318     * @return the active storage backend type
319     */
320    public static String getActiveStorageType(CmsParameterConfiguration configuration) {
321
322        return getStorageType(configuration, getActiveStorageId(configuration));
323    }
324
325    /**
326     * Returns the backend identifiers used for VFS data storage.<p>
327     *
328     * @param configuration the runtime property configuration
329     * @return the active backend followed by all distinct legacy backends
330     */
331    public static List<String> getDataStorageIds(CmsParameterConfiguration configuration) {
332
333        List<String> result = new ArrayList<String>();
334        String activeStorageId = getActiveStorageId(configuration);
335        result.add(activeStorageId);
336        for (String legacyStorage : parseCommaSeparatedList(configuration.getString(PARAM_STORAGE_LEGACY, ""))) {
337            if (!result.contains(legacyStorage)) {
338                result.add(legacyStorage);
339            }
340        }
341        return result;
342    }
343
344    /**
345     * Returns the configured file system repository path.<p>
346     *
347     * @param configuration the runtime property configuration
348     * @param storageId the storage backend id
349     * @return the configured repository path
350     */
351    public static String getFileSystemStoragePath(CmsParameterConfiguration configuration, String storageId) {
352
353        String type = getStorageType(configuration, storageId);
354        if (!CmsFsStorage.STORAGE_TYPE.equals(type)) {
355            throw new IllegalArgumentException(
356                Messages.get().getBundle().key(Messages.ERR_STORAGE_UNSUPPORTED_TYPE_2, type, storageId));
357        }
358        return requireConfigValue(
359            configuration,
360            PARAM_STORAGE_BACKEND_PREFIX + storageId + "." + PARAM_PATH,
361            storageId);
362    }
363
364    /**
365     * Returns the type for a configured storage backend.<p>
366     *
367     * @param configuration the runtime property configuration
368     * @param storageId the storage backend id
369     * @return the storage backend type
370     */
371    public static String getStorageType(CmsParameterConfiguration configuration, String storageId) {
372
373        if (I_CmsDbStorage.STORAGE_TYPE.equals(storageId)) {
374            return I_CmsDbStorage.STORAGE_TYPE;
375        }
376        String prefix = PARAM_STORAGE_BACKEND_PREFIX + storageId + ".";
377        return configuration.getString(prefix + PARAM_TYPE, null);
378    }
379
380    /**
381     * Parses a comma-separated backend list.
382     *
383     * @param value the raw configuration value
384     * @return the parsed backend identifiers
385     */
386    private static List<String> parseCommaSeparatedList(String value) {
387
388        List<String> result = new ArrayList<String>();
389        if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
390            return result;
391        }
392        for (String part : value.split(",")) {
393            String trimmed = part.trim();
394            if (!trimmed.isEmpty() && !result.contains(trimmed)) {
395                result.add(trimmed);
396            }
397        }
398        return result;
399    }
400
401    /**
402     * Reads the shared S3 connection settings.<p>
403     *
404     * @param configuration the runtime property configuration
405     * @param prefix the property prefix, including its trailing dot
406     * @param bucketOverride an optional bucket override
407     * @return the S3 client configuration
408     */
409    private static CmsS3ClientConfiguration parseS3ClientConfiguration(
410        CmsParameterConfiguration configuration,
411        String prefix,
412        String bucketOverride) {
413
414        String configurationName = prefix.substring(0, prefix.length() - 1);
415        String endpoint = requireConfigValue(configuration, prefix + PARAM_ENDPOINT, configurationName);
416        String bucket = CmsStringUtil.isNotEmptyOrWhitespaceOnly(bucketOverride)
417        ? bucketOverride.trim()
418        : requireConfigValue(configuration, prefix + PARAM_BUCKET, configurationName);
419        String accessKey = requireConfigValue(configuration, prefix + PARAM_ACCESS_KEY, configurationName);
420        String secretKey = requireConfigValue(configuration, prefix + PARAM_SECRET_KEY, configurationName);
421        boolean pathStyle = configuration.getBoolean(prefix + PARAM_PATH_STYLE, true);
422        int connectionTimeout = configuration.getInteger(
423            prefix + PARAM_CONNECTION_TIMEOUT,
424            CmsS3ClientConfiguration.DEFAULT_CONNECTION_TIMEOUT);
425        int connectionAcquisitionTimeout = configuration.getInteger(
426            prefix + PARAM_CONNECTION_ACQUISITION_TIMEOUT,
427            CmsS3ClientConfiguration.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT);
428        int socketTimeout = configuration.getInteger(
429            prefix + PARAM_SOCKET_TIMEOUT,
430            CmsS3ClientConfiguration.DEFAULT_SOCKET_TIMEOUT);
431        int apiCallAttemptTimeout = configuration.getInteger(
432            prefix + PARAM_API_CALL_ATTEMPT_TIMEOUT,
433            CmsS3ClientConfiguration.DEFAULT_API_CALL_ATTEMPT_TIMEOUT);
434        int apiCallTimeout = configuration.getInteger(
435            prefix + PARAM_API_CALL_TIMEOUT,
436            CmsS3ClientConfiguration.DEFAULT_API_CALL_TIMEOUT);
437        int maxRetries = configuration.getInteger(
438            prefix + PARAM_MAX_RETRIES,
439            CmsS3ClientConfiguration.DEFAULT_MAX_RETRIES);
440        int maxConnections = configuration.getInteger(
441            prefix + PARAM_MAX_CONNECTIONS,
442            CmsS3ClientConfiguration.DEFAULT_MAX_CONNECTIONS);
443        String region = configuration.getString(prefix + PARAM_REGION, CmsS3ClientConfiguration.DEFAULT_REGION);
444        return new CmsS3ClientConfiguration(
445            endpoint,
446            bucket,
447            accessKey,
448            secretKey,
449            pathStyle,
450            region,
451            connectionTimeout,
452            socketTimeout,
453            apiCallAttemptTimeout,
454            apiCallTimeout,
455            maxRetries,
456            maxConnections,
457            connectionAcquisitionTimeout);
458    }
459
460    /**
461     * Reads a required configuration value.
462     *
463     * @param configuration the runtime property configuration
464     * @param key the property key
465     * @param storageId the backend identifier
466     *
467     * @return the configured value
468     */
469    private static String requireConfigValue(CmsParameterConfiguration configuration, String key, String storageId) {
470
471        String value = configuration.getString(key, null);
472        if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
473            throw new IllegalArgumentException(
474                Messages.get().getBundle().key(Messages.ERR_STORAGE_MISSING_CONFIG_2, key, storageId));
475        }
476        return value.trim();
477    }
478
479    /**
480     * Closes all configured storage backends.<p>
481     *
482     * @throws Exception if closing one or more storage backends fails
483     */
484    public void close() throws Exception {
485
486        Exception failure = null;
487        if (m_storages == null) {
488            return;
489        }
490        for (I_CmsStorage storage : m_storages.values()) {
491            try {
492                storage.close();
493            } catch (Exception e) {
494                if (LOG.isErrorEnabled()) {
495                    LOG.error(
496                        Messages.get().getBundle().key(
497                            Messages.LOG_STORAGE_CLOSE_FAILED_1,
498                            storage.getStorageIdentifier()),
499                        e);
500                }
501                if (failure == null) {
502                    failure = e;
503                } else {
504                    failure.addSuppressed(e);
505                }
506            }
507        }
508        if (failure != null) {
509            throw failure;
510        }
511    }
512
513    /**
514     * Deletes content from external storage if it is no longer referenced in any
515     * content table.<p>
516     *
517     * @param dbc the database context
518     * @param storage the stable storage identifier stored in the STORAGE column
519     * @param hash the hash of the content to delete
520     */
521    public void deleteContent(CmsDbContext dbc, String storage, String hash) {
522
523        if (CmsStringUtil.isEmpty(hash) || CmsStringUtil.isEmpty(storage)) {
524            return;
525        }
526        try {
527            if (!isContentReferenced(dbc, storage, hash)) {
528                if (LOG.isDebugEnabled()) {
529                    LOG.debug("Blob with hash " + hash + " is not referenced in " + storage + " any more. Delete it.");
530                }
531                try {
532                    I_CmsStorage targetStorage = m_storages.get(storage);
533                    if (targetStorage != null) {
534                        targetStorage.deleteContent(dbc, hash);
535                    } else {
536                        LOG.error(
537                            Messages.get().getBundle().key(Messages.LOG_STORAGE_DELETE_BACKEND_MISSING_1, storage));
538                    }
539                } catch (Exception e) {
540                    LOG.error(Messages.get().getBundle().key(Messages.LOG_STORAGE_DELETE_FAILED_2, hash, storage), e);
541                }
542            } else {
543                if (LOG.isDebugEnabled()) {
544                    LOG.debug("Blob with hash " + hash + " is still referenced. Skipping deletion.");
545                }
546            }
547        } catch (CmsDbSqlException e) {
548            LOG.error(Messages.get().getBundle().key(Messages.LOG_STORAGE_REFERENCES_FAILED_1, hash), e);
549        }
550    }
551
552    /**
553     * Returns a delivery-capable storage backend by its stable storage identifier.<p>
554     *
555     * @param storage the stable storage identifier
556     * @return the delivery-capable storage backend, or <code>null</code> if the backend does not support delivery
557     * @throws CmsStorageException if no backend is configured for the storage identifier
558     */
559    public I_CmsStorageDelivery getDeliveryStorage(String storage) throws CmsStorageException {
560
561        if (!isActiveStorage(storage)) {
562            return null;
563        }
564        I_CmsStorage configuredStorage = getConfiguredStorageForRead(storage);
565        if (configuredStorage instanceof I_CmsStorageDelivery) {
566            return (I_CmsStorageDelivery)configuredStorage;
567        }
568        return null;
569    }
570
571    /**
572     * Returns a configured storage backend by its stable storage identifier.<p>
573     *
574     * @param storage the stable storage identifier
575     * @return the configured storage backend, or null if no such backend is configured
576     */
577    public I_CmsStorage getStorage(String storage) {
578
579        return m_storages.get(storage);
580    }
581
582    /**
583     * Returns if the given stable storage identifier belongs to the active storage backend.<p>
584     *
585     * @param storage the stable storage identifier
586     *
587     * @return <code>true</code> if the storage identifier belongs to the active backend
588     */
589    public boolean isActiveStorage(String storage) {
590
591        return (m_activeStorage != null) && m_activeStorage.getStorageIdentifier().equals(storage);
592    }
593
594    /**
595     * Loads the content either from the provided local bytes or from external
596     * storage.<p>
597     *
598     * @param dbc the database context
599     * @param contents the local bytes (from FILE_CONTENT column)
600     * @param storage the stable storage identifier stored in the STORAGE column
601     * @param hash the SHA-512 content hash stored in the HASH column
602     * @return the raw bytes of the file
603     * @throws CmsStorageException if external storage content can not be loaded
604     */
605    public byte[] loadContent(CmsDbContext dbc, byte[] contents, String storage, String hash)
606    throws CmsStorageException {
607
608        if (CmsStringUtil.isNotEmpty(storage) && CmsStringUtil.isNotEmpty(hash)) {
609            try {
610                I_CmsStorage configuredStorage = getConfiguredStorageForRead(storage);
611                byte[] result = configuredStorage.loadContent(dbc, hash);
612                if (result == null) {
613                    throw new CmsStorageBlobNotFoundException(
614                        Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_MISSING_2, storage, hash));
615                }
616                return result;
617            } catch (CmsStorageException e) {
618                throw e;
619            } catch (Exception e) {
620                throw new CmsStorageException(
621                    Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_LOAD_FAILED_2, storage, hash),
622                    e);
623            }
624        } else {
625            return contents;
626        }
627    }
628
629    /**
630     * Loads content either from the provided local bytes or from external storage and passes it to an input stream handler.<p>
631     *
632     * @param dbc the database context
633     * @param contents the local bytes (from FILE_CONTENT column)
634     * @param storage the stable storage identifier stored in the STORAGE column
635     * @param hash the SHA-512 content hash stored in the HASH column
636     * @param handler the stream handler
637     * @throws CmsStorageException if external storage content can not be loaded or handled
638     */
639    public void loadContentFrom(
640        CmsDbContext dbc,
641        byte[] contents,
642        String storage,
643        String hash,
644        I_CmsFileContentStreamHandler handler)
645    throws CmsStorageException {
646
647        if (CmsStringUtil.isNotEmpty(storage) && CmsStringUtil.isNotEmpty(hash)) {
648            try {
649                I_CmsStorage configuredStorage = getConfiguredStorageForRead(storage);
650                configuredStorage.loadContentFrom(dbc, hash, handler);
651            } catch (CmsStorageException e) {
652                throw e;
653            } catch (Exception e) {
654                throw new CmsStorageException(
655                    Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_LOAD_FAILED_2, storage, hash),
656                    e);
657            }
658        } else {
659            try {
660                if (contents != null) {
661                    try (ByteArrayInputStream in = new ByteArrayInputStream(contents)) {
662                        handler.read(in);
663                    }
664                }
665            } catch (Exception e) {
666                throw new CmsStorageException(
667                    Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_LOAD_FAILED_2, storage, hash),
668                    e);
669            }
670        }
671    }
672
673    /**
674     * Loads content either from the provided local bytes or from external storage into an output stream.<p>
675     *
676     * @param dbc the database context
677     * @param contents the local bytes (from FILE_CONTENT column)
678     * @param storage the stable storage identifier stored in the STORAGE column
679     * @param hash the SHA-512 content hash stored in the HASH column
680     * @param out the output stream to write to
681     * @throws CmsStorageException if external storage content can not be loaded or written
682     */
683    public void loadContentTo(CmsDbContext dbc, byte[] contents, String storage, String hash, OutputStream out)
684    throws CmsStorageException {
685
686        if (CmsStringUtil.isNotEmpty(storage) && CmsStringUtil.isNotEmpty(hash)) {
687            try {
688                I_CmsStorage configuredStorage = getConfiguredStorageForRead(storage);
689                configuredStorage.loadContentTo(dbc, hash, out);
690            } catch (CmsStorageException e) {
691                throw e;
692            } catch (Exception e) {
693                throw new CmsStorageException(
694                    Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_LOAD_FAILED_2, storage, hash),
695                    e);
696            }
697        } else {
698            try {
699                if (contents != null) {
700                    out.write(contents);
701                }
702            } catch (Exception e) {
703                throw new CmsStorageException(
704                    Messages.get().getBundle().key(Messages.ERR_STORAGE_BLOB_LOAD_FAILED_2, storage, hash),
705                    e);
706            }
707        }
708    }
709
710    /**
711     * Prepares content for storage by checking the policy and offloading to
712     * external storage if required.<p>
713     *
714     * @param dbc the database context
715     * @param context the storage policy context
716     * @return the storage result containing either the bytes or the hash
717     * @throws CmsDataAccessException if storing required external content fails
718     */
719    public StorageResult prepareContent(CmsDbContext dbc, CmsStoragePolicyContext context)
720    throws CmsDataAccessException {
721
722        if (m_storagePolicy.isExternalStorageRequired(context)) {
723            byte[] rawData = context.getContent();
724            String hash = null;
725            try {
726                hash = calculateSha512(rawData);
727                m_activeStorage.storeContent(dbc, hash, rawData);
728                return new StorageResult(m_activeStorage.getStorageIdentifier(), hash);
729            } catch (Exception e) {
730                throw new CmsDataAccessException(
731                    Messages.get().container(
732                        Messages.ERR_STORAGE_EXTERNAL_WRITE_2,
733                        hash,
734                        m_activeStorage.getStorageIdentifier()),
735                    e);
736            }
737        }
738        return new StorageResult(context.getContent());
739    }
740
741    /**
742     * Validates all configured storage backends.<p>
743     *
744     * @param dbc the database context
745     * @throws Exception if a configured storage backend is not available
746     */
747    public void validateStorages(CmsDbContext dbc) throws Exception {
748
749        for (I_CmsStorage storage : m_storages.values()) {
750            String identifier = storage.getStorageIdentifier();
751            String storageId = m_storageBackendIds.get(identifier);
752            String role = getStorageRole(storageId);
753            if (CmsLog.INIT.isInfoEnabled()) {
754                CmsLog.INIT.info(
755                    Messages.get().getBundle().key(
756                        Messages.INIT_STORAGE_BACKEND_CHECKING_3,
757                        storageId,
758                        identifier,
759                        role));
760            }
761            try {
762                storage.validateAvailable(dbc);
763            } catch (Exception e) {
764                String reason = getFailureReason(e);
765                if (CmsLog.INIT.isErrorEnabled()) {
766                    CmsLog.INIT.error(
767                        Messages.get().getBundle().key(
768                            Messages.INIT_STORAGE_BACKEND_FAILED_4,
769                            new Object[] {storageId, identifier, role, reason}),
770                        e);
771                }
772                throw e;
773            }
774            if (CmsLog.INIT.isInfoEnabled()) {
775                CmsLog.INIT.info(
776                    Messages.get().getBundle().key(Messages.INIT_STORAGE_BACKEND_OK_3, storageId, identifier, role));
777            }
778        }
779    }
780
781    /**
782     * Calculates the SHA-512 hash of the given content and returns it as a hex
783     * string.<p>
784     *
785     * @param content the content to hash
786     * @return the SHA-512 hex string
787     * @throws NoSuchAlgorithmException if the hashing algorithm is not available
788     */
789    private String calculateSha512(byte[] content) throws NoSuchAlgorithmException {
790
791        MessageDigest md = MessageDigest.getInstance("SHA-512");
792        byte[] hash = md.digest(content);
793        StringBuilder sb = new StringBuilder(128);
794        for (byte b : hash) {
795            String hex = Integer.toHexString(0xff & b);
796            if (hex.length() == 1) {
797                sb.append('0'); // pad with leading zero
798            }
799            sb.append(hex);
800        }
801        return sb.toString();
802    }
803
804    /**
805     * Creates the database storage backend.<p>
806     *
807     * @param configuredClassName the configured implementation class, or null
808     * @return the database storage backend
809     */
810    private I_CmsDbStorage createDbStorage(String configuredClassName) {
811
812        String className = configuredClassName;
813        if (CmsStringUtil.isEmptyOrWhitespaceOnly(className)) {
814            className = getDefaultDbStorageClassName();
815        }
816        try {
817            Class<?> storageClass = Class.forName(className);
818            if (!I_CmsDbStorage.class.isAssignableFrom(storageClass)) {
819                throw new IllegalArgumentException(
820                    Messages.get().getBundle().key(
821                        Messages.ERR_STORAGE_DB_CLASS_INVALID_2,
822                        className,
823                        I_CmsDbStorage.class.getName()));
824            }
825            Constructor<?> constructor = storageClass.getConstructor(CmsSqlManager.class);
826            return (I_CmsDbStorage)constructor.newInstance(m_sqlManager);
827        } catch (IllegalArgumentException e) {
828            throw e;
829        } catch (Exception e) {
830            throw new IllegalArgumentException(
831                Messages.get().getBundle().key(Messages.ERR_STORAGE_DB_CLASS_CREATE_1, className),
832                e);
833        }
834    }
835
836    /**
837     * Creates a storage backend from the runtime property configuration.
838     *
839     * @param id the configured backend id
840     * @param configuration the runtime property configuration
841     * @return the storage backend
842     */
843    private I_CmsStorage createStorage(String id, CmsParameterConfiguration configuration) {
844
845        String prefix = PARAM_STORAGE_BACKEND_PREFIX + id + ".";
846        if (I_CmsDbStorage.STORAGE_TYPE.equals(id)) {
847            return createDbStorage(configuration.getString(prefix + PARAM_CLASS, null));
848        }
849        String type = configuration.getString(prefix + PARAM_TYPE, null);
850        if (CmsStringUtil.isEmptyOrWhitespaceOnly(type)) {
851            throw new IllegalArgumentException(Messages.get().getBundle().key(Messages.ERR_STORAGE_MISSING_TYPE_1, id));
852        }
853        if (I_CmsDbStorage.STORAGE_TYPE.equals(type)) {
854            throw new IllegalArgumentException(
855                Messages.get().getBundle().key(Messages.ERR_STORAGE_DB_RESERVED_ID_1, id));
856        }
857        if (CmsS3Storage.STORAGE_TYPE.equals(type)) {
858            String endpoint = requireConfigValue(configuration, prefix + PARAM_ENDPOINT, id);
859            String bucket = requireConfigValue(configuration, prefix + PARAM_BUCKET, id);
860            String accessKey = requireConfigValue(configuration, prefix + PARAM_ACCESS_KEY, id);
861            String secretKey = requireConfigValue(configuration, prefix + PARAM_SECRET_KEY, id);
862            boolean pathStyle = configuration.getBoolean(prefix + PARAM_PATH_STYLE, true);
863            int connectionTimeout = configuration.getInteger(
864                prefix + PARAM_CONNECTION_TIMEOUT,
865                CmsS3ClientConfiguration.DEFAULT_CONNECTION_TIMEOUT);
866            int connectionAcquisitionTimeout = configuration.getInteger(
867                prefix + PARAM_CONNECTION_ACQUISITION_TIMEOUT,
868                CmsS3ClientConfiguration.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT);
869            int socketTimeout = configuration.getInteger(
870                prefix + PARAM_SOCKET_TIMEOUT,
871                CmsS3ClientConfiguration.DEFAULT_SOCKET_TIMEOUT);
872            int apiCallAttemptTimeout = configuration.getInteger(
873                prefix + PARAM_API_CALL_ATTEMPT_TIMEOUT,
874                CmsS3ClientConfiguration.DEFAULT_API_CALL_ATTEMPT_TIMEOUT);
875            int apiCallTimeout = configuration.getInteger(
876                prefix + PARAM_API_CALL_TIMEOUT,
877                CmsS3ClientConfiguration.DEFAULT_API_CALL_TIMEOUT);
878            int maxRetries = configuration.getInteger(
879                prefix + PARAM_MAX_RETRIES,
880                CmsS3ClientConfiguration.DEFAULT_MAX_RETRIES);
881            int maxConnections = configuration.getInteger(
882                prefix + PARAM_MAX_CONNECTIONS,
883                CmsS3ClientConfiguration.DEFAULT_MAX_CONNECTIONS);
884            String region = configuration.getString(prefix + PARAM_REGION, CmsS3ClientConfiguration.DEFAULT_REGION);
885            return new CmsS3Storage(
886                id,
887                new CmsS3ClientConfiguration(
888                    endpoint,
889                    bucket,
890                    accessKey,
891                    secretKey,
892                    pathStyle,
893                    region,
894                    connectionTimeout,
895                    socketTimeout,
896                    apiCallAttemptTimeout,
897                    apiCallTimeout,
898                    maxRetries,
899                    maxConnections,
900                    connectionAcquisitionTimeout));
901        }
902        if (CmsFsStorage.STORAGE_TYPE.equals(type)) {
903            String path = requireConfigValue(configuration, prefix + PARAM_PATH, id);
904            return new CmsFsStorage(id, path);
905        }
906        throw new IllegalArgumentException(
907            Messages.get().getBundle().key(Messages.ERR_STORAGE_UNSUPPORTED_TYPE_2, type, id));
908    }
909
910    /**
911     * Creates the storage policy from the VFS configuration.
912     *
913     * @param configuration the storage policy configuration
914     * @return the storage policy
915     */
916    private I_CmsStoragePolicy createStoragePolicy(CmsStoragePolicyConfiguration configuration) {
917
918        String className = CmsNoExternalStoragePolicy.class.getName();
919        CmsParameterConfiguration parameters = null;
920        if ((configuration != null) && CmsStringUtil.isNotEmptyOrWhitespaceOnly(configuration.getClassName())) {
921            className = configuration.getClassName();
922            parameters = configuration.getConfiguration();
923        }
924        try {
925            Object instance = Class.forName(className).newInstance();
926            if (!(instance instanceof I_CmsStoragePolicy)) {
927                throw new IllegalArgumentException(
928                    Messages.get().getBundle().key(
929                        Messages.ERR_STORAGE_POLICY_INVALID_2,
930                        className,
931                        I_CmsStoragePolicy.class.getName()));
932            }
933            I_CmsStoragePolicy policy = (I_CmsStoragePolicy)instance;
934            if (policy instanceof I_CmsConfigurationParameterHandler) {
935                I_CmsConfigurationParameterHandler parameterHandler = (I_CmsConfigurationParameterHandler)policy;
936                if (parameters != null) {
937                    for (String key : parameters.keySet()) {
938                        parameterHandler.addConfigurationParameter(key, parameters.get(key));
939                    }
940                }
941                parameterHandler.initConfiguration();
942            }
943            return policy;
944        } catch (CmsConfigurationException e) {
945            throw new IllegalArgumentException(
946                Messages.get().getBundle().key(Messages.ERR_STORAGE_POLICY_INIT_1, className),
947                e);
948        } catch (IllegalArgumentException e) {
949            throw e;
950        } catch (Exception e) {
951            throw new IllegalArgumentException(
952                Messages.get().getBundle().key(Messages.ERR_STORAGE_POLICY_CREATE_1, className),
953                e);
954        }
955    }
956
957    /**
958     * Returns the configured storage backend for reading external content.<p>
959     *
960     * @param storage the storage identifier
961     * @return the configured storage backend
962     * @throws CmsStorageException if no backend is configured for the storage identifier
963     */
964    private I_CmsStorage getConfiguredStorageForRead(String storage) throws CmsStorageException {
965
966        I_CmsStorage configuredStorage = m_storages.get(storage);
967        if (configuredStorage == null) {
968            String message = Messages.get().getBundle().key(Messages.ERR_STORAGE_UNCONFIGURED_1, storage);
969            LOG.error(message);
970            throw new CmsStorageException(message);
971        }
972        return configuredStorage;
973    }
974
975    /**
976     * Returns the default DB storage class name for the active database package.<p>
977     *
978     * @return the default DB storage class name
979     */
980    private String getDefaultDbStorageClassName() {
981
982        Package sqlManagerPackage = m_sqlManager.getClass().getPackage();
983        String packageName = sqlManagerPackage != null ? sqlManagerPackage.getName() : null;
984        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(packageName)) {
985            String className = packageName + ".CmsDbStorage";
986            try {
987                Class.forName(className);
988                return className;
989            } catch (ClassNotFoundException e) {
990                // use the generic implementation below
991            }
992        }
993        return org.opencms.db.generic.CmsDbStorage.class.getName();
994    }
995
996    /**
997     * Returns a short reason for a backend validation failure.<p>
998     *
999     * @param e the failure
1000     * @return the failure reason
1001     */
1002    private String getFailureReason(Exception e) {
1003
1004        String message = e.getMessage();
1005        if (CmsStringUtil.isEmptyOrWhitespaceOnly(message)) {
1006            message = e.getClass().getName();
1007        }
1008        return message;
1009    }
1010
1011    /**
1012     * Returns the configured role for a storage id.<p>
1013     *
1014     * @param storageId the storage id
1015     * @return the configured role
1016     */
1017    private String getStorageRole(String storageId) {
1018
1019        return getStorageRole(storageId, m_activeStorageId);
1020    }
1021
1022    /**
1023     * Returns the configured role for a storage id.<p>
1024     *
1025     * @param storageId the storage id
1026     * @param activeStorageId the active storage id
1027     * @return the configured role
1028     */
1029    private String getStorageRole(String storageId, String activeStorageId) {
1030
1031        return activeStorageId.equals(storageId) ? "active" : "legacy";
1032    }
1033
1034    /**
1035     * Initializes the configured storage backends.
1036     *
1037     * @param configuration the runtime property configuration
1038     */
1039    private void initStorages(CmsParameterConfiguration configuration) {
1040
1041        m_storages = new LinkedHashMap<String, I_CmsStorage>();
1042        m_storageBackendIds = new LinkedHashMap<String, String>();
1043
1044        String activeStorageId = configuration.getString(PARAM_STORAGE_ACTIVE, I_CmsDbStorage.STORAGE_TYPE).trim();
1045        List<String> referencedStorages = new ArrayList<String>();
1046        referencedStorages.add(activeStorageId);
1047        for (String legacyStorage : parseCommaSeparatedList(configuration.getString(PARAM_STORAGE_LEGACY, ""))) {
1048            if (legacyStorage.equals(activeStorageId)) {
1049                throw new IllegalArgumentException(
1050                    Messages.get().getBundle().key(Messages.ERR_STORAGE_LEGACY_CONTAINS_ACTIVE_1, activeStorageId));
1051            }
1052            if (!referencedStorages.contains(legacyStorage)) {
1053                referencedStorages.add(legacyStorage);
1054            }
1055        }
1056        I_CmsStorage activeStorageCandidate = null;
1057        for (String storageId : referencedStorages) {
1058            I_CmsStorage storage = createStorage(storageId, configuration);
1059            String identifier = storage.getStorageIdentifier();
1060            if (m_storages.containsKey(identifier)) {
1061                // if it's the DB storage, it's already in the map
1062                storage = m_storages.get(identifier);
1063            } else {
1064                m_storages.put(identifier, storage);
1065                m_storageBackendIds.put(identifier, storageId);
1066            }
1067            if (CmsLog.INIT.isInfoEnabled()) {
1068                CmsLog.INIT.info(
1069                    Messages.get().getBundle().key(
1070                        Messages.INIT_STORAGE_BACKEND_CONFIGURED_3,
1071                        storageId,
1072                        identifier,
1073                        getStorageRole(storageId, activeStorageId)));
1074            }
1075            if (storageId.equals(activeStorageId)) {
1076                activeStorageCandidate = storage;
1077            }
1078        }
1079        m_activeStorageId = activeStorageId;
1080        m_activeStorage = activeStorageCandidate;
1081        if (m_activeStorage == null) {
1082            throw new IllegalArgumentException(
1083                Messages.get().getBundle().key(Messages.ERR_STORAGE_ACTIVE_MISSING_1, activeStorageId));
1084        }
1085    }
1086
1087    /**
1088     * Checks if a specific hash is still used in any online, history or offline
1089     * content table for the given storage identifier.<p>
1090     *
1091     * @param dbc the database context
1092     * @param storage the storage identifier
1093     * @param hash the hash to check
1094     * @return true if the hash is still in use for that storage
1095     * @throws CmsDbSqlException if database access fails
1096     */
1097    private boolean isContentReferenced(CmsDbContext dbc, String storage, String hash) throws CmsDbSqlException {
1098
1099        Connection conn = null;
1100        PreparedStatement stmt = null;
1101        ResultSet res = null;
1102        try {
1103            conn = m_sqlManager.getConnection(dbc);
1104            stmt = m_sqlManager.getPreparedStatement(conn, "C_STORAGE_CONTENT_INUSE");
1105            stmt.setString(1, hash);
1106            stmt.setString(2, storage);
1107            stmt.setString(3, hash);
1108            stmt.setString(4, storage);
1109            res = stmt.executeQuery();
1110            return res.next();
1111        } catch (SQLException e) {
1112            throw new CmsDbSqlException(
1113                Messages.get().container(Messages.ERR_STORAGE_SQL_1, CmsDbSqlException.getErrorQuery(stmt)),
1114                e);
1115        } finally {
1116            m_sqlManager.closeAll(dbc, conn, stmt, res);
1117        }
1118    }
1119}