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