001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.kernel.impl; 007 008import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX; 009import static org.slf4j.LoggerFactory.getLogger; 010 011import java.sql.Timestamp; 012import java.time.Instant; 013import java.time.temporal.ChronoUnit; 014import java.util.Collections; 015import java.util.List; 016import java.util.Map; 017import java.util.NoSuchElementException; 018import java.util.Queue; 019import java.util.Spliterator; 020import java.util.Spliterators; 021import java.util.concurrent.ConcurrentLinkedQueue; 022import java.util.concurrent.TimeUnit; 023import java.util.function.Consumer; 024import java.util.stream.Stream; 025import java.util.stream.StreamSupport; 026 027import javax.annotation.Nonnull; 028import javax.annotation.PostConstruct; 029import javax.inject.Inject; 030import javax.sql.DataSource; 031 032import org.fcrepo.common.db.DbPlatform; 033import org.fcrepo.config.FedoraPropsConfig; 034import org.fcrepo.kernel.api.ContainmentIndex; 035import org.fcrepo.kernel.api.Transaction; 036import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 037import org.fcrepo.kernel.api.identifiers.FedoraId; 038 039import org.slf4j.Logger; 040import org.springframework.dao.EmptyResultDataAccessException; 041import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 042import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 043import org.springframework.stereotype.Component; 044import org.springframework.transaction.annotation.Propagation; 045import org.springframework.transaction.annotation.Transactional; 046 047import com.github.benmanes.caffeine.cache.Cache; 048import com.github.benmanes.caffeine.cache.Caffeine; 049 050/** 051 * @author peichman 052 * @author whikloj 053 * @since 6.0.0 054 */ 055@Component("containmentIndexImpl") 056public class ContainmentIndexImpl implements ContainmentIndex { 057 058 private static final Logger LOGGER = getLogger(ContainmentIndexImpl.class); 059 060 private int containsLimit = 50000; 061 062 @Inject 063 private DataSource dataSource; 064 065 private NamedParameterJdbcTemplate jdbcTemplate; 066 067 private DbPlatform dbPlatform; 068 069 public static final String RESOURCES_TABLE = "containment"; 070 071 private static final String TRANSACTION_OPERATIONS_TABLE = "containment_transactions"; 072 073 public static final String FEDORA_ID_COLUMN = "fedora_id"; 074 075 private static final String PARENT_COLUMN = "parent"; 076 077 private static final String TRANSACTION_ID_COLUMN = "transaction_id"; 078 079 private static final String OPERATION_COLUMN = "operation"; 080 081 private static final String START_TIME_COLUMN = "start_time"; 082 083 private static final String END_TIME_COLUMN = "end_time"; 084 085 private static final String UPDATED_COLUMN = "updated"; 086 087 /* 088 * Select children of a resource that are not marked as deleted. 089 */ 090 private static final String SELECT_CHILDREN = "SELECT " + FEDORA_ID_COLUMN + 091 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + " IS NULL" + 092 " ORDER BY " + FEDORA_ID_COLUMN + " LIMIT :containsLimit OFFSET :offSet"; 093 094 /* 095 * Select children of a memento of a resource. 096 */ 097 private static final String SELECT_CHILDREN_OF_MEMENTO = "SELECT " + FEDORA_ID_COLUMN + 098 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + START_TIME_COLUMN + 099 " <= :asOfTime AND (" + END_TIME_COLUMN + " > :asOfTime OR " + END_TIME_COLUMN + " IS NULL) ORDER BY " + 100 FEDORA_ID_COLUMN + " LIMIT :containsLimit OFFSET :offSet"; 101 102 /* 103 * Select children of a parent from resources table and from the transaction table with an 'add' operation, 104 * but exclude any records that also exist in the transaction table with a 'delete' or 'purge' operation. 105 */ 106 private static final String SELECT_CHILDREN_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 107 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent" + 108 " AND " + END_TIME_COLUMN + " IS NULL " + 109 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + 110 " WHERE " + PARENT_COLUMN + " = :parent AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 111 " AND " + OPERATION_COLUMN + " = 'add') x" + 112 " WHERE NOT EXISTS " + 113 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 114 " WHERE " + PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = x." + FEDORA_ID_COLUMN + 115 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))" + 116 " ORDER BY x." + FEDORA_ID_COLUMN + " LIMIT :containsLimit OFFSET :offSet"; 117 118 /* 119 * Select all children of a resource that are marked for deletion. 120 */ 121 private static final String SELECT_DELETED_CHILDREN = "SELECT " + FEDORA_ID_COLUMN + 122 " FROM " + RESOURCES_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + 123 " IS NOT NULL ORDER BY " + FEDORA_ID_COLUMN + " LIMIT :containsLimit OFFSET :offSet"; 124 125 /* 126 * Select children of a resource plus children 'delete'd in the non-committed transaction, but excluding any 127 * 'add'ed in the non-committed transaction. 128 */ 129 private static final String SELECT_DELETED_CHILDREN_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + 130 " FROM (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + 131 " WHERE " + PARENT_COLUMN + " = :parent AND " + END_TIME_COLUMN + " IS NOT NULL UNION" + 132 " SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 133 PARENT_COLUMN + " = :parent AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 134 OPERATION_COLUMN + " = 'delete') x" + 135 " WHERE NOT EXISTS " + 136 "(SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + PARENT_COLUMN + " = :parent AND " + 137 FEDORA_ID_COLUMN + " = x." + FEDORA_ID_COLUMN + " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 138 OPERATION_COLUMN + " = 'add') ORDER BY x." + FEDORA_ID_COLUMN + " LIMIT :containsLimit OFFSET :offSet"; 139 140 /* 141 * Upsert a parent child relationship to the transaction operation table. 142 */ 143 private static final String UPSERT_RECORDS_POSTGRESQL = "INSERT INTO " + TRANSACTION_OPERATIONS_TABLE + 144 " ( " + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ", " + 145 TRANSACTION_ID_COLUMN + ", " + OPERATION_COLUMN + ") VALUES (:parent, :child, :startTime, :endTime, " + 146 ":transactionId, :operation) ON CONFLICT ( " + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ") " + 147 "DO UPDATE SET " + PARENT_COLUMN + " = EXCLUDED." + PARENT_COLUMN + ", " + 148 START_TIME_COLUMN + " = EXCLUDED." + START_TIME_COLUMN + ", " + END_TIME_COLUMN + " = EXCLUDED." + 149 END_TIME_COLUMN + ", " + OPERATION_COLUMN + " = EXCLUDED." + OPERATION_COLUMN; 150 151 private static final String UPSERT_RECORDS_MYSQL_MARIA = "INSERT INTO " + TRANSACTION_OPERATIONS_TABLE + 152 " (" + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ", " + 153 TRANSACTION_ID_COLUMN + ", " + OPERATION_COLUMN + ") VALUES (:parent, :child, :startTime, :endTime, " + 154 ":transactionId, :operation) ON DUPLICATE KEY UPDATE " + 155 PARENT_COLUMN + " = VALUES(" + PARENT_COLUMN + "), " + START_TIME_COLUMN + " = VALUES(" + 156 START_TIME_COLUMN + "), " + END_TIME_COLUMN + " = VALUES(" + END_TIME_COLUMN + "), " + OPERATION_COLUMN + 157 " = VALUES(" + OPERATION_COLUMN + ")"; 158 159 private static final String UPSERT_RECORDS_H2 = "MERGE INTO " + TRANSACTION_OPERATIONS_TABLE + 160 " (" + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ", " + 161 TRANSACTION_ID_COLUMN + ", " + OPERATION_COLUMN + ") KEY (" + FEDORA_ID_COLUMN + ", " + 162 TRANSACTION_ID_COLUMN + ") VALUES (:parent, :child, :startTime, :endTime, :transactionId, :operation)"; 163 164 private static final String DIRECT_UPDATE_END_TIME = "UPDATE " + RESOURCES_TABLE + 165 " SET " + END_TIME_COLUMN + " = :endTime WHERE " + 166 PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = :child"; 167 168 private static final Map<DbPlatform, String> UPSERT_MAPPING = Map.of( 169 DbPlatform.H2, UPSERT_RECORDS_H2, 170 DbPlatform.MYSQL, UPSERT_RECORDS_MYSQL_MARIA, 171 DbPlatform.MARIADB, UPSERT_RECORDS_MYSQL_MARIA, 172 DbPlatform.POSTGRESQL, UPSERT_RECORDS_POSTGRESQL 173 ); 174 175 private static final String DIRECT_INSERT_RECORDS = "INSERT INTO " + RESOURCES_TABLE + 176 " (" + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ")" + 177 " VALUES (:parent, :child, :startTime, :endTime)"; 178 179 private static final String DIRECT_INSERT_POSTGRESQL = " ON CONFLICT ( " + FEDORA_ID_COLUMN + ")" + 180 "DO UPDATE SET " + PARENT_COLUMN + " = EXCLUDED." + PARENT_COLUMN + ", " + 181 START_TIME_COLUMN + " = EXCLUDED." + START_TIME_COLUMN + ", " + 182 END_TIME_COLUMN + " = EXCLUDED." + END_TIME_COLUMN; 183 184 private static final String DIRECT_INSERT_MYSQL_MARIA = " ON DUPLICATE KEY UPDATE " + 185 PARENT_COLUMN + " = VALUES(" + PARENT_COLUMN + "), " + START_TIME_COLUMN + " = VALUES(" + 186 START_TIME_COLUMN + "), " + END_TIME_COLUMN + " = VALUES(" + END_TIME_COLUMN + ")"; 187 188 private static final String DIRECT_INSERT_H2 = "MERGE INTO " + RESOURCES_TABLE + 189 " (" + PARENT_COLUMN + ", " + FEDORA_ID_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ")" + 190 " KEY (" + FEDORA_ID_COLUMN + ") VALUES (:parent, :child, :startTime, :endTime)"; 191 192 private static final Map<DbPlatform, String> DIRECT_UPSERT_MAPPING = Map.of( 193 DbPlatform.H2, DIRECT_INSERT_H2, 194 DbPlatform.MYSQL, DIRECT_INSERT_RECORDS + DIRECT_INSERT_MYSQL_MARIA, 195 DbPlatform.MARIADB, DIRECT_INSERT_RECORDS + DIRECT_INSERT_MYSQL_MARIA, 196 DbPlatform.POSTGRESQL, DIRECT_INSERT_RECORDS + DIRECT_INSERT_POSTGRESQL 197 ); 198 199 private static final String DIRECT_PURGE = "DELETE FROM containment WHERE fedora_id = :child"; 200 201 /* 202 * Remove an insert row from the transaction operation table for this parent child relationship. 203 */ 204 private static final String UNDO_INSERT_CHILD_IN_TRANSACTION = "DELETE FROM " + TRANSACTION_OPERATIONS_TABLE + 205 " WHERE " + PARENT_COLUMN + " = :parent AND " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 206 + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 207 208 /* 209 * Remove a mark as deleted row from the transaction operation table for this child relationship (no parent). 210 */ 211 private static final String UNDO_DELETE_CHILD_IN_TRANSACTION_NO_PARENT = "DELETE FROM " + 212 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 213 + " = :transactionId AND " + OPERATION_COLUMN + " = 'delete'"; 214 215 /* 216 * Is this parent child relationship being added in this transaction? 217 */ 218 private static final String IS_CHILD_ADDED_IN_TRANSACTION = "SELECT TRUE FROM " + TRANSACTION_OPERATIONS_TABLE + 219 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + PARENT_COLUMN + " = :parent" + 220 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 221 222 /* 223 * Is this child's relationship being marked for deletion in this transaction (no parent)? 224 */ 225 private static final String IS_CHILD_DELETED_IN_TRANSACTION_NO_PARENT = "SELECT TRUE FROM " + 226 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child " + 227 " AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'delete'"; 228 229 /* 230 * Delete all rows from the transaction operation table for this transaction. 231 */ 232 private static final String DELETE_ENTIRE_TRANSACTION = "DELETE FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 233 TRANSACTION_ID_COLUMN + " = :transactionId"; 234 235 /* 236 * Add to the main table all rows from the transaction operation table marked 'add' for this transaction. 237 */ 238 private static final String COMMIT_ADD_RECORDS_POSTGRESQL = "INSERT INTO " + RESOURCES_TABLE + 239 " ( " + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ") " + 240 "SELECT " + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + 241 " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + OPERATION_COLUMN + " = 'add' AND " + 242 TRANSACTION_ID_COLUMN + " = :transactionId ON CONFLICT ( " + FEDORA_ID_COLUMN + " )" + 243 " DO UPDATE SET " + PARENT_COLUMN + " = EXCLUDED." + PARENT_COLUMN + ", " + 244 START_TIME_COLUMN + " = EXCLUDED." + START_TIME_COLUMN + ", " + END_TIME_COLUMN + " = EXCLUDED." + 245 END_TIME_COLUMN; 246 247 private static final String COMMIT_ADD_RECORDS_MYSQL_MARIA = "INSERT INTO " + RESOURCES_TABLE + 248 " (" + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ") " + 249 "SELECT " + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + 250 " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + OPERATION_COLUMN + " = 'add' AND " + 251 TRANSACTION_ID_COLUMN + " = :transactionId ON DUPLICATE KEY UPDATE " + 252 PARENT_COLUMN + " = VALUES(" + PARENT_COLUMN + "), " + START_TIME_COLUMN + " = VALUES(" + 253 START_TIME_COLUMN + "), " + END_TIME_COLUMN + " = VALUES(" + END_TIME_COLUMN + ")"; 254 255 private static final String COMMIT_ADD_RECORDS_H2 = "MERGE INTO " + RESOURCES_TABLE + 256 " (" + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + START_TIME_COLUMN + ", " + END_TIME_COLUMN + ") " + 257 "KEY (" + FEDORA_ID_COLUMN + ") SELECT " + FEDORA_ID_COLUMN + ", " + PARENT_COLUMN + ", " + 258 START_TIME_COLUMN + ", " + END_TIME_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 259 OPERATION_COLUMN + " = 'add' AND " + TRANSACTION_ID_COLUMN + " = :transactionId"; 260 261 private static final Map<DbPlatform, String> COMMIT_ADD_RECORDS_MAP = Map.of( 262 DbPlatform.H2, COMMIT_ADD_RECORDS_H2, 263 DbPlatform.MYSQL, COMMIT_ADD_RECORDS_MYSQL_MARIA, 264 DbPlatform.MARIADB, COMMIT_ADD_RECORDS_MYSQL_MARIA, 265 DbPlatform.POSTGRESQL, COMMIT_ADD_RECORDS_POSTGRESQL 266 ); 267 268 /* 269 * Add an end time to the rows in the main table that match all rows from transaction operation table marked 270 * 'delete' for this transaction. 271 */ 272 private static final String COMMIT_DELETE_RECORDS_H2 = "UPDATE " + RESOURCES_TABLE + 273 " r SET r." + END_TIME_COLUMN + " = ( SELECT t." + END_TIME_COLUMN + " FROM " + 274 TRANSACTION_OPERATIONS_TABLE + " t " + 275 " WHERE t." + FEDORA_ID_COLUMN + " = r." + FEDORA_ID_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + 276 " = :transactionId AND t." + OPERATION_COLUMN + 277 " = 'delete' AND t." + PARENT_COLUMN + " = r." + PARENT_COLUMN + " AND r." + 278 END_TIME_COLUMN + " IS NULL)" + 279 " WHERE EXISTS (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + FEDORA_ID_COLUMN + 280 " = r." + FEDORA_ID_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + " = :transactionId AND t." + 281 OPERATION_COLUMN + " = 'delete' AND t." + PARENT_COLUMN + " = r." + PARENT_COLUMN + " AND r." + 282 END_TIME_COLUMN + " IS NULL)"; 283 284 private static final String COMMIT_DELETE_RECORDS_MYSQL = "UPDATE " + RESOURCES_TABLE + 285 " r INNER JOIN " + TRANSACTION_OPERATIONS_TABLE + " t ON t." + FEDORA_ID_COLUMN + " = r." + 286 FEDORA_ID_COLUMN + " SET r." + END_TIME_COLUMN + " = t." + END_TIME_COLUMN + 287 " WHERE t." + PARENT_COLUMN + " = r." + 288 PARENT_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + " = :transactionId AND t." + OPERATION_COLUMN + 289 " = 'delete' AND r." + END_TIME_COLUMN + " IS NULL"; 290 291 private static final String COMMIT_DELETE_RECORDS_POSTGRES = "UPDATE " + RESOURCES_TABLE + " SET " + 292 END_TIME_COLUMN + " = t." + END_TIME_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + 293 FEDORA_ID_COLUMN + " = " + RESOURCES_TABLE + "." + FEDORA_ID_COLUMN + " AND t." + PARENT_COLUMN + 294 " = " + RESOURCES_TABLE + "." + PARENT_COLUMN + " AND t." + TRANSACTION_ID_COLUMN + 295 " = :transactionId AND t." + OPERATION_COLUMN + " = 'delete' AND " + RESOURCES_TABLE + "." + 296 END_TIME_COLUMN + " IS NULL"; 297 298 private final Map<DbPlatform, String> COMMIT_DELETE_RECORDS = Map.of( 299 DbPlatform.H2, COMMIT_DELETE_RECORDS_H2, 300 DbPlatform.MARIADB, COMMIT_DELETE_RECORDS_MYSQL, 301 DbPlatform.MYSQL, COMMIT_DELETE_RECORDS_MYSQL, 302 DbPlatform.POSTGRESQL, COMMIT_DELETE_RECORDS_POSTGRES 303 ); 304 305 /* 306 * Remove from the main table all rows from transaction operation table marked 'purge' for this transaction. 307 */ 308 private static final String COMMIT_PURGE_RECORDS = "DELETE FROM " + RESOURCES_TABLE + " WHERE " + 309 "EXISTS (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " t WHERE t." + 310 TRANSACTION_ID_COLUMN + " = :transactionId AND t." + OPERATION_COLUMN + " = 'purge' AND" + 311 " t." + FEDORA_ID_COLUMN + " = " + RESOURCES_TABLE + "." + FEDORA_ID_COLUMN + 312 " AND t." + PARENT_COLUMN + " = " + RESOURCES_TABLE + "." + PARENT_COLUMN + ")"; 313 314 /* 315 * Query if a resource exists in the main table and is not deleted. 316 */ 317 private static final String RESOURCE_EXISTS = "SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + 318 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NULL"; 319 320 /* 321 * Resource exists as a record in the transaction operations table with an 'add' operation and not also 322 * exists as a 'delete' operation. 323 */ 324 private static final String RESOURCE_EXISTS_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 325 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 326 " AND " + END_TIME_COLUMN + " IS NULL UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + 327 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 328 " = :transactionId" + " AND " + OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS " + 329 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 330 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 331 " AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))"; 332 333 /* 334 * Query if a resource exists in the main table even if it is deleted. 335 */ 336 private static final String RESOURCE_OR_TOMBSTONE_EXISTS = "SELECT " + FEDORA_ID_COLUMN + " FROM " + 337 RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child"; 338 339 /* 340 * Resource exists as a record in the main table even if deleted or in the transaction operations table with an 341 * 'add' operation and not also exists as a 'delete' operation. 342 */ 343 private static final String RESOURCE_OR_TOMBSTONE_EXISTS_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM" + 344 " (SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 345 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + 346 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 347 " = :transactionId" + " AND " + OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS " + 348 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 349 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 350 " AND " + OPERATION_COLUMN + " IN ('delete', 'purge'))"; 351 352 353 /* 354 * Get the parent ID for this resource from the main table if not deleted. 355 */ 356 private static final String PARENT_EXISTS = "SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + 357 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NULL"; 358 359 /* 360 * Get the parent ID for this resource from the operations table for an 'add' operation in this transaction, but 361 * exclude any 'delete' operations for this resource in this transaction. 362 */ 363 private static final String PARENT_EXISTS_IN_TRANSACTION = "SELECT x." + PARENT_COLUMN + " FROM" + 364 " (SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 365 " AND " + END_TIME_COLUMN + " IS NULL" + 366 " UNION SELECT " + PARENT_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + 367 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 368 " AND " + OPERATION_COLUMN + " = 'add') x" + 369 " WHERE NOT EXISTS " + 370 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 371 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + " = :transactionId" + 372 " AND " + OPERATION_COLUMN + " = 'delete')"; 373 374 /* 375 * Get the parent ID for this resource from the main table if deleted. 376 */ 377 private static final String PARENT_EXISTS_DELETED = "SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + 378 " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + END_TIME_COLUMN + " IS NOT NULL"; 379 380 /* 381 * Get the parent ID for this resource from main table and the operations table for a 'delete' operation in this 382 * transaction, excluding any 'add' operations for this resource in this transaction. 383 */ 384 private static final String PARENT_EXISTS_DELETED_IN_TRANSACTION = "SELECT x." + PARENT_COLUMN + " FROM" + 385 " (SELECT " + PARENT_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child" + 386 " AND " + END_TIME_COLUMN + " IS NOT NULL UNION SELECT " + PARENT_COLUMN + " FROM " + 387 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN + 388 " = :transactionId AND " + OPERATION_COLUMN + " = 'delete') x WHERE NOT EXISTS " + 389 " (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + 390 TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add')"; 391 392 /* 393 * Does this resource exist in the transaction operation table for an 'add' record. 394 */ 395 private static final String IS_CHILD_ADDED_IN_TRANSACTION_NO_PARENT = "SELECT TRUE FROM " + 396 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + 397 TRANSACTION_ID_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 398 399 /* 400 * Delete a row from the transaction operation table with this resource and 'add' operation, no parent required. 401 */ 402 private static final String UNDO_INSERT_CHILD_IN_TRANSACTION_NO_PARENT = "DELETE FROM " + 403 TRANSACTION_OPERATIONS_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :child AND " + TRANSACTION_ID_COLUMN 404 + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 405 406 private static final String TRUNCATE_TABLE = "TRUNCATE TABLE "; 407 408 /* 409 * Any record tracked in the containment index is either active or a tombstone. Either way it exists for the 410 * purpose of finding ghost nodes. 411 */ 412 private static final String SELECT_ID_LIKE = "SELECT " + FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + 413 FEDORA_ID_COLUMN + " LIKE :resourceId"; 414 415 private static final String SELECT_ID_LIKE_IN_TRANSACTION = "SELECT x." + FEDORA_ID_COLUMN + " FROM (SELECT " + 416 FEDORA_ID_COLUMN + " FROM " + RESOURCES_TABLE + " WHERE " + FEDORA_ID_COLUMN + " LIKE :resourceId" + 417 " UNION SELECT " + FEDORA_ID_COLUMN + " FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + 418 FEDORA_ID_COLUMN + " LIKE :resourceId AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 419 OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS (SELECT 1 FROM " + TRANSACTION_OPERATIONS_TABLE + 420 " WHERE " + FEDORA_ID_COLUMN + " LIKE :resourceId AND " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 421 OPERATION_COLUMN + " = 'delete')"; 422 423 private static final String SELECT_LAST_UPDATED = "SELECT " + UPDATED_COLUMN + " FROM " + RESOURCES_TABLE + 424 " WHERE " + FEDORA_ID_COLUMN + " = :resourceId"; 425 426 private static final String UPDATE_LAST_UPDATED = "UPDATE " + RESOURCES_TABLE + " SET " + UPDATED_COLUMN + 427 " = :updated WHERE " + FEDORA_ID_COLUMN + " = :resourceId"; 428 429 private static final String CONDITIONALLY_UPDATE_LAST_UPDATED = "UPDATE " + RESOURCES_TABLE + 430 " SET " + UPDATED_COLUMN + " = :updated WHERE " + FEDORA_ID_COLUMN + " = :resourceId" + 431 " AND (" + UPDATED_COLUMN + " IS NULL OR " + UPDATED_COLUMN + " < :updated)"; 432 433 private static final String SELECT_LAST_UPDATED_IN_TX = "SELECT MAX(x.updated)" + 434 " FROM (SELECT " + UPDATED_COLUMN + " as updated FROM " + RESOURCES_TABLE + " WHERE " + 435 FEDORA_ID_COLUMN + " = :resourceId UNION SELECT " + START_TIME_COLUMN + 436 " as updated FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + PARENT_COLUMN + " = :resourceId AND " + 437 OPERATION_COLUMN + " = 'add' AND " + TRANSACTION_ID_COLUMN + " = :transactionId UNION SELECT " + 438 END_TIME_COLUMN + " as updated FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + PARENT_COLUMN + 439 " = :resourceId AND " + OPERATION_COLUMN + " = 'delete' AND " + TRANSACTION_ID_COLUMN + 440 " = :transactionId UNION SELECT " + END_TIME_COLUMN + 441 " as updated FROM " + TRANSACTION_OPERATIONS_TABLE + " WHERE " + PARENT_COLUMN + " = :resourceId AND " + 442 OPERATION_COLUMN + " = 'add' AND " + TRANSACTION_ID_COLUMN + " = :transactionId) x"; 443 444 private static final String GET_UPDATED_RESOURCES = "SELECT DISTINCT " + PARENT_COLUMN + " FROM " + 445 TRANSACTION_OPERATIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 446 OPERATION_COLUMN + " in ('add', 'delete')"; 447 448 /* 449 * Get the startTime for the specified resource from the main table, if it exists. 450 */ 451 private static final String GET_START_TIME = "SELECT " + START_TIME_COLUMN + " FROM " + RESOURCES_TABLE + 452 " WHERE " + FEDORA_ID_COLUMN + " = :child"; 453 454 /* 455 * Get all resources deleted in this transaction 456 */ 457 private static final String GET_DELETED_RESOURCES = "SELECT " + FEDORA_ID_COLUMN + " FROM " + 458 TRANSACTION_OPERATIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 459 OPERATION_COLUMN + " = 'delete'"; 460 461 /* 462 * Get all resources added in this transaction 463 */ 464 private static final String GET_ADDED_RESOURCES = "SELECT " + FEDORA_ID_COLUMN + " FROM " + 465 TRANSACTION_OPERATIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :transactionId AND " + 466 OPERATION_COLUMN + " = 'add'"; 467 468 @Inject 469 private FedoraPropsConfig fedoraPropsConfig; 470 471 private Cache<String, String> getContainedByCache; 472 473 private Cache<String, Boolean> resourceExistsCache; 474 475 /** 476 * Connect to the database 477 */ 478 @PostConstruct 479 private void setup() { 480 jdbcTemplate = getNamedParameterJdbcTemplate(); 481 dbPlatform = DbPlatform.fromDataSource(dataSource); 482 this.getContainedByCache = Caffeine.newBuilder() 483 .maximumSize(fedoraPropsConfig.getContainmentCacheSize()) 484 .expireAfterAccess(fedoraPropsConfig.getContainmentCacheTimeout(), TimeUnit.MINUTES) 485 .build(); 486 this.resourceExistsCache = Caffeine.newBuilder() 487 .maximumSize(fedoraPropsConfig.getContainmentCacheSize()) 488 .expireAfterAccess(fedoraPropsConfig.getContainmentCacheTimeout(), TimeUnit.MINUTES) 489 .build(); 490 } 491 492 private NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { 493 return new NamedParameterJdbcTemplate(getDataSource()); 494 } 495 496 void setContainsLimit(final int limit) { 497 containsLimit = limit; 498 } 499 500 @Override 501 public Stream<String> getContains(@Nonnull final Transaction tx, final FedoraId fedoraId) { 502 final String resourceId = fedoraId.isMemento() ? fedoraId.getBaseId() : fedoraId.getFullId(); 503 final Instant asOfTime = fedoraId.isMemento() ? fedoraId.getMementoInstant() : null; 504 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 505 parameterSource.addValue("parent", resourceId); 506 507 LOGGER.debug("getContains for {} in transaction {} and instant {}", resourceId, tx, asOfTime); 508 509 final String query; 510 if (asOfTime == null) { 511 if (tx.isOpenLongRunning()) { 512 // we are in a transaction 513 parameterSource.addValue("transactionId", tx.getId()); 514 query = SELECT_CHILDREN_IN_TRANSACTION; 515 } else { 516 // not in a transaction 517 query = SELECT_CHILDREN; 518 } 519 } else { 520 parameterSource.addValue("asOfTime", formatInstant(asOfTime)); 521 query = SELECT_CHILDREN_OF_MEMENTO; 522 } 523 524 return StreamSupport.stream(new ContainmentIterator(query, parameterSource), false); 525 } 526 527 @Override 528 public Stream<String> getContainsDeleted(@Nonnull final Transaction tx, final FedoraId fedoraId) { 529 final String resourceId = fedoraId.getFullId(); 530 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 531 parameterSource.addValue("parent", resourceId); 532 533 final String query; 534 if (tx.isOpenLongRunning()) { 535 // we are in a transaction 536 parameterSource.addValue("transactionId", tx.getId()); 537 query = SELECT_DELETED_CHILDREN_IN_TRANSACTION; 538 } else { 539 // not in a transaction 540 query = SELECT_DELETED_CHILDREN; 541 } 542 LOGGER.debug("getContainsDeleted for {} in transaction {}", resourceId, tx); 543 return StreamSupport.stream(new ContainmentIterator(query, parameterSource), false); 544 } 545 546 @Override 547 public String getContainedBy(@Nonnull final Transaction tx, final FedoraId resource) { 548 final String resourceID = resource.getFullId(); 549 final String parentID; 550 if (tx.isOpenLongRunning()) { 551 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_IN_TRANSACTION, Map.of("child", resourceID, 552 "transactionId", tx.getId()), String.class).stream().findFirst().orElse(null); 553 } else { 554 parentID = this.getContainedByCache.get(resourceID, key -> 555 jdbcTemplate.queryForList(PARENT_EXISTS, Map.of("child", key), String.class).stream() 556 .findFirst().orElse(null) 557 ); 558 } 559 return parentID; 560 } 561 562 @Override 563 public void addContainedBy(@Nonnull final Transaction tx, final FedoraId parent, final FedoraId child) { 564 addContainedBy(tx, parent, child, Instant.now(), null); 565 } 566 567 @Override 568 public void addContainedBy(@Nonnull final Transaction tx, final FedoraId parent, final FedoraId child, 569 final Instant startTime, final Instant endTime) { 570 // Don't add containment for these types of children 571 if (childShouldNotBeContained(child)) { 572 return; 573 } 574 tx.doInTx(() -> { 575 final String parentID = parent.getFullId(); 576 final String childID = child.getFullId(); 577 578 if (!tx.isShortLived()) { 579 LOGGER.debug("Adding: parent: {}, child: {}, in txn: {}, start time {}, end time {}", parentID, childID, 580 tx.getId(), formatInstant(startTime), formatInstant(endTime)); 581 doUpsert(tx, parentID, childID, startTime, endTime, "add"); 582 } else { 583 LOGGER.debug("Adding: parent: {}, child: {}, start time {}, end time {}", parentID, childID, 584 formatInstant(startTime), formatInstant(endTime)); 585 doDirectUpsert(parentID, childID, startTime, endTime); 586 } 587 }); 588 } 589 590 private boolean childShouldNotBeContained(final FedoraId child) { 591 return child.isAcl(); 592 } 593 594 @Override 595 public void removeContainedBy(@Nonnull final Transaction tx, final FedoraId parent, final FedoraId child) { 596 tx.doInTx(() -> { 597 final String parentID = parent.getFullId(); 598 final String childID = child.getFullId(); 599 600 if (!tx.isShortLived()) { 601 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 602 parameterSource.addValue("parent", parentID); 603 parameterSource.addValue("child", childID); 604 parameterSource.addValue("transactionId", tx.getId()); 605 final boolean addedInTxn = !jdbcTemplate.queryForList(IS_CHILD_ADDED_IN_TRANSACTION, parameterSource) 606 .isEmpty(); 607 if (addedInTxn) { 608 jdbcTemplate.update(UNDO_INSERT_CHILD_IN_TRANSACTION, parameterSource); 609 } else { 610 doUpsert(tx, parentID, childID, null, Instant.now(), "delete"); 611 } 612 } else { 613 doDirectUpsert(parentID, childID, null, Instant.now()); 614 this.getContainedByCache.invalidate(childID); 615 } 616 }); 617 } 618 619 @Override 620 public void removeResource(@Nonnull final Transaction tx, final FedoraId resource) { 621 tx.doInTx(() -> { 622 final String resourceID = resource.getFullId(); 623 624 if (!tx.isShortLived()) { 625 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 626 parameterSource.addValue("child", resourceID); 627 parameterSource.addValue("transactionId", tx.getId()); 628 final boolean addedInTxn = !jdbcTemplate.queryForList(IS_CHILD_ADDED_IN_TRANSACTION_NO_PARENT, 629 parameterSource).isEmpty(); 630 if (addedInTxn) { 631 jdbcTemplate.update(UNDO_INSERT_CHILD_IN_TRANSACTION_NO_PARENT, parameterSource); 632 } else { 633 final String parent = getContainedBy(tx, resource); 634 if (parent != null) { 635 LOGGER.debug("Marking containment relationship between parent ({}) and child ({}) deleted", 636 parent, resourceID); 637 doUpsert(tx, parent, resourceID, null, Instant.now(), "delete"); 638 } 639 } 640 } else { 641 final String parent = getContainedBy(tx, resource); 642 if (parent != null) { 643 LOGGER.debug("Marking containment relationship between parent ({}) and child ({}) deleted", parent, 644 resourceID); 645 doDirectUpsert(parent, resourceID, null, Instant.now()); 646 this.getContainedByCache.invalidate(resourceID); 647 } 648 } 649 }); 650 } 651 652 @Override 653 public void purgeResource(@Nonnull final Transaction tx, final FedoraId resource) { 654 tx.doInTx(() -> { 655 final String resourceID = resource.getFullId(); 656 657 final String parent = getContainedByDeleted(tx, resource); 658 659 if (parent != null) { 660 LOGGER.debug("Removing containment relationship between parent ({}) and child ({})", 661 parent, resourceID); 662 663 if (!tx.isShortLived()) { 664 doUpsert(tx, parent, resourceID, null, null, "purge"); 665 } else { 666 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 667 parameterSource.addValue("child", resourceID); 668 jdbcTemplate.update(DIRECT_PURGE, parameterSource); 669 } 670 } 671 }); 672 } 673 674 /** 675 * Do the Upsert action to the transaction table. 676 * @param tx the transaction 677 * @param parentId the containing resource id 678 * @param resourceId the contained resource id 679 * @param startTime the instant the relationship started, if null get the current time from the main table. 680 * @param endTime the instant the relationship ended or null for none. 681 * @param operation the operation to perform. 682 */ 683 private void doUpsert(final Transaction tx, final String parentId, final String resourceId, final Instant startTime, 684 final Instant endTime, final String operation) { 685 final var parameterSource = new MapSqlParameterSource(); 686 parameterSource.addValue("child", resourceId); 687 parameterSource.addValue("transactionId", tx.getId()); 688 parameterSource.addValue("parent", parentId); 689 if (startTime == null) { 690 parameterSource.addValue("startTime", formatInstant(getCurrentStartTime(resourceId))); 691 } else { 692 parameterSource.addValue("startTime", formatInstant(startTime)); 693 } 694 parameterSource.addValue("endTime", formatInstant(endTime)); 695 parameterSource.addValue("operation", operation); 696 jdbcTemplate.update(UPSERT_MAPPING.get(dbPlatform), parameterSource); 697 } 698 699 /** 700 * Do the Upsert directly to the containment index; not the tx table 701 * 702 * @param parentId the containing resource id 703 * @param resourceId the contained resource id 704 * @param startTime the instant the relationship started, if null get the current time from the main table. 705 * @param endTime the instant the relationship ended or null for none. 706 */ 707 private void doDirectUpsert(final String parentId, final String resourceId, final Instant startTime, 708 final Instant endTime) { 709 final var parameterSource = new MapSqlParameterSource(); 710 parameterSource.addValue("child", resourceId); 711 parameterSource.addValue("parent", parentId); 712 parameterSource.addValue("endTime", formatInstant(endTime)); 713 714 final String query; 715 716 if (startTime == null) { 717 // This the case for an update 718 query = DIRECT_UPDATE_END_TIME; 719 } else { 720 // This is the case for a new record 721 parameterSource.addValue("startTime", formatInstant(startTime)); 722 query = DIRECT_UPSERT_MAPPING.get(dbPlatform); 723 } 724 725 jdbcTemplate.update(query, parameterSource); 726 updateParentTimestamp(parentId, startTime, endTime); 727 resourceExistsCache.invalidate(resourceId); 728 } 729 730 private void updateParentTimestamp(final String parentId, final Instant startTime, final Instant endTime) { 731 final var parameterSource = new MapSqlParameterSource(); 732 final var updated = endTime == null ? startTime : endTime; 733 parameterSource.addValue("resourceId", parentId); 734 parameterSource.addValue("updated", formatInstant(updated)); 735 jdbcTemplate.update(CONDITIONALLY_UPDATE_LAST_UPDATED, parameterSource); 736 } 737 738 /** 739 * Find parent for a resource using a deleted containment relationship. 740 * @param tx the transaction. 741 * @param resource the child resource id. 742 * @return the parent id. 743 */ 744 private String getContainedByDeleted(final Transaction tx, final FedoraId resource) { 745 final String resourceID = resource.getFullId(); 746 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 747 parameterSource.addValue("child", resourceID); 748 final List<String> parentID; 749 if (tx.isOpenLongRunning()) { 750 parameterSource.addValue("transactionId", tx.getId()); 751 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_DELETED_IN_TRANSACTION, parameterSource, String.class); 752 } else { 753 parentID = jdbcTemplate.queryForList(PARENT_EXISTS_DELETED, parameterSource, String.class); 754 } 755 return parentID.stream().findFirst().orElse(null); 756 } 757 758 @Override 759 public void commitTransaction(final Transaction tx) { 760 if (!tx.isShortLived()) { 761 tx.ensureCommitting(); 762 try { 763 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 764 parameterSource.addValue("transactionId", tx.getId()); 765 final List<String> changedParents = jdbcTemplate.queryForList(GET_UPDATED_RESOURCES, parameterSource, 766 String.class); 767 final List<String> removedResources = jdbcTemplate.queryForList(GET_DELETED_RESOURCES, parameterSource, 768 String.class); 769 final List<String> addedResources = jdbcTemplate.queryForList(GET_ADDED_RESOURCES, parameterSource, 770 String.class); 771 final int purged = jdbcTemplate.update(COMMIT_PURGE_RECORDS, parameterSource); 772 final int deleted = jdbcTemplate.update(COMMIT_DELETE_RECORDS.get(dbPlatform), parameterSource); 773 final int added = jdbcTemplate.update(COMMIT_ADD_RECORDS_MAP.get(dbPlatform), parameterSource); 774 for (final var parent : changedParents) { 775 final var updated = jdbcTemplate.queryForObject(SELECT_LAST_UPDATED_IN_TX, 776 Map.of("resourceId", parent, "transactionId", tx.getId()), Timestamp.class); 777 if (updated != null) { 778 jdbcTemplate.update(UPDATE_LAST_UPDATED, 779 Map.of("resourceId", parent, "updated", updated)); 780 } 781 } 782 jdbcTemplate.update(DELETE_ENTIRE_TRANSACTION, parameterSource); 783 this.getContainedByCache.invalidateAll(removedResources); 784 // Add inserted records to removed records list. 785 removedResources.addAll(addedResources); 786 this.resourceExistsCache.invalidateAll(removedResources); 787 LOGGER.debug("Commit of tx {} complete with {} adds, {} deletes and {} purges", 788 tx.getId(), added, deleted, purged); 789 } catch (final Exception e) { 790 LOGGER.warn("Unable to commit containment index transaction {}: {}", tx, e.getMessage()); 791 throw new RepositoryRuntimeException("Unable to commit containment index transaction", e); 792 } 793 } 794 } 795 796 @Transactional(propagation = Propagation.NOT_SUPPORTED) 797 @Override 798 public void rollbackTransaction(final Transaction tx) { 799 if (!tx.isShortLived()) { 800 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 801 parameterSource.addValue("transactionId", tx.getId()); 802 jdbcTemplate.update(DELETE_ENTIRE_TRANSACTION, parameterSource); 803 } 804 } 805 806 @Override 807 public void clearAllTransactions() { 808 jdbcTemplate.update(TRUNCATE_TABLE + TRANSACTION_OPERATIONS_TABLE, Collections.emptyMap()); 809 } 810 811 @Override 812 public boolean resourceExists(@Nonnull final Transaction tx, final FedoraId fedoraId, 813 final boolean includeDeleted) { 814 // Get the containing ID because fcr:metadata will not exist here but MUST exist if the containing resource does 815 final String resourceId = fedoraId.getBaseId(); 816 LOGGER.debug("Checking if {} exists in transaction {}", resourceId, tx); 817 if (fedoraId.isRepositoryRoot()) { 818 // Root always exists. 819 return true; 820 } 821 if (tx.isOpenLongRunning()) { 822 final var queryToUse = includeDeleted ? RESOURCE_OR_TOMBSTONE_EXISTS_IN_TRANSACTION : 823 RESOURCE_EXISTS_IN_TRANSACTION; 824 return !jdbcTemplate.queryForList(queryToUse, 825 Map.of("child", resourceId, "transactionId", tx.getId()), String.class).isEmpty(); 826 } else if (includeDeleted) { 827 final Boolean exists = resourceExistsCache.getIfPresent(resourceId); 828 if (exists != null && exists) { 829 // Only return true, false values might change once deleted resources are included. 830 return true; 831 } 832 return !jdbcTemplate.queryForList(RESOURCE_OR_TOMBSTONE_EXISTS, 833 Map.of("child", resourceId), String.class).isEmpty(); 834 } else { 835 return resourceExistsCache.get(resourceId, key -> !jdbcTemplate.queryForList(RESOURCE_EXISTS, 836 Map.of("child", resourceId), String.class).isEmpty() 837 ); 838 } 839 } 840 841 @Override 842 public FedoraId getContainerIdByPath(final Transaction tx, final FedoraId fedoraId, final boolean checkDeleted) { 843 if (fedoraId.isRepositoryRoot()) { 844 // If we are root then we are the top. 845 return fedoraId; 846 } 847 final String parent = getContainedBy(tx, fedoraId); 848 if (parent != null) { 849 return FedoraId.create(parent); 850 } 851 String fullId = fedoraId.getFullId(); 852 while (fullId.contains("/")) { 853 fullId = fedoraId.getResourceId().substring(0, fullId.lastIndexOf("/")); 854 if (fullId.equals(FEDORA_ID_PREFIX)) { 855 return FedoraId.getRepositoryRootId(); 856 } 857 final FedoraId testID = FedoraId.create(fullId); 858 if (resourceExists(tx, testID, checkDeleted)) { 859 return testID; 860 } 861 } 862 return FedoraId.getRepositoryRootId(); 863 } 864 865 @Override 866 public void reset() { 867 try { 868 jdbcTemplate.update(TRUNCATE_TABLE + RESOURCES_TABLE, Collections.emptyMap()); 869 jdbcTemplate.update(TRUNCATE_TABLE + TRANSACTION_OPERATIONS_TABLE, Collections.emptyMap()); 870 this.getContainedByCache.invalidateAll(); 871 } catch (final Exception e) { 872 throw new RepositoryRuntimeException("Failed to truncate containment tables", e); 873 } 874 } 875 876 @Override 877 public boolean hasResourcesStartingWith(final Transaction tx, final FedoraId fedoraId) { 878 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 879 parameterSource.addValue("resourceId", fedoraId.getFullId() + "/%"); 880 final boolean matchingIds; 881 if (tx.isOpenLongRunning()) { 882 parameterSource.addValue("transactionId", tx.getId()); 883 matchingIds = !jdbcTemplate.queryForList(SELECT_ID_LIKE_IN_TRANSACTION, parameterSource, String.class) 884 .isEmpty(); 885 } else { 886 matchingIds = !jdbcTemplate.queryForList(SELECT_ID_LIKE, parameterSource, String.class).isEmpty(); 887 } 888 return matchingIds; 889 } 890 891 @Override 892 public Instant containmentLastUpdated(final Transaction tx, final FedoraId fedoraId) { 893 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 894 parameterSource.addValue("resourceId", fedoraId.getFullId()); 895 final String queryToUse; 896 if (tx.isOpenLongRunning()) { 897 parameterSource.addValue("transactionId", tx.getId()); 898 queryToUse = SELECT_LAST_UPDATED_IN_TX; 899 } else { 900 queryToUse = SELECT_LAST_UPDATED; 901 } 902 try { 903 return fromTimestamp(jdbcTemplate.queryForObject(queryToUse, parameterSource, Timestamp.class)); 904 } catch (final EmptyResultDataAccessException e) { 905 return null; 906 } 907 } 908 909 /** 910 * Get the data source backing this containment index 911 * @return data source 912 */ 913 public DataSource getDataSource() { 914 return dataSource; 915 } 916 917 /** 918 * Set the data source backing this containment index 919 * @param dataSource data source 920 */ 921 public void setDataSource(final DataSource dataSource) { 922 this.dataSource = dataSource; 923 } 924 925 /** 926 * Get the current startTime for the resource 927 * @param resourceId id of the resource 928 * @return start time or null if no committed record. 929 */ 930 private Instant getCurrentStartTime(final String resourceId) { 931 return fromTimestamp(jdbcTemplate.queryForObject(GET_START_TIME, Map.of( 932 "child", resourceId 933 ), Timestamp.class)); 934 } 935 936 private Instant fromTimestamp(final Timestamp timestamp) { 937 if (timestamp != null) { 938 return timestamp.toInstant(); 939 } 940 return null; 941 } 942 943 /** 944 * Format an instant to a timestamp without milliseconds, due to precision 945 * issues with memento datetimes. 946 * @param instant the instant to format. 947 * @return the datetime timestamp 948 */ 949 private Timestamp formatInstant(final Instant instant) { 950 if (instant == null) { 951 return null; 952 } 953 return Timestamp.from(instant.truncatedTo(ChronoUnit.SECONDS)); 954 } 955 956 /** 957 * Private class to back a stream with a paged DB query. 958 * 959 * If this needs to be run in parallel we will have to override trySplit() and determine a good method to split on. 960 */ 961 private class ContainmentIterator extends Spliterators.AbstractSpliterator<String> { 962 final Queue<String> children = new ConcurrentLinkedQueue<>(); 963 int numOffsets = 0; 964 final String queryToUse; 965 final MapSqlParameterSource parameterSource; 966 967 public ContainmentIterator(final String query, final MapSqlParameterSource parameters) { 968 super(Long.MAX_VALUE, Spliterator.ORDERED); 969 queryToUse = query; 970 parameterSource = parameters; 971 parameterSource.addValue("containsLimit", containsLimit); 972 } 973 974 @Override 975 public boolean tryAdvance(final Consumer<? super String> action) { 976 try { 977 action.accept(children.remove()); 978 } catch (final NoSuchElementException e) { 979 parameterSource.addValue("offSet", numOffsets * containsLimit); 980 numOffsets += 1; 981 children.addAll(jdbcTemplate.queryForList(queryToUse, parameterSource, String.class)); 982 if (children.size() == 0) { 983 // no more elements. 984 return false; 985 } 986 action.accept(children.remove()); 987 } 988 return true; 989 } 990 } 991}