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.services; 019 020import static org.slf4j.LoggerFactory.getLogger; 021 022import java.sql.ResultSet; 023import java.sql.SQLException; 024import java.sql.Timestamp; 025import java.time.Instant; 026import java.util.Map; 027import java.util.NoSuchElementException; 028import java.util.Queue; 029import java.util.Spliterator; 030import java.util.Spliterators; 031import java.util.concurrent.ConcurrentLinkedQueue; 032import java.util.function.Consumer; 033import java.util.stream.Stream; 034import java.util.stream.StreamSupport; 035 036import javax.annotation.PostConstruct; 037import javax.inject.Inject; 038import javax.sql.DataSource; 039import javax.transaction.Transactional; 040 041import org.fcrepo.common.db.DbPlatform; 042import org.fcrepo.kernel.api.identifiers.FedoraId; 043 044import org.apache.jena.graph.Node; 045import org.apache.jena.graph.NodeFactory; 046import org.apache.jena.graph.Triple; 047import org.slf4j.Logger; 048import org.springframework.jdbc.core.RowCallbackHandler; 049import org.springframework.jdbc.core.RowMapper; 050import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 051import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 052import org.springframework.stereotype.Component; 053 054/** 055 * Manager for the membership index 056 * 057 * @author bbpennel 058 */ 059@Component 060public class MembershipIndexManager { 061 private static final Logger log = getLogger(MembershipIndexManager.class); 062 063 private static final Timestamp NO_END_TIMESTAMP = Timestamp.from(MembershipServiceImpl.NO_END_INSTANT); 064 private static final Timestamp NO_START_TIMESTAMP = Timestamp.from(Instant.parse("1000-01-01T00:00:00.000Z")); 065 066 private static final String ADD_OPERATION = "add"; 067 private static final String DELETE_OPERATION = "delete"; 068 private static final String FORCE_FLAG = "force"; 069 070 private static final String TX_ID_PARAM = "txId"; 071 private static final String SUBJECT_ID_PARAM = "subjectId"; 072 private static final String NO_END_TIME_PARAM = "noEndTime"; 073 private static final String ADD_OP_PARAM = "addOp"; 074 private static final String DELETE_OP_PARAM = "deleteOp"; 075 private static final String MEMENTO_TIME_PARAM = "mementoTime"; 076 private static final String PROPERTY_PARAM = "property"; 077 private static final String TARGET_ID_PARAM = "targetId"; 078 private static final String SOURCE_ID_PARAM = "sourceId"; 079 private static final String PROXY_ID_PARAM = "proxyId"; 080 private static final String START_TIME_PARAM = "startTime"; 081 private static final String END_TIME_PARAM = "endTime"; 082 private static final String LAST_UPDATED_PARAM = "lastUpdated"; 083 private static final String OPERATION_PARAM = "operation"; 084 private static final String FORCE_PARAM = "forceFlag"; 085 private static final String LIMIT_PARAM = "limit"; 086 private static final String OFFSET_PARAM = "offSet"; 087 088 private static final String SELECT_ALL_MEMBERSHIP = "SELECT * FROM membership"; 089 090 private static final String SELECT_ALL_OPERATIONS = "SELECT * FROM membership_tx_operations"; 091 092 private static final String SELECT_MEMBERSHIP_IN_TX = 093 "SELECT property, object_id" + 094 " FROM membership m" + 095 " WHERE subject_id = :subjectId" + 096 " AND end_time = :noEndTime" + 097 " AND NOT EXISTS (" + 098 " SELECT 1" + 099 " FROM membership_tx_operations mto" + 100 " WHERE mto.subject_id = :subjectId" + 101 " AND mto.source_id = m.source_id" + 102 " AND mto.object_id = m.object_id" + 103 " AND mto.tx_id = :txId" + 104 " AND mto.operation = :deleteOp)" + 105 " UNION" + 106 " SELECT property, object_id" + 107 " FROM membership_tx_operations" + 108 " WHERE subject_id = :subjectId" + 109 " AND tx_id = :txId" + 110 " AND end_time = :noEndTime" + 111 " AND operation = :addOp" + 112 " ORDER BY property, object_id" + 113 " LIMIT :limit OFFSET :offSet"; 114 115 private static final String SELECT_MEMBERSHIP_MEMENTO_IN_TX = 116 "SELECT property, object_id" + 117 " FROM membership m" + 118 " WHERE m.subject_id = :subjectId" + 119 " AND m.start_time <= :mementoTime" + 120 " AND m.end_time > :mementoTime" + 121 " AND NOT EXISTS (" + 122 " SELECT 1" + 123 " FROM membership_tx_operations mto" + 124 " WHERE mto.subject_id = :subjectId" + 125 " AND mto.source_id = m.source_id" + 126 " AND mto.property = m.property" + 127 " AND mto.object_id = m.object_id" + 128 " AND mto.end_time <= :mementoTime" + 129 " AND mto.tx_id = :txId" + 130 " AND mto.operation = :deleteOp)" + 131 " UNION" + 132 " SELECT property, object_id" + 133 " FROM membership_tx_operations" + 134 " WHERE subject_id = :subjectId" + 135 " AND tx_id = :txId" + 136 " AND start_time <= :mementoTime" + 137 " AND end_time > :mementoTime" + 138 " AND operation = :addOp" + 139 " ORDER BY property, object_id" + 140 " LIMIT :limit OFFSET :offSet"; 141 142 private static final String SELECT_LAST_UPDATED = 143 "SELECT max(last_updated) as last_updated" + 144 " FROM membership" + 145 " WHERE subject_id = :subjectId"; 146 147 // For mementos, use the start_time instead of last_updated as the 148 // end_time reflects when the next version starts 149 private static final String SELECT_LAST_UPDATED_MEMENTO = 150 "SELECT max(start_time)" + 151 " FROM membership" + 152 " WHERE subject_id = :subjectId" + 153 " AND start_time <= :mementoTime" + 154 " AND end_time > :mementoTime"; 155 156 private static final String SELECT_LAST_UPDATED_IN_TX = 157 "SELECT max(combined.updated) as last_updated" + 158 " FROM (" + 159 " SELECT max(last_updated) as updated" + 160 " FROM membership m" + 161 " WHERE subject_id = :subjectId" + 162 " AND NOT EXISTS (" + 163 " SELECT 1" + 164 " FROM membership_tx_operations mto" + 165 " WHERE mto.subject_id = :subjectId" + 166 " AND mto.source_id = m.source_id" + 167 " AND mto.object_id = m.object_id" + 168 " AND mto.tx_id = :txId" + 169 " AND mto.operation = :deleteOp)" + 170 " UNION" + 171 " SELECT max(last_updated) as updated" + 172 " FROM membership_tx_operations" + 173 " WHERE subject_id = :subjectId" + 174 " AND tx_id = :txId" + 175 ") combined"; 176 177 private static final String INSERT_MEMBERSHIP_IN_TX = 178 "INSERT INTO membership_tx_operations (subject_id, property, object_id, source_id," + 179 " proxy_id, start_time, end_time, last_updated, tx_id, operation)" + 180 " VALUES (:subjectId, :property, :targetId, :sourceId," + 181 " :proxyId, :startTime, :endTime, :lastUpdated, :txId, :operation)"; 182 183 private static final String END_EXISTING_MEMBERSHIP = 184 "INSERT INTO membership_tx_operations (subject_id, property, object_id, source_id," + 185 " proxy_id, start_time, end_time, last_updated, tx_id, operation)" + 186 " SELECT m.subject_id, m.property, m.object_id, m.source_id, m.proxy_id, m.start_time," + 187 " :endTime, :endTime, :txId, :deleteOp" + 188 " FROM membership m" + 189 " WHERE m.source_id = :sourceId" + 190 " AND m.proxy_id = :proxyId" + 191 " AND m.end_time = :noEndTime"; 192 193 private static final String CLEAR_FOR_PROXY_IN_TX = 194 "DELETE FROM membership_tx_operations" + 195 " WHERE source_id = :sourceId" + 196 " AND tx_id = :txId" + 197 " AND proxy_id = :proxyId" + 198 " AND force_flag IS NULL"; 199 200 private static final String CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX = 201 "DELETE FROM membership_tx_operations" + 202 " WHERE source_id = :sourceId" + 203 " AND tx_id = :txId" + 204 " AND operation = :addOp"; 205 206 // Add "delete" entries for all existing membership from the given source, if not already deleted 207 private static final String END_EXISTING_FOR_SOURCE = 208 "INSERT INTO membership_tx_operations (subject_id, property, object_id, source_id," + 209 " proxy_id, start_time, end_time, last_updated, tx_id, operation)" + 210 " SELECT subject_id, property, object_id, source_id," + 211 " proxy_id, start_time, :endTime, :endTime, :txId, :deleteOp" + 212 " FROM membership m" + 213 " WHERE source_id = :sourceId" + 214 " AND end_time = :noEndTime" + 215 " AND NOT EXISTS (" + 216 " SELECT TRUE" + 217 " FROM membership_tx_operations mtx" + 218 " WHERE mtx.subject_id = m.subject_id" + 219 " AND mtx.property = m.property" + 220 " AND mtx.object_id = m.object_id" + 221 " AND mtx.source_id = m.source_id" + 222 " AND mtx.proxy_id = m.proxy_id" + 223 " AND mtx.operation = :deleteOp" + 224 ")"; 225 226 private static final String DELETE_EXISTING_FOR_SOURCE_AFTER = 227 "INSERT INTO membership_tx_operations(subject_id, property, object_id, source_id," + 228 " proxy_id, start_time, end_time, last_updated, tx_id, operation, force_flag)" + 229 " SELECT subject_id, property, object_id, source_id, proxy_id, start_time, end_time," + 230 " last_updated, :txId, :deleteOp, :forceFlag" + 231 " FROM membership m" + 232 " WHERE m.source_id = :sourceId" + 233 " AND (m.start_time >= :startTime" + 234 " OR m.end_time >= :startTime)"; 235 236 private static final String DELETE_EXISTING_FOR_PROXY_AFTER = 237 "INSERT INTO membership_tx_operations(subject_id, property, object_id, source_id," + 238 " proxy_id, start_time, end_time, last_updated, tx_id, operation, force_flag)" + 239 " SELECT subject_id, property, object_id, source_id, proxy_id, start_time, end_time," + 240 " last_updated, :txId, :deleteOp, :forceFlag" + 241 " FROM membership m" + 242 " WHERE m.proxy_id = :proxyId" + 243 " AND (m.start_time >= :startTime" + 244 " OR m.end_time >= :startTime)"; 245 246 private static final String PURGE_ALL_REFERENCES_MEMBERSHIP = 247 "DELETE from membership" + 248 " where source_id = :targetId" + 249 " OR subject_id = :targetId" + 250 " OR object_id = :targetId"; 251 252 private static final String PURGE_ALL_REFERENCES_TRANSACTION = 253 "DELETE from membership_tx_operations" + 254 " WHERE tx_id = :txId" + 255 " AND (source_id = :targetId" + 256 " OR subject_id = :targetId" + 257 " OR object_id = :targetId)"; 258 259 private static final String COMMIT_DELETES = 260 "DELETE from membership" + 261 " WHERE EXISTS (" + 262 " SELECT TRUE" + 263 " FROM membership_tx_operations mto" + 264 " WHERE mto.tx_id = :txId" + 265 " AND mto.operation = :deleteOp" + 266 " AND mto.force_flag = :forceFlag" + 267 " AND membership.source_id = mto.source_id" + 268 " AND membership.proxy_id = mto.proxy_id" + 269 " AND membership.subject_id = mto.subject_id" + 270 " AND membership.property = mto.property" + 271 " AND membership.object_id = mto.object_id" + 272 " )"; 273 274 private static final String COMMIT_ENDS_H2 = 275 "UPDATE membership m" + 276 " SET end_time = (" + 277 " SELECT mto.end_time" + 278 " FROM membership_tx_operations mto" + 279 " WHERE mto.tx_id = :txId" + 280 " AND m.source_id = mto.source_id" + 281 " AND m.proxy_id = mto.proxy_id" + 282 " AND m.subject_id = mto.subject_id" + 283 " AND m.property = mto.property" + 284 " AND m.object_id = mto.object_id" + 285 " AND mto.operation = :deleteOp" + 286 " )," + 287 " last_updated = (" + 288 " SELECT mto.end_time" + 289 " FROM membership_tx_operations mto" + 290 " WHERE mto.tx_id = :txId" + 291 " AND m.source_id = mto.source_id" + 292 " AND m.proxy_id = mto.proxy_id" + 293 " AND m.subject_id = mto.subject_id" + 294 " AND m.property = mto.property" + 295 " AND m.object_id = mto.object_id" + 296 " AND mto.operation = :deleteOp" + 297 " )" + 298 " WHERE EXISTS (" + 299 "SELECT TRUE" + 300 " FROM membership_tx_operations mto" + 301 " WHERE mto.tx_id = :txId" + 302 " AND mto.operation = :deleteOp" + 303 " AND m.source_id = mto.source_id" + 304 " AND m.proxy_id = mto.proxy_id" + 305 " AND m.subject_id = mto.subject_id" + 306 " AND m.property = mto.property" + 307 " AND m.object_id = mto.object_id" + 308 " )"; 309 310 private static final String COMMIT_ENDS_POSTGRES = 311 "UPDATE membership" + 312 " SET end_time = mto.end_time, last_updated = mto.end_time" + 313 " FROM membership_tx_operations mto" + 314 " WHERE mto.tx_id = :txId" + 315 " AND mto.operation = :deleteOp" + 316 " AND membership.source_id = mto.source_id" + 317 " AND membership.proxy_id = mto.proxy_id" + 318 " AND membership.subject_id = mto.subject_id" + 319 " AND membership.property = mto.property" + 320 " AND membership.object_id = mto.object_id"; 321 322 private static final String COMMIT_ENDS_MYSQL = 323 "UPDATE membership m" + 324 " INNER JOIN membership_tx_operations mto ON" + 325 " m.source_id = mto.source_id" + 326 " AND m.proxy_id = mto.proxy_id" + 327 " AND m.subject_id = mto.subject_id" + 328 " AND m.property = mto.property" + 329 " AND m.object_id = mto.object_id" + 330 " SET m.end_time = mto.end_time, m.last_updated = mto.end_time" + 331 " WHERE mto.tx_id = :txId" + 332 " AND mto.operation = :deleteOp"; 333 334 private static final Map<DbPlatform, String> COMMIT_ENDS_MAP = Map.of( 335 DbPlatform.MYSQL, COMMIT_ENDS_MYSQL, 336 DbPlatform.MARIADB, COMMIT_ENDS_MYSQL, 337 DbPlatform.POSTGRESQL, COMMIT_ENDS_POSTGRES, 338 DbPlatform.H2, COMMIT_ENDS_H2 339 ); 340 341 // Transfer all "add" operations from tx to committed membership, unless the entry already exists 342 private static final String COMMIT_ADDS = 343 "INSERT INTO membership" + 344 " (subject_id, property, object_id, source_id, proxy_id, start_time, end_time, last_updated)" + 345 " SELECT subject_id, property, object_id, source_id, proxy_id, start_time, end_time, last_updated" + 346 " FROM membership_tx_operations mto" + 347 " WHERE mto.tx_id = :txId" + 348 " AND mto.operation = :addOp" + 349 " AND NOT EXISTS (" + 350 " SELECT TRUE" + 351 " FROM membership m" + 352 " WHERE m.source_id = mto.source_id" + 353 " AND m.proxy_id = mto.proxy_id" + 354 " AND m.subject_id = mto.subject_id" + 355 " AND m.property = mto.property" + 356 " AND m.object_id = mto.object_id" + 357 " AND m.start_time = mto.start_time" + 358 " AND m.end_time = mto.end_time" + 359 " )"; 360 361 private static final String DELETE_TRANSACTION = 362 "DELETE FROM membership_tx_operations" + 363 " WHERE tx_id = :txId"; 364 365 private static final String TRUNCATE_MEMBERSHIP = "TRUNCATE TABLE membership"; 366 367 private static final String TRUNCATE_MEMBERSHIP_TX = "TRUNCATE TABLE membership_tx_operations"; 368 369 @Inject 370 private DataSource dataSource; 371 372 private NamedParameterJdbcTemplate jdbcTemplate; 373 374 private DbPlatform dbPlatform; 375 376 private static final int MEMBERSHIP_LIMIT = 50000; 377 378 @PostConstruct 379 public void setUp() { 380 jdbcTemplate = new NamedParameterJdbcTemplate(getDataSource()); 381 dbPlatform = DbPlatform.fromDataSource(dataSource); 382 } 383 384 /** 385 * End a membership from the child of a Direct/IndirectContainer, setting an end time if committed, 386 * or clearing from the current tx if it was newly added. 387 * 388 * @param txId transaction id 389 * @param sourceId ID of the direct/indirect container whose membership should be ended 390 * @param proxyId ID of the proxy producing this membership, when applicable 391 * @param endTime the time the resource was deleted, generally its last modified 392 */ 393 @Transactional 394 public void endMembershipFromChild(final String txId, final FedoraId sourceId, final FedoraId proxyId, 395 final Instant endTime) { 396 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 397 parameterSource.addValue(TX_ID_PARAM, txId); 398 parameterSource.addValue(SOURCE_ID_PARAM, sourceId.getFullId()); 399 parameterSource.addValue(PROXY_ID_PARAM, proxyId.getFullId()); 400 401 final int affected = jdbcTemplate.update(CLEAR_FOR_PROXY_IN_TX, parameterSource); 402 403 // If no rows were deleted, then assume we need to delete permanent entry 404 if (affected == 0) { 405 final MapSqlParameterSource parameterSource2 = new MapSqlParameterSource(); 406 parameterSource2.addValue(TX_ID_PARAM, txId); 407 parameterSource2.addValue(SOURCE_ID_PARAM, sourceId.getFullId()); 408 parameterSource2.addValue(PROXY_ID_PARAM, proxyId.getFullId()); 409 parameterSource2.addValue(END_TIME_PARAM, formatInstant(endTime)); 410 parameterSource2.addValue(NO_END_TIME_PARAM, NO_END_TIMESTAMP); 411 parameterSource2.addValue(DELETE_OP_PARAM, DELETE_OPERATION); 412 jdbcTemplate.update(END_EXISTING_MEMBERSHIP, parameterSource2); 413 } 414 } 415 416 @Transactional 417 public void deleteMembershipForProxyAfter(final String txId, final FedoraId sourceId, final FedoraId proxyId, 418 final Instant afterTime) { 419 // Clear all membership added in this transaction 420 final var parameterSource = Map.of( 421 TX_ID_PARAM, txId, 422 SOURCE_ID_PARAM, sourceId.getFullId(), 423 PROXY_ID_PARAM, proxyId.getFullId(), 424 OPERATION_PARAM, ADD_OPERATION); 425 426 jdbcTemplate.update(CLEAR_FOR_PROXY_IN_TX, parameterSource); 427 428 final var afterTimestamp = afterTime == null ? NO_START_TIMESTAMP : formatInstant(afterTime); 429 430 // Delete all existing membership entries that start after or end after the given timestamp 431 final Map<String, Object> parameterSource2 = Map.of( 432 TX_ID_PARAM, txId, 433 PROXY_ID_PARAM, proxyId.getFullId(), 434 START_TIME_PARAM, afterTimestamp, 435 FORCE_PARAM, FORCE_FLAG, 436 DELETE_OP_PARAM, DELETE_OPERATION); 437 jdbcTemplate.update(DELETE_EXISTING_FOR_PROXY_AFTER, parameterSource2); 438 } 439 440 /** 441 * End all membership properties resulting from the specified source container 442 * @param txId transaction id 443 * @param sourceId ID of the direct/indirect container whose membership should be ended 444 * @param endTime the time the resource was deleted, generally its last modified 445 */ 446 @Transactional 447 public void endMembershipForSource(final String txId, final FedoraId sourceId, final Instant endTime) { 448 final Map<String, Object> parameterSource = Map.of( 449 TX_ID_PARAM, txId, 450 SOURCE_ID_PARAM, sourceId.getFullId(), 451 ADD_OP_PARAM, ADD_OPERATION); 452 453 jdbcTemplate.update(CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX, parameterSource); 454 455 final Map<String, Object> parameterSource2 = Map.of( 456 TX_ID_PARAM, txId, 457 SOURCE_ID_PARAM, sourceId.getFullId(), 458 END_TIME_PARAM, formatInstant(endTime), 459 NO_END_TIME_PARAM, NO_END_TIMESTAMP, 460 DELETE_OP_PARAM, DELETE_OPERATION); 461 jdbcTemplate.update(END_EXISTING_FOR_SOURCE, parameterSource2); 462 } 463 464 /** 465 * Delete membership entries that are active at or after the given timestamp for the specified source 466 * @param txId transaction id 467 * @param sourceId ID of the direct/indirect container 468 * @param afterTime time at or after which membership should be removed 469 */ 470 @Transactional 471 public void deleteMembershipForSourceAfter(final String txId, final FedoraId sourceId, final Instant afterTime) { 472 // Clear all membership added in this transaction 473 final Map<String, Object> parameterSource = Map.of( 474 TX_ID_PARAM, txId, 475 SOURCE_ID_PARAM, sourceId.getFullId(), 476 ADD_OP_PARAM, ADD_OPERATION); 477 478 jdbcTemplate.update(CLEAR_ALL_ADDED_FOR_SOURCE_IN_TX, parameterSource); 479 480 final var afterTimestamp = afterTime == null ? NO_START_TIMESTAMP : formatInstant(afterTime); 481 482 // Delete all existing membership entries that start after or end after the given timestamp 483 final Map<String, Object> parameterSource2 = Map.of( 484 TX_ID_PARAM, txId, 485 SOURCE_ID_PARAM, sourceId.getFullId(), 486 START_TIME_PARAM, afterTimestamp, 487 FORCE_PARAM, FORCE_FLAG, 488 DELETE_OP_PARAM, DELETE_OPERATION); 489 jdbcTemplate.update(DELETE_EXISTING_FOR_SOURCE_AFTER, parameterSource2); 490 } 491 492 /** 493 * Clean up any references to the target id, in transactions and outside 494 * @param txId transaction id 495 * @param targetId identifier of the resource to cleanup membership references for 496 */ 497 @Transactional 498 public void deleteMembershipReferences(final String txId, final FedoraId targetId) { 499 final Map<String, Object> parameterSource = Map.of( 500 TARGET_ID_PARAM, targetId.getFullId(), 501 TX_ID_PARAM, txId); 502 503 jdbcTemplate.update(PURGE_ALL_REFERENCES_TRANSACTION, parameterSource); 504 jdbcTemplate.update(PURGE_ALL_REFERENCES_MEMBERSHIP, parameterSource); 505 } 506 507 /** 508 * Add new membership property to the index, clearing any delete 509 * operations for the property if necessary. 510 * @param txId transaction id 511 * @param sourceId ID of the direct/indirect container which produced the membership 512 * @param proxyId ID of the proxy producing this membership, when applicable 513 * @param membership membership triple 514 * @param startTime time the membership triple was added 515 */ 516 @Transactional 517 public void addMembership(final String txId, final FedoraId sourceId, final FedoraId proxyId, 518 final Triple membership, final Instant startTime) { 519 if (membership == null) { 520 return; 521 } 522 addMembership(txId, sourceId, proxyId, membership, startTime, null); 523 } 524 525 /** 526 * Add new membership property to the index 527 * @param txId transaction id 528 * @param sourceId ID of the direct/indirect container which produced the membership 529 * @param proxyId ID of the proxy producing this membership, when applicable 530 * @param membership membership triple 531 * @param startTime time the membership triple was added 532 * @param endTime time the membership triple ends, or never if not provided 533 */ 534 @Transactional 535 public void addMembership(final String txId, final FedoraId sourceId, final FedoraId proxyId, 536 final Triple membership, final Instant startTime, final Instant endTime) { 537 final Timestamp endTimestamp; 538 final Timestamp lastUpdated; 539 final Timestamp startTimestamp = formatInstant(startTime); 540 if (endTime == null) { 541 endTimestamp = NO_END_TIMESTAMP; 542 lastUpdated = startTimestamp; 543 } else { 544 endTimestamp = formatInstant(endTime); 545 lastUpdated = endTimestamp; 546 } 547 // Add the new membership operation 548 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 549 parameterSource.addValue(SUBJECT_ID_PARAM, membership.getSubject().getURI()); 550 parameterSource.addValue(PROPERTY_PARAM, membership.getPredicate().getURI()); 551 parameterSource.addValue(TARGET_ID_PARAM, membership.getObject().getURI()); 552 parameterSource.addValue(SOURCE_ID_PARAM, sourceId.getFullId()); 553 parameterSource.addValue(PROXY_ID_PARAM, proxyId.getFullId()); 554 parameterSource.addValue(START_TIME_PARAM, startTimestamp); 555 parameterSource.addValue(END_TIME_PARAM, endTimestamp); 556 parameterSource.addValue(LAST_UPDATED_PARAM, lastUpdated); 557 parameterSource.addValue(TX_ID_PARAM, txId); 558 parameterSource.addValue(OPERATION_PARAM, ADD_OPERATION); 559 560 jdbcTemplate.update(INSERT_MEMBERSHIP_IN_TX, parameterSource); 561 } 562 563 /** 564 * Get a stream of membership triples with 565 * @param txId transaction from which membership will be retrieved, or null for no transaction 566 * @param subjectId ID of the subject 567 * @return Stream of membership triples 568 */ 569 public Stream<Triple> getMembership(final String txId, final FedoraId subjectId) { 570 final Node subjectNode = NodeFactory.createURI(subjectId.getBaseId()); 571 572 final RowMapper<Triple> membershipMapper = (rs, rowNum) -> 573 Triple.create(subjectNode, 574 NodeFactory.createURI(rs.getString("property")), 575 NodeFactory.createURI(rs.getString("object_id"))); 576 577 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 578 parameterSource.addValue(TX_ID_PARAM, txId); 579 580 final String query; 581 if (subjectId.isMemento()) { 582 parameterSource.addValue(SUBJECT_ID_PARAM, subjectId.getBaseId()); 583 parameterSource.addValue(MEMENTO_TIME_PARAM, formatInstant(subjectId.getMementoInstant())); 584 query = SELECT_MEMBERSHIP_MEMENTO_IN_TX; 585 } else { 586 parameterSource.addValue(SUBJECT_ID_PARAM, subjectId.getFullId()); 587 parameterSource.addValue(NO_END_TIME_PARAM, NO_END_TIMESTAMP); 588 query = SELECT_MEMBERSHIP_IN_TX; 589 } 590 591 return StreamSupport.stream(new MembershipIterator(query, parameterSource, membershipMapper), false); 592 } 593 594 public Instant getLastUpdated(final String txId, final FedoraId subjectId) { 595 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 596 597 parameterSource.addValue(NO_END_TIME_PARAM, NO_END_TIMESTAMP); 598 final String lastUpdatedQuery; 599 if (subjectId.isMemento()) { 600 lastUpdatedQuery = SELECT_LAST_UPDATED_MEMENTO; 601 parameterSource.addValue(SUBJECT_ID_PARAM, subjectId.getBaseId()); 602 parameterSource.addValue(MEMENTO_TIME_PARAM, formatInstant(subjectId.getMementoInstant())); 603 } else if (txId == null) { 604 lastUpdatedQuery = SELECT_LAST_UPDATED; 605 parameterSource.addValue(SUBJECT_ID_PARAM, subjectId.getFullId()); 606 } else { 607 lastUpdatedQuery = SELECT_LAST_UPDATED_IN_TX; 608 parameterSource.addValue(SUBJECT_ID_PARAM, subjectId.getFullId()); 609 parameterSource.addValue(TX_ID_PARAM, txId); 610 parameterSource.addValue(DELETE_OP_PARAM, DELETE_OPERATION); 611 } 612 613 return jdbcTemplate.queryForObject(lastUpdatedQuery, parameterSource, Instant.class); 614 } 615 616 /** 617 * Perform a commit of operations stored in the specified transaction 618 * @param txId transaction id 619 */ 620 @Transactional 621 public void commitTransaction(final String txId) { 622 final Map<String, String> parameterSource = Map.of(TX_ID_PARAM, txId, 623 ADD_OP_PARAM, ADD_OPERATION, 624 DELETE_OP_PARAM, DELETE_OPERATION, 625 FORCE_PARAM, FORCE_FLAG); 626 627 jdbcTemplate.update(COMMIT_DELETES, parameterSource); 628 final int ends = jdbcTemplate.update(COMMIT_ENDS_MAP.get(this.dbPlatform), parameterSource); 629 final int adds = jdbcTemplate.update(COMMIT_ADDS, parameterSource); 630 final int cleaned = jdbcTemplate.update(DELETE_TRANSACTION, parameterSource); 631 632 log.debug("Completed commit, {} ended, {} adds, {} operations", ends, adds, cleaned); 633 } 634 635 /** 636 * Delete all entries related to a transaction 637 * @param txId transaction id 638 */ 639 public void deleteTransaction(final String txId) { 640 final Map<String, String> parameterSource = Map.of(TX_ID_PARAM, txId); 641 jdbcTemplate.update(DELETE_TRANSACTION, parameterSource); 642 } 643 644 /** 645 * Format an instant to a timestamp without milliseconds, due to precision 646 * issues with memento datetimes. 647 * @param instant 648 * @return 649 */ 650 private Timestamp formatInstant(final Instant instant) { 651 final var timestamp = Timestamp.from(instant); 652 timestamp.setNanos(0); 653 return timestamp; 654 } 655 656 /** 657 * Clear all entries from the index 658 */ 659 @Transactional 660 public void clearIndex() { 661 jdbcTemplate.update(TRUNCATE_MEMBERSHIP, Map.of()); 662 jdbcTemplate.update(TRUNCATE_MEMBERSHIP_TX, Map.of()); 663 } 664 665 /** 666 * Log all membership entries, for debugging usage only 667 */ 668 public void logMembership() { 669 log.info("source_id, proxy_id, subject_id, property, object_id, start_time, end_time, last_updated"); 670 jdbcTemplate.query(SELECT_ALL_MEMBERSHIP, new RowCallbackHandler() { 671 @Override 672 public void processRow(final ResultSet rs) throws SQLException { 673 log.info("{}, {}, {}, {}, {}, {}, {}, {}", 674 rs.getString("source_id"), rs.getString("proxy_id"), rs.getString("subject_id"), 675 rs.getString("property"), rs.getString("object_id"), rs.getTimestamp("start_time"), 676 rs.getTimestamp("end_time"), rs.getTimestamp("last_updated")); 677 } 678 }); 679 } 680 681 /** 682 * Log all membership operations, for debugging usage only 683 */ 684 public void logOperations() { 685 log.info("source_id, proxy_id, subject_id, property, object_id, start_time, end_time," 686 + " last_updated, tx_id, operation, force_flag"); 687 jdbcTemplate.query(SELECT_ALL_OPERATIONS, new RowCallbackHandler() { 688 @Override 689 public void processRow(final ResultSet rs) throws SQLException { 690 log.info("{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}", 691 rs.getString("source_id"), rs.getString("proxy_id"), rs.getString("subject_id"), 692 rs.getString("property"), rs.getString("object_id"), rs.getTimestamp("start_time"), 693 rs.getTimestamp("end_time"), rs.getTimestamp("last_updated"), rs.getString("tx_id"), 694 rs.getString("operation"), rs.getString("force_flag")); 695 } 696 }); 697 } 698 699 /** 700 * Set the JDBC datastore. 701 * @param dataSource the dataStore. 702 */ 703 public void setDataSource(final DataSource dataSource) { 704 this.dataSource = dataSource; 705 } 706 707 /** 708 * Get the JDBC datastore. 709 * @return the dataStore. 710 */ 711 public DataSource getDataSource() { 712 return dataSource; 713 } 714 715 /** 716 * Private class to back a stream with a paged DB query. 717 * 718 * If this needs to be run in parallel we will have to override trySplit() and determine a good method to split on. 719 */ 720 private class MembershipIterator extends Spliterators.AbstractSpliterator<Triple> { 721 final Queue<Triple> children = new ConcurrentLinkedQueue<>(); 722 int numOffsets = 0; 723 final String queryToUse; 724 final MapSqlParameterSource parameterSource; 725 final RowMapper<Triple> rowMapper; 726 727 public MembershipIterator(final String query, final MapSqlParameterSource parameters, 728 final RowMapper<Triple> mapper) { 729 super(Long.MAX_VALUE, Spliterator.ORDERED); 730 queryToUse = query; 731 parameterSource = parameters; 732 rowMapper = mapper; 733 parameterSource.addValue(ADD_OP_PARAM, ADD_OPERATION); 734 parameterSource.addValue(DELETE_OP_PARAM, DELETE_OPERATION); 735 parameterSource.addValue(LIMIT_PARAM, MEMBERSHIP_LIMIT); 736 } 737 738 @Override 739 public boolean tryAdvance(final Consumer<? super Triple> action) { 740 try { 741 action.accept(children.remove()); 742 } catch (final NoSuchElementException e) { 743 parameterSource.addValue(OFFSET_PARAM, numOffsets * MEMBERSHIP_LIMIT); 744 numOffsets += 1; 745 children.addAll(jdbcTemplate.query(queryToUse, parameterSource, rowMapper)); 746 if (children.size() == 0) { 747 // no more elements. 748 return false; 749 } 750 action.accept(children.remove()); 751 } 752 return true; 753 } 754 } 755}