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