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}