001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.kernel.impl; 019 020import com.google.common.base.Preconditions; 021import org.fcrepo.common.db.DbPlatform; 022import org.fcrepo.kernel.api.ContainmentIndex; 023import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 024import org.fcrepo.kernel.api.identifiers.FedoraId; 025import org.slf4j.Logger; 026import org.springframework.core.io.DefaultResourceLoader; 027import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 028import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 029import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; 030import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; 031import org.springframework.stereotype.Component; 032import org.springframework.transaction.annotation.Transactional; 033 034import javax.annotation.Nonnull; 035import javax.annotation.PostConstruct; 036import javax.inject.Inject; 037import javax.sql.DataSource; 038 039import java.sql.Timestamp; 040import java.time.Instant; 041import java.util.Collections; 042import java.util.List; 043import java.util.Map; 044import java.util.stream.Stream; 045 046import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX; 047import static org.slf4j.LoggerFactory.getLogger; 048 049/** 050 * @author peichman 051 * @author whikloj 052 * @since 6.0.0 053 */ 054@Component("containmentIndexImpl") 055public class ContainmentIndexImpl implements ContainmentIndex { 056 057 private static final Logger LOGGER = getLogger(ContainmentIndexImpl.class); 058 059 @Inject 060 private DataSource dataSource; 061 062 private NamedParameterJdbcTemplate jdbcTemplate; 063 064 private DbPlatform dbPlatform; 065 066 public static final String RESOURCES_TABLE = "containment"; 067 068 private static final String TRANSACTION_OPERATIONS_TABLE = "containment_transactions"; 069 070 public static final String FEDORA_ID_COLUMN = "fedora_id"; 071 072 private static final String PARENT_COLUMN = "parent"; 073 074 private static final String TRANSACTION_ID_COLUMN = "transaction_id"; 075 076 private static final String OPERATION_COLUMN = "operation"; 077 078 private static final String START_TIME_COLUMN = "start_time"; 079 080 private static final String END_TIME_COLUMN = "end_time"; 081 082 /* 083 * Select children of a resource that are not marked as deleted. 084 */ 085 private static final String SELECT_CHILDREN = "SELECT " + FEDORA_ID_COLUMN + 086 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + " IS NULL"; 087 088 /* 089 * Select children of a memento of a resource. 090 */ 091 private static final String SELECT_CHILDREN_OF_MEMENTO = "SELECT " + FEDORA_ID_COLUMN + 092 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + START_TIME_COLUMN + 093 " <= :asOfTime AND (" + END_TIME_COLUMN + " > :asOfTime OR " + END_TIME_COLUMN + " IS NULL)"; 094 095 /* 096 * Select children of a parent from resources table and from the transaction table with an 'add' operation, 097 * but exclude any records that also exist in the transaction table with a 'delete' or 'purge' operation. 098 */ 099 private static final String SELECT_CHILDREN_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 100 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent" + 101 " AND " + END_TIME_COLUMN + " IS NULL " + 102 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + 103 " WHERE " + PARENT_COLUMN + " = :parent AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 104 " AND " + OPERATION_COLUMN + " = 'add') x" + 105 " WHERE NOT EXISTS " + 106 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 107 " WHERE " + PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = x." + FEDORA_ID_COLUMN + 108 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))"; 109 110 /* 111 * Select all children of a resource that are marked for deletion. 112 */ 113 private static final String SELECT_DELETED_CHILDREN = "SELECT " + FEDORA_ID_COLUMN + 114 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + 115 " IS NOT NULL"; 116 117 /* 118 * Select children of a resource plus children 'delete'd in the non-committed transaction, but excluding any 119 * 'add'ed in the non-committed transaction. 120 */ 121 private static final String SELECT_DELETED_CHILDREN_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + 122 " FROM (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + 123 " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + " IS NOT NULL UNION" + 124 " SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 125 PARENT_COLUMN + " = :parent AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 126 OPERATION_COLUMN + " = 'delete') x" + 127 " WHERE NOT EXISTS " + 128 "(SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + 129 FEDORA_ID_COLUMN + " = x." + FEDORA_ID_COLUMN + " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 130 OPERATION_COLUMN + " = 'add')"; 131 132 /* 133 * Insert a parent child relationship to the transaction operation table. 134 */ 135 private static final String INSERT_CHILD_IN_TRANSACTION = "INSERT INTO " + TRANSACTION_OPERATIONS_TABLE + 136 " ( " + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ", " + 137 TRANSACTION_ID_COLUMN + ", " + OPERATION_COLUMN + " ) VALUES (:parent, :child, :startTime, :endTime, " + 138 ":transactionId, 'add')"; 139 140 /* 141 * Remove an insert row from the transaction operation table for this parent child relationship. 142 */ 143 private static final String UNDO_INSERT_CHILD_IN_TRANSACTION = "DELETE FROM " + TRANSACTION_OPERATIONS_TABLE + 144 " WHERE " + PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 145 + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 146 147 /* 148 * Add a parent child relationship deletion to the transaction operation table. 149 */ 150 private static final String DELETE_CHILD_IN_TRANSACTION = "INSERT INTO " + TRANSACTION_OPERATIONS_TABLE + 151 " ( " + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + END_TIME_COLUMN + ", " + TRANSACTION_ID_COLUMN + 152 ", " + OPERATION_COLUMN + " ) VALUES (:parent, :child, :endTime, :transactionId, 'delete')"; 153 154 /* 155 * Add a parent child relationship purge to the transaction operation table. 156 */ 157 private static final String PURGE_CHILD_IN_TRANSACTION = "INSERT INTO " + TRANSACTION_OPERATIONS_TABLE + 158 " ( " + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ", " + OPERATION_COLUMN + 159 " ) VALUES (:parent, :child, :transactionId, 'purge')"; 160 161 /* 162 * Remove a mark as deleted row from the transaction operation table for this child relationship (no parent). 163 */ 164 private static final String UNDO_DELETE_CHILD_IN_TRANSACTION_NO_PARENT = "DELETE FROM " + 165 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 166 + " = :transactionId AND " + OPERATION_COLUMN + " = 'delete'"; 167 168 /* 169 * Remove a purge row from the transaction operation table for this parent child relationship. 170 */ 171 private static final String UNDO_PURGE_CHILD_IN_TRANSACTION = "DELETE FROM " + TRANSACTION_OPERATIONS_TABLE + 172 " WHERE " + PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 173 + " = :transactionId AND " + OPERATION_COLUMN + " = 'purge'"; 174 175 /* 176 * Is this parent child relationship being added in this transaction? 177 */ 178 private static final String IS_CHILD_ADDED_IN_TRANSACTION = "SELECT TRUE FROM " + TRANSACTION_OPERATIONS_TABLE + 179 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + PARENT_COLUMN + " = :parent" + 180 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 181 182 /* 183 * Is this child's relationship being marked for deletion in this transaction (no parent)? 184 */ 185 private static final String IS_CHILD_DELETED_IN_TRANSACTION_NO_PARENT = "SELECT TRUE FROM " + 186 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child " + 187 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'delete'"; 188 189 /* 190 * Is this parent child relationship being purged in this transaction? 191 */ 192 private static final String IS_CHILD_PURGED_IN_TRANSACTION = "SELECT TRUE FROM " + TRANSACTION_OPERATIONS_TABLE + 193 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + PARENT_COLUMN + " = :parent" + 194 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'purge'"; 195 196 /* 197 * Delete all rows from the transaction operation table for this transaction. 198 */ 199 private static final String DELETE_ENTIRE_TRANSACTION = "DELETE FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 200 TRANSACTION_ID_COLUMN + " = :transactionId"; 201 202 /* 203 * Add to the main table all rows from the transaction operation table marked 'add' for this transaction. 204 */ 205 private static final String COMMIT_ADD_RECORDS = "INSERT INTO " + RESOURCES_TABLE + " ( " + FEDORA_ID_COLUMN + ", " 206 + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + " ) SELECT " + FEDORA_ID_COLUMN + 207 ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + " FROM " + 208 TRANSACTION_OPERATIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 209 OPERATION_COLUMN + " = 'add'"; 210 211 /* 212 * Add an end time to the rows in the main table that match all rows from transaction operation table marked 213 * 'delete' for this transaction. 214 */ 215 private static final String COMMIT_DELETE_RECORDS_H2 = "UPDATE " + RESOURCES_TABLE + 216 " r SET r." + END_TIME_COLUMN + " = ( SELECT t." + END_TIME_COLUMN + " FROM " + 217 TRANSACTION_OPERATIONS_TABLE + " t " + 218 " WHERE t." + FEDORA_ID_COLUMN + " = r." + FEDORA_ID_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + 219 " = :transactionId AND t." + OPERATION_COLUMN + 220 " = 'delete' AND t." + PARENT_COLUMN + " = r." + PARENT_COLUMN + " AND r." + 221 END_TIME_COLUMN + " IS NULL)" + 222 " WHERE EXISTS (SELECT * FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + FEDORA_ID_COLUMN + 223 " = r." + FEDORA_ID_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + " = :transactionId AND t." + 224 OPERATION_COLUMN + " = 'delete' AND t." + PARENT_COLUMN + " = r." + PARENT_COLUMN + " AND r." + 225 END_TIME_COLUMN + " IS NULL)"; 226 227 private static final String COMMIT_DELETE_RECORDS_MYSQL = "UPDATE " + RESOURCES_TABLE + 228 " r INNER JOIN " + TRANSACTION_OPERATIONS_TABLE + " t ON t." + FEDORA_ID_COLUMN + " = r." + 229 FEDORA_ID_COLUMN + " SET r." + END_TIME_COLUMN + " = t." + END_TIME_COLUMN + 230 ", r." + START_TIME_COLUMN + " = r." + START_TIME_COLUMN + " WHERE t." + PARENT_COLUMN + " = r." + 231 PARENT_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + " = :transactionId AND t." + OPERATION_COLUMN + 232 " = 'delete' AND r." + END_TIME_COLUMN + " IS NULL"; 233 234 private static final String COMMIT_DELETE_RECORDS_POSTGRES = "UPDATE " + RESOURCES_TABLE + " SET " + 235 END_TIME_COLUMN + " = t." + END_TIME_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + 236 FEDORA_ID_COLUMN + " = " + RESOURCES_TABLE + "." + FEDORA_ID_COLUMN + " AND t." + PARENT_COLUMN + 237 " = " + RESOURCES_TABLE + "." + PARENT_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + 238 " = :transactionId AND t." + OPERATION_COLUMN + " = 'delete' AND " + RESOURCES_TABLE + "." + 239 END_TIME_COLUMN + " IS NULL"; 240 241 private Map<DbPlatform, String> COMMIT_DELETE_RECORDS = Map.of( 242 DbPlatform.H2, COMMIT_DELETE_RECORDS_H2, 243 DbPlatform.MARIADB, COMMIT_DELETE_RECORDS_MYSQL, 244 DbPlatform.MYSQL, COMMIT_DELETE_RECORDS_MYSQL, 245 DbPlatform.POSTGRESQL, COMMIT_DELETE_RECORDS_POSTGRES 246 ); 247 248 /* 249 * Remove from the main table all rows from transaction operation table marked 'purge' for this transaction. 250 */ 251 private static final String COMMIT_PURGE_RECORDS = "DELETE FROM " + RESOURCES_TABLE + " WHERE " + 252 "EXISTS (SELECT * FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + 253 TRANSACTION_ID_COLUMN + " = :transactionId AND t." + OPERATION_COLUMN + " = 'purge' AND" + 254 " t." + FEDORA_ID_COLUMN + " = " + RESOURCES_TABLE + "." + FEDORA_ID_COLUMN + 255 " AND t." + PARENT_COLUMN + " = " + RESOURCES_TABLE + "." + PARENT_COLUMN + ")"; 256 257 /* 258 * Query if a resource exists in the main table and is not deleted. 259 */ 260 private static final String RESOURCE_EXISTS = "SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + 261 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NULL"; 262 263 /* 264 * Resource exists as a record in the transaction operations table with an 'add' operation and not also 265 * exists as a 'delete' operation. 266 */ 267 private static final String RESOURCE_EXISTS_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 268 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 269 " AND " + END_TIME_COLUMN + " IS NULL UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + 270 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 271 " = :transactionId" + " AND " + OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS " + 272 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 273 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 274 " AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))"; 275 276 /* 277 * Query if a resource exists in the main table even if it is deleted. 278 */ 279 private static final String RESOURCE_OR_TOMBSTONE_EXISTS = "SELECT " + FEDORA_ID_COLUMN + " FROM " + 280 RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child"; 281 282 /* 283 * Resource exists as a record in the main table even if deleted or in the transaction operations table with an 284 * 'add' operation and not also exists as a 'delete' operation. 285 */ 286 private static final String RESOURCE_OR_TOMBSTONE_EXISTS_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 287 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 288 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + 289 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 290 " = :transactionId" + " AND " + OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS " + 291 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 292 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 293 " AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))"; 294 295 296 /* 297 * Get the parent ID for this resource from the main table if not deleted. 298 */ 299 private static final String PARENT_EXISTS = "SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + 300 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NULL"; 301 302 /* 303 * Get the parent ID for this resource from the operations table for an 'add' operation in this transaction, but 304 * exclude any 'delete' operations for this resource in this transaction. 305 */ 306 private static final String PARENT_EXISTS_IN_TRANSACTION = "SELECT x." + PARENT_COLUMN + " FROM" + 307 " (SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 308 " AND " + END_TIME_COLUMN + " IS NULL" + 309 " UNION SELECT " + PARENT_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + 310 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 311 " AND " + OPERATION_COLUMN + " = 'add') x" + 312 " WHERE NOT EXISTS " + 313 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 314 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 315 " AND " + OPERATION_COLUMN + " = 'delete')"; 316 317 /* 318 * Get the parent ID for this resource from the main table if deleted. 319 */ 320 private static final String PARENT_EXISTS_DELETED = "SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + 321 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NOT NULL"; 322 323 /* 324 * Get the parent ID for this resource from main table and the operations table for a 'delete' operation in this 325 * transaction, excluding any 'add' operations for this resource in this transaction. 326 */ 327 private static final String PARENT_EXISTS_DELETED_IN_TRANSACTION = "SELECT x." + PARENT_COLUMN + " FROM" + 328 " (SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 329 " AND " + END_TIME_COLUMN + " IS NOT NULL UNION SELECT " + PARENT_COLUMN + " FROM " + 330 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 331 " = :transactionId AND " + OPERATION_COLUMN + " = 'delete') x WHERE NOT EXISTS " + 332 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + 333 TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add')"; 334 335 /* 336 * Does this resource exist in the transaction operation table for an 'add' record. 337 */ 338 private static final String IS_CHILD_ADDED_IN_TRANSACTION_NO_PARENT = "SELECT TRUE FROM " + 339 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + 340 TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 341 342 /* 343 * Delete a row from the transaction operation table with this resource and 'add' operation, no parent required. 344 */ 345 private static final String UNDO_INSERT_CHILD_IN_TRANSACTION_NO_PARENT = "DELETE FROM " + 346 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 347 + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 348 349 private static final String TRUNCATE_TABLE = "TRUNCATE TABLE "; 350 351 /* 352 * Any record tracked in the containment index is either active or a tombstone. Either way it exists for the 353 * purpose of finding ghost nodes. 354 */ 355 private static final String SELECT_ID_LIKE = "SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + 356 FEDORA_ID_COLUMN + " LIKE :resourceId"; 357 358 private static final String SELECT_ID_LIKE_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM (SELECT " + 359 FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " LIKE :resourceId" + 360 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 361 FEDORA_ID_COLUMN + " LIKE :resourceId AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 362 OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 363 " WHERE " + FEDORA_ID_COLUMN + " LIKE :resourceId AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 364 OPERATION_COLUMN + " = 'delete')"; 365 366 private static final Map<DbPlatform, String> DDL_MAP = Map.of( 367 DbPlatform.MYSQL, "sql/mysql-containment.sql", 368 DbPlatform.H2, "sql/default-containment.sql", 369 DbPlatform.POSTGRESQL, "sql/postgres-containment.sql", 370 DbPlatform.MARIADB, "sql/default-containment.sql" 371 ); 372 373 /** 374 * Connect to the database 375 */ 376 @PostConstruct 377 private void setup() { 378 jdbcTemplate = getNamedParameterJdbcTemplate(); 379 380 dbPlatform = DbPlatform.fromDataSource(dataSource); 381 382 Preconditions.checkArgument(DDL_MAP.containsKey(dbPlatform), 383 "Missing DDL mapping for %s", dbPlatform); 384 385 final var ddl = DDL_MAP.get(dbPlatform); 386 LOGGER.info("Applying ddl: {}", ddl); 387 DatabasePopulatorUtils.execute( 388 new ResourceDatabasePopulator(new DefaultResourceLoader().getResource("classpath:" + ddl)), 389 dataSource); 390 } 391 392 private NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { 393 return new NamedParameterJdbcTemplate(getDataSource()); 394 } 395 396 @Override 397 public Stream<String> getContains(final String txId, final FedoraId fedoraId) { 398 final String resourceId = fedoraId.isMemento() ? fedoraId.getBaseId() : fedoraId.getFullId(); 399 final Instant asOfTime = fedoraId.isMemento() ? fedoraId.getMementoInstant() : null; 400 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 401 parameterSource.addValue("parent", resourceId); 402 403 final List<String> children; 404 if (asOfTime == null) { 405 if (txId != null) { 406 // we are in a transaction 407 parameterSource.addValue("transactionId", txId); 408 children = jdbcTemplate.queryForList(SELECT_CHILDREN_IN_TRANSACTION, parameterSource, String.class); 409 } else { 410 // not in a transaction 411 children = jdbcTemplate.queryForList(SELECT_CHILDREN, parameterSource, String.class); 412 } 413 } else { 414 parameterSource.addValue("asOfTime", formatInstant(asOfTime)); 415 children = jdbcTemplate.queryForList(SELECT_CHILDREN_OF_MEMENTO, parameterSource, String.class); 416 } 417 LOGGER.debug("getContains for {} in transaction {} and instant {} found {} children", 418 resourceId, txId, asOfTime, children.size()); 419 return children.stream(); 420 } 421 422 @Override 423 public Stream<String> getContainsDeleted(final String txId, final FedoraId fedoraId) { 424 final String resourceId = fedoraId.getFullId(); 425 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 426 parameterSource.addValue("parent", resourceId); 427 428 final List<String> children; 429 if (txId != null) { 430 // we are in a transaction 431 parameterSource.addValue("transactionId", txId); 432 children = jdbcTemplate.queryForList(SELECT_DELETED_CHILDREN_IN_TRANSACTION, parameterSource, String.class); 433 } else { 434 // not in a transaction 435 children = jdbcTemplate.queryForList(SELECT_DELETED_CHILDREN, parameterSource, String.class); 436 } 437 LOGGER.debug("getContainsDeleted for {} in transaction {} found {} children", 438 resourceId, txId, children.size()); 439 return children.stream(); 440 } 441 442 @Override 443 public String getContainedBy(final String txId, final FedoraId resource) { 444 final String resourceID = resource.getFullId(); 445 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 446 parameterSource.addValue("child", resourceID); 447 final List<String> parentID; 448 if (txId != null) { 449 parameterSource.addValue("transactionId", txId); 450 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_IN_TRANSACTION, parameterSource, String.class); 451 } else { 452 parentID = jdbcTemplate.queryForList(PARENT_EXISTS, parameterSource, String.class); 453 } 454 return parentID.stream().findFirst().orElse(null); 455 } 456 457 @Override 458 public void addContainedBy(@Nonnull final String txId, final FedoraId parent, final FedoraId child) { 459 addContainedBy(txId, parent, child, Instant.now(), null); 460 } 461 462 @Override 463 public void addContainedBy(@Nonnull final String txId, final FedoraId parent, final FedoraId child, 464 final Instant startTime, final Instant endTime) { 465 final String parentID = parent.getFullId(); 466 final String childID = child.getFullId(); 467 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 468 469 LOGGER.debug("Adding: parent: {}, child: {}, in txn: {}, start time {}, end time {}", parentID, childID, txId, 470 formatInstant(startTime), formatInstant(endTime)); 471 472 parameterSource.addValue("parent", parentID); 473 parameterSource.addValue("child", childID); 474 parameterSource.addValue("transactionId", txId); 475 parameterSource.addValue("startTime", formatInstant(startTime)); 476 parameterSource.addValue("endTime", formatInstant(endTime)); 477 final boolean purgedInTxn = !jdbcTemplate.queryForList(IS_CHILD_PURGED_IN_TRANSACTION, parameterSource) 478 .isEmpty(); 479 if (purgedInTxn) { 480 // We purged it, but are re-adding it so remove the purge operation. 481 jdbcTemplate.update(UNDO_PURGE_CHILD_IN_TRANSACTION, parameterSource); 482 } 483 jdbcTemplate.update(INSERT_CHILD_IN_TRANSACTION, parameterSource); 484 } 485 486 @Override 487 public void removeContainedBy(@Nonnull final String txId, final FedoraId parent, final FedoraId child) { 488 final String parentID = parent.getFullId(); 489 final String childID = child.getFullId(); 490 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 491 parameterSource.addValue("parent", parentID); 492 parameterSource.addValue("child", childID); 493 parameterSource.addValue("transactionId", txId); 494 parameterSource.addValue("endTime", formatInstant(Instant.now())); 495 final boolean addedInTxn = !jdbcTemplate.queryForList(IS_CHILD_ADDED_IN_TRANSACTION, parameterSource) 496 .isEmpty(); 497 if (addedInTxn) { 498 jdbcTemplate.update(UNDO_INSERT_CHILD_IN_TRANSACTION, parameterSource); 499 } else { 500 jdbcTemplate.update(DELETE_CHILD_IN_TRANSACTION, parameterSource); 501 } 502 } 503 504 @Override 505 public void removeResource(@Nonnull final String txId, final FedoraId resource) { 506 final String resourceID = resource.getFullId(); 507 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 508 parameterSource.addValue("child", resourceID); 509 parameterSource.addValue("transactionId", txId); 510 final boolean addedInTxn = !jdbcTemplate.queryForList(IS_CHILD_ADDED_IN_TRANSACTION_NO_PARENT, 511 parameterSource).isEmpty(); 512 if (addedInTxn) { 513 jdbcTemplate.update(UNDO_INSERT_CHILD_IN_TRANSACTION_NO_PARENT, parameterSource); 514 } else { 515 final String parent = getContainedBy(txId, resource); 516 if (parent != null) { 517 LOGGER.debug("Marking containment relationship between parent ({}) and child ({}) deleted", parent, 518 resourceID); 519 parameterSource.addValue("parent", parent); 520 parameterSource.addValue("endTime", formatInstant(Instant.now())); 521 jdbcTemplate.update(DELETE_CHILD_IN_TRANSACTION, parameterSource); 522 } 523 } 524 } 525 526 @Override 527 public void purgeResource(@Nonnull final String txId, final FedoraId resource) { 528 final String resourceID = resource.getFullId(); 529 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 530 parameterSource.addValue("child", resourceID); 531 parameterSource.addValue("transactionId", txId); 532 final String parent = getContainedByDeleted(txId, resource); 533 final boolean deletedInTxn = !jdbcTemplate.queryForList(IS_CHILD_DELETED_IN_TRANSACTION_NO_PARENT, 534 parameterSource).isEmpty(); 535 if (deletedInTxn) { 536 jdbcTemplate.update(UNDO_DELETE_CHILD_IN_TRANSACTION_NO_PARENT, parameterSource); 537 } 538 if (parent != null) { 539 LOGGER.debug("Removing containment relationship between parent ({}) and child ({})", parent, resourceID); 540 parameterSource.addValue("parent", parent); 541 jdbcTemplate.update(PURGE_CHILD_IN_TRANSACTION, parameterSource); 542 } 543 } 544 545 /** 546 * Find parent for a resource using a deleted containment relationship. 547 * @param txId the transaction id. 548 * @param resource the child resource id. 549 * @return the parent id. 550 */ 551 private String getContainedByDeleted(final String txId, final FedoraId resource) { 552 final String resourceID = resource.getFullId(); 553 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 554 parameterSource.addValue("child", resourceID); 555 final List<String> parentID; 556 if (txId != null) { 557 parameterSource.addValue("transactionId", txId); 558 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_DELETED_IN_TRANSACTION, parameterSource, String.class); 559 } else { 560 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_DELETED, parameterSource, String.class); 561 } 562 return parentID.stream().findFirst().orElse(null); 563 } 564 565 @Transactional 566 @Override 567 public void commitTransaction(final String txId) { 568 if (txId != null) { 569 try { 570 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 571 parameterSource.addValue("transactionId", txId); 572 573 jdbcTemplate.update(COMMIT_PURGE_RECORDS, parameterSource); 574 jdbcTemplate.update(COMMIT_DELETE_RECORDS.get(dbPlatform), parameterSource); 575 jdbcTemplate.update(COMMIT_ADD_RECORDS, parameterSource); 576 jdbcTemplate.update(DELETE_ENTIRE_TRANSACTION, parameterSource); 577 } catch (final Exception e) { 578 LOGGER.warn("Unable to commit containment index transaction {}: {}", txId, e.getMessage()); 579 throw new RepositoryRuntimeException("Unable to commit containment index transaction", e); 580 } 581 } 582 } 583 584 @Override 585 public void rollbackTransaction(final String txId) { 586 if (txId != null) { 587 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 588 parameterSource.addValue("transactionId", txId); 589 jdbcTemplate.update(DELETE_ENTIRE_TRANSACTION, parameterSource); 590 } 591 } 592 593 @Override 594 public boolean resourceExists(final String txId, final FedoraId fedoraId, final boolean includeDeleted) { 595 // Get the containing ID because fcr:metadata will not exist here but MUST exist if the containing resource does 596 final String resourceId = fedoraId.getBaseId(); 597 LOGGER.debug("Checking if {} exists in transaction {}", resourceId, txId); 598 if (fedoraId.isRepositoryRoot()) { 599 // Root always exists. 600 return true; 601 } 602 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 603 parameterSource.addValue("child", resourceId); 604 final boolean exists; 605 if (txId != null) { 606 final var queryToUse = includeDeleted ? RESOURCE_OR_TOMBSTONE_EXISTS_IN_TRANSACTION : 607 RESOURCE_EXISTS_IN_TRANSACTION; 608 parameterSource.addValue("transactionId", txId); 609 exists = !jdbcTemplate.queryForList(queryToUse, parameterSource, String.class) 610 .isEmpty(); 611 } else { 612 final var queryToUse = includeDeleted ? RESOURCE_OR_TOMBSTONE_EXISTS : 613 RESOURCE_EXISTS; 614 exists = !jdbcTemplate.queryForList(queryToUse, parameterSource, String.class).isEmpty(); 615 } 616 return exists; 617 } 618 619 @Override 620 public FedoraId getContainerIdByPath(final String txId, final FedoraId fedoraId, final boolean checkDeleted) { 621 if (fedoraId.isRepositoryRoot()) { 622 // If we are root then we are the top. 623 return fedoraId; 624 } 625 final String parent = getContainedBy(txId, fedoraId); 626 if (parent != null) { 627 return FedoraId.create(parent); 628 } 629 String fullId = fedoraId.getFullId(); 630 while (fullId.contains("/")) { 631 fullId = fedoraId.getResourceId().substring(0, fullId.lastIndexOf("/")); 632 if (fullId.equals(FEDORA_ID_PREFIX)) { 633 return FedoraId.getRepositoryRootId(); 634 } 635 final FedoraId testID = FedoraId.create(fullId); 636 if (resourceExists(txId, testID, checkDeleted)) { 637 return testID; 638 } 639 } 640 return FedoraId.getRepositoryRootId(); 641 } 642 643 @Transactional 644 @Override 645 public void reset() { 646 try { 647 jdbcTemplate.update(TRUNCATE_TABLE + RESOURCES_TABLE, Collections.emptyMap()); 648 jdbcTemplate.update(TRUNCATE_TABLE + TRANSACTION_OPERATIONS_TABLE, Collections.emptyMap()); 649 } catch (final Exception e) { 650 throw new RepositoryRuntimeException("Failed to truncate containment tables", e); 651 } 652 } 653 654 @Override 655 public boolean hasResourcesStartingWith(final String txId, final FedoraId fedoraId) { 656 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 657 parameterSource.addValue("resourceId", fedoraId.getFullId() + "/%"); 658 final boolean matchingIds; 659 if (txId != null) { 660 parameterSource.addValue("transactionId", txId); 661 matchingIds = !jdbcTemplate.queryForList(SELECT_ID_LIKE_IN_TRANSACTION, parameterSource, String.class) 662 .isEmpty(); 663 } else { 664 matchingIds = !jdbcTemplate.queryForList(SELECT_ID_LIKE, parameterSource, String.class).isEmpty(); 665 } 666 return matchingIds; 667 } 668 669 /** 670 * Get the data source backing this containment index 671 * @return data source 672 */ 673 public DataSource getDataSource() { 674 return dataSource; 675 } 676 677 /** 678 * Set the data source backing this containment index 679 * @param dataSource data source 680 */ 681 public void setDataSource(final DataSource dataSource) { 682 this.dataSource = dataSource; 683 } 684 685 /** 686 * Format an instant to a timestamp without milliseconds, due to precision 687 * issues with memento datetimes. 688 * @param instant 689 * @return the timestamp 690 */ 691 private Timestamp formatInstant(final Instant instant) { 692 if (instant == null) { 693 return null; 694 } 695 final var timestamp = Timestamp.from(instant); 696 timestamp.setNanos(0); 697 return timestamp; 698 } 699}