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.services; 007 008import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX; 009import static org.slf4j.LoggerFactory.getLogger; 010 011import java.util.List; 012import java.util.Map; 013import java.util.function.Predicate; 014import java.util.stream.Collectors; 015import java.util.stream.Stream; 016 017import javax.annotation.Nonnull; 018import javax.annotation.PostConstruct; 019import javax.inject.Inject; 020import javax.sql.DataSource; 021 022import org.fcrepo.kernel.api.ContainmentIndex; 023import org.fcrepo.kernel.api.RdfStream; 024import org.fcrepo.kernel.api.Transaction; 025import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 026import org.fcrepo.kernel.api.identifiers.FedoraId; 027import org.fcrepo.kernel.api.models.FedoraResource; 028import org.fcrepo.kernel.api.models.NonRdfSourceDescription; 029import org.fcrepo.kernel.api.observer.EventAccumulator; 030import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 031import org.fcrepo.kernel.api.services.ReferenceService; 032import org.fcrepo.kernel.impl.operations.ReferenceOperation; 033import org.fcrepo.kernel.impl.operations.ReferenceOperationBuilder; 034 035import org.apache.jena.graph.Node; 036import org.apache.jena.graph.NodeFactory; 037import org.apache.jena.graph.Triple; 038import org.apache.jena.sparql.core.Quad; 039import org.slf4j.Logger; 040import org.springframework.beans.factory.annotation.Autowired; 041import org.springframework.beans.factory.annotation.Qualifier; 042import org.springframework.jdbc.core.RowMapper; 043import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; 044import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; 045import org.springframework.stereotype.Component; 046import org.springframework.transaction.annotation.Propagation; 047import org.springframework.transaction.annotation.Transactional; 048 049/** 050 * Implementation of reference service. 051 * @author whikloj 052 * @since 6.0.0 053 */ 054@Component("referenceServiceImpl") 055public class ReferenceServiceImpl implements ReferenceService { 056 057 private static final Logger LOGGER = getLogger(ReferenceServiceImpl.class); 058 059 @Inject 060 private DataSource dataSource; 061 062 @Inject 063 private EventAccumulator eventAccumulator; 064 065 @Autowired 066 @Qualifier("containmentIndex") 067 private ContainmentIndex containmentIndex; 068 069 private NamedParameterJdbcTemplate jdbcTemplate; 070 071 private static final String TABLE_NAME = "reference"; 072 073 private static final String TRANSACTION_TABLE = "reference_transaction_operations"; 074 075 private static final String RESOURCE_COLUMN = "fedora_id"; 076 077 private static final String SUBJECT_COLUMN = "subject_id"; 078 079 private static final String PROPERTY_COLUMN = "property"; 080 081 private static final String TARGET_COLUMN = "target_id"; 082 083 private static final String OPERATION_COLUMN = "operation"; 084 085 private static final String TRANSACTION_COLUMN = "transaction_id"; 086 087 private static final String SELECT_INBOUND = "SELECT " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + " FROM " + 088 TABLE_NAME + " WHERE " + TARGET_COLUMN + " = :targetId"; 089 090 private static final String SELECT_INBOUND_IN_TRANSACTION = "SELECT x." + SUBJECT_COLUMN + ", x." + 091 PROPERTY_COLUMN + " FROM " + "(SELECT " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + " FROM " + TABLE_NAME + 092 " WHERE " + TARGET_COLUMN + " = :targetId UNION " + "SELECT " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + 093 " FROM " + TRANSACTION_TABLE + " WHERE " + TARGET_COLUMN + " = :targetId AND " 094 + TRANSACTION_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS " + 095 "(SELECT 1 FROM " + TRANSACTION_TABLE + " WHERE " + TARGET_COLUMN + " = :targetId AND " + 096 OPERATION_COLUMN + " = 'delete')"; 097 098 private static final String SELECT_OUTBOUND = "SELECT " + SUBJECT_COLUMN + ", " + TARGET_COLUMN + ", " + 099 PROPERTY_COLUMN + " FROM " + TABLE_NAME + " WHERE " + RESOURCE_COLUMN + " = :resourceId"; 100 101 private static final String SELECT_OUTBOUND_IN_TRANSACTION = "SELECT x." + SUBJECT_COLUMN + ", x." + TARGET_COLUMN + 102 ", x." + PROPERTY_COLUMN + " FROM " + "(SELECT " + SUBJECT_COLUMN + ", " + TARGET_COLUMN + ", " + 103 PROPERTY_COLUMN + " FROM " + TABLE_NAME + " WHERE " + RESOURCE_COLUMN + " = :resourceId UNION " + 104 "SELECT " + SUBJECT_COLUMN + ", " + TARGET_COLUMN + ", " + PROPERTY_COLUMN + " FROM " + TRANSACTION_TABLE + 105 " WHERE " + RESOURCE_COLUMN + " = :resourceId " + "AND " + TRANSACTION_COLUMN + " = :transactionId AND " + 106 OPERATION_COLUMN + " = 'add') x WHERE NOT EXISTS (SELECT 1 FROM " + TRANSACTION_TABLE + " WHERE " + 107 RESOURCE_COLUMN + " = :resourceId AND " + OPERATION_COLUMN + " = 'delete')"; 108 109 private static final String INSERT_REFERENCE_IN_TRANSACTION = "INSERT INTO " + TRANSACTION_TABLE + "(" + 110 RESOURCE_COLUMN + ", " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + ", " + TARGET_COLUMN + ", " + 111 TRANSACTION_COLUMN + ", " + OPERATION_COLUMN + ") VALUES (:resourceId, :subjectId, :property, :targetId, " + 112 ":transactionId, 'add')"; 113 114 private static final String INSERT_REFERENCE_DIRECT = "INSERT INTO " + TABLE_NAME + "(" + 115 RESOURCE_COLUMN + ", " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + ", " + TARGET_COLUMN + 116 ") VALUES (:resourceId, :subjectId, :property, :targetId)"; 117 118 private static final String UNDO_INSERT_REFERENCE_IN_TRANSACTION = "DELETE FROM " + TRANSACTION_TABLE + " WHERE " + 119 RESOURCE_COLUMN + " = :resourceId AND " + SUBJECT_COLUMN + " = :subjectId AND " + PROPERTY_COLUMN + 120 " = :property AND " + TARGET_COLUMN + " = :targetId AND " + TRANSACTION_COLUMN + " = :transactionId AND " + 121 OPERATION_COLUMN + " = 'add'"; 122 123 private static final String DELETE_REFERENCE_IN_TRANSACTION = "INSERT INTO " + TRANSACTION_TABLE + "(" + 124 RESOURCE_COLUMN + ", " + SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + ", " + TARGET_COLUMN + ", " + 125 TRANSACTION_COLUMN + ", " + OPERATION_COLUMN + ") VALUES (:resourceId, :subjectId, :property, :targetId, " + 126 ":transactionId, 'delete')"; 127 128 private static final String DELETE_REFERENCE_DIRECT = "DELETE FROM reference" + 129 " WHERE fedora_id = :resourceId AND subject_id = :subjectId" + 130 " AND property = :property AND target_id = :targetId"; 131 132 private static final String UNDO_DELETE_REFERENCE_IN_TRANSACTION = "DELETE FROM " + TRANSACTION_TABLE + " WHERE " + 133 RESOURCE_COLUMN + " = :resourceId AND " + SUBJECT_COLUMN + " = :subjectId AND " + PROPERTY_COLUMN + 134 " = :property AND " + TARGET_COLUMN + " = :targetId AND " + TRANSACTION_COLUMN + " = :transactionId AND " + 135 OPERATION_COLUMN + " = 'delete'"; 136 137 private static final String IS_REFERENCE_ADDED_IN_TRANSACTION = "SELECT TRUE FROM " + TRANSACTION_TABLE + " WHERE " 138 + RESOURCE_COLUMN + " = :resourceId AND " + SUBJECT_COLUMN + " = :subjectId AND " + PROPERTY_COLUMN + 139 " = :property AND " + TARGET_COLUMN + " = :targetId AND " + TRANSACTION_COLUMN + " = :transactionId AND " + 140 OPERATION_COLUMN + " = 'add'"; 141 142 private static final String IS_REFERENCE_DELETED_IN_TRANSACTION = "SELECT TRUE FROM " + TRANSACTION_TABLE + 143 " WHERE " + RESOURCE_COLUMN + " = :resourceId AND " + SUBJECT_COLUMN + " = :subjectId AND " + 144 PROPERTY_COLUMN + " = :property AND " + TARGET_COLUMN + " = :targetId AND " + TRANSACTION_COLUMN + 145 " = :transactionId AND " + OPERATION_COLUMN + " = 'delete'"; 146 147 private static final String COMMIT_ADD_RECORDS = "INSERT INTO " + TABLE_NAME + " ( " + RESOURCE_COLUMN + ", " + 148 SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + ", " + TARGET_COLUMN + " ) SELECT " + RESOURCE_COLUMN + ", " + 149 SUBJECT_COLUMN + ", " + PROPERTY_COLUMN + ", " + TARGET_COLUMN + " FROM " + TRANSACTION_TABLE + " WHERE " + 150 TRANSACTION_COLUMN + " = :transactionId AND " + OPERATION_COLUMN + " = 'add'"; 151 152 private static final String COMMIT_DELETE_RECORDS = "DELETE FROM " + TABLE_NAME + " WHERE " + 153 "EXISTS (SELECT * FROM " + TRANSACTION_TABLE + " t WHERE t." + 154 TRANSACTION_COLUMN + " = :transactionId AND t." + OPERATION_COLUMN + " = 'delete' AND" + 155 " t." + RESOURCE_COLUMN + " = " + TABLE_NAME + "." + RESOURCE_COLUMN + " AND" + 156 " t." + SUBJECT_COLUMN + " = " + TABLE_NAME + "." + SUBJECT_COLUMN + 157 " AND t." + PROPERTY_COLUMN + " = " + TABLE_NAME + "." + PROPERTY_COLUMN + 158 " AND t." + TARGET_COLUMN + " = " + TABLE_NAME + "." + TARGET_COLUMN + ")"; 159 160 private static final String DELETE_TRANSACTION = "DELETE FROM " + TRANSACTION_TABLE + " WHERE " + 161 TRANSACTION_COLUMN + " = :transactionId"; 162 163 private static final String TRUNCATE_TABLE = "TRUNCATE TABLE " + TABLE_NAME; 164 private static final String TRUNCATE_TX_TABLE = "TRUNCATE TABLE " + TRANSACTION_TABLE; 165 166 @PostConstruct 167 public void setUp() { 168 jdbcTemplate = new NamedParameterJdbcTemplate(getDataSource()); 169 } 170 171 @Override 172 public RdfStream getInboundReferences(@Nonnull final Transaction tx, final FedoraResource resource) { 173 final String resourceId = resource.getFedoraId().getFullId(); 174 final Node subject = NodeFactory.createURI(resourceId); 175 final Stream<Triple> stream = getReferencesInternal(tx, resourceId); 176 if (resource instanceof NonRdfSourceDescription) { 177 final Stream<Triple> stream2 = getReferencesInternal(tx, resource.getFedoraId().getBaseId()); 178 return new DefaultRdfStream(subject, Stream.concat(stream, stream2)); 179 } 180 return new DefaultRdfStream(subject, stream); 181 } 182 183 /** 184 * Get the inbound references for the resource Id and the transaction id. 185 * @param tx transaction or null for none. 186 * @param targetId the id that will be the target of references. 187 * @return RDF stream of inbound references 188 */ 189 private Stream<Triple> getReferencesInternal(final Transaction tx, final String targetId) { 190 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 191 parameterSource.addValue("targetId", targetId); 192 final Node targetNode = NodeFactory.createURI(targetId); 193 194 final RowMapper<Triple> inboundMapper = (rs, rowNum) -> 195 Triple.create(NodeFactory.createURI(rs.getString(SUBJECT_COLUMN)), 196 NodeFactory.createURI(rs.getString(PROPERTY_COLUMN)), 197 targetNode); 198 199 final String query; 200 201 if (tx.isOpenLongRunning()) { 202 // we are in a transaction 203 parameterSource.addValue("transactionId", tx.getId()); 204 query = SELECT_INBOUND_IN_TRANSACTION; 205 } else { 206 // not in a transaction 207 query = SELECT_INBOUND; 208 } 209 210 final var references = jdbcTemplate.query(query, parameterSource, inboundMapper); 211 212 LOGGER.debug("getInboundReferences for {} in transaction {} found {} references", 213 targetId, tx, references.size()); 214 return references.stream(); 215 } 216 217 @Override 218 public void deleteAllReferences(@Nonnull final Transaction tx, final FedoraId resourceId) { 219 final List<Quad> deleteReferences = getOutboundReferences(tx, resourceId); 220 if (resourceId.isDescription()) { 221 // Also get the binary references 222 deleteReferences.addAll(getOutboundReferences(tx, resourceId.asBaseId())); 223 } 224 // Remove all the existing references. 225 deleteReferences.forEach(t -> removeReference(tx, t)); 226 } 227 228 /** 229 * Get a stream of quads of resources being referenced from the provided resource, the graph of the quad is the 230 * URI of the resource the reference is from. 231 * @param tx transaction Id or null if none. 232 * @param resourceId the resource Id. 233 * @return list of Quads 234 */ 235 private List<Quad> getOutboundReferences(final Transaction tx, final FedoraId resourceId) { 236 final MapSqlParameterSource parameterSource = new MapSqlParameterSource(); 237 parameterSource.addValue("resourceId", resourceId.getFullId()); 238 final Node subjectNode = NodeFactory.createURI(resourceId.getFullId()); 239 240 final RowMapper<Quad> outboundMapper = (rs, rowNum) -> 241 Quad.create(subjectNode, 242 NodeFactory.createURI(rs.getString(SUBJECT_COLUMN)), 243 NodeFactory.createURI(rs.getString(PROPERTY_COLUMN)), 244 NodeFactory.createURI(rs.getString(TARGET_COLUMN))); 245 246 final String query; 247 248 if (tx.isOpenLongRunning()) { 249 // we are in a long-running transaction 250 parameterSource.addValue("transactionId", tx.getId()); 251 query = SELECT_OUTBOUND_IN_TRANSACTION; 252 } else { 253 // not in a transaction or in a short-lived transaction 254 query = SELECT_OUTBOUND; 255 } 256 257 final var references = jdbcTemplate.query(query, parameterSource, outboundMapper); 258 259 LOGGER.debug("getOutboundReferences for {} in transaction {} found {} references", 260 resourceId, tx, references.size()); 261 return references; 262 } 263 264 @Override 265 public void updateReferences(@Nonnull final Transaction tx, final FedoraId resourceId, final String userPrincipal, 266 final RdfStream rdfStream) { 267 try { 268 final List<Triple> addReferences = getReferencesFromRdf(rdfStream).collect(Collectors.toList()); 269 // This predicate checks for items we are adding, so we don't bother to delete and then re-add them. 270 final Predicate<Quad> notInAdds = q -> !addReferences.contains(q.asTriple()); 271 // References from this resource. 272 final List<Quad> existingReferences = getOutboundReferences(tx, resourceId); 273 if (resourceId.isDescription()) { 274 // Resource is a binary description so also get the binary references. 275 existingReferences.addAll(getOutboundReferences(tx, resourceId.asBaseId())); 276 } 277 // Remove any existing references not being re-added. 278 existingReferences.stream().filter(notInAdds).forEach(t -> removeReference(tx, t)); 279 final Node resourceNode = NodeFactory.createURI(resourceId.getFullId()); 280 // This predicate checks for references that didn't already exist in the database. 281 final Predicate<Triple> alreadyExists = t -> !existingReferences.contains(Quad.create(resourceNode, t)); 282 // Add the new references. 283 addReferences.stream().filter(alreadyExists).forEach(r -> 284 addReference(tx, Quad.create(resourceNode, r), userPrincipal)); 285 } catch (final Exception e) { 286 LOGGER.warn("Unable to update reference index for resource {} in transaction {}: {}", 287 resourceId.getFullId(), tx.getId(), e.getMessage()); 288 throw new RepositoryRuntimeException("Unable to update reference index", e); 289 } 290 } 291 292 @Override 293 public void commitTransaction(final Transaction tx) { 294 if (!tx.isShortLived()) { 295 tx.ensureCommitting(); 296 try { 297 final Map<String, String> parameterSource = Map.of("transactionId", tx.getId()); 298 jdbcTemplate.update(COMMIT_DELETE_RECORDS, parameterSource); 299 jdbcTemplate.update(COMMIT_ADD_RECORDS, parameterSource); 300 jdbcTemplate.update(DELETE_TRANSACTION, parameterSource); 301 } catch (final Exception e) { 302 LOGGER.warn("Unable to commit reference index transaction {}: {}", tx, e.getMessage()); 303 throw new RepositoryRuntimeException("Unable to commit reference index transaction", e); 304 } 305 } 306 } 307 308 @Transactional(propagation = Propagation.NOT_SUPPORTED) 309 @Override 310 public void rollbackTransaction(final Transaction tx) { 311 if (!tx.isShortLived()) { 312 try { 313 final Map<String, String> parameterSource = Map.of("transactionId", tx.getId()); 314 jdbcTemplate.update(DELETE_TRANSACTION, parameterSource); 315 } catch (final Exception e) { 316 LOGGER.warn("Unable to rollback reference index transaction {}: {}", tx, e.getMessage()); 317 throw new RepositoryRuntimeException("Unable to rollback reference index transaction", e); 318 } 319 } 320 } 321 322 @Override 323 public void clearAllTransactions() { 324 jdbcTemplate.update(TRUNCATE_TX_TABLE, Map.of()); 325 } 326 327 @Override 328 public void reset() { 329 try { 330 jdbcTemplate.update(TRUNCATE_TABLE, Map.of()); 331 jdbcTemplate.update(TRUNCATE_TX_TABLE, Map.of()); 332 } catch (final Exception e) { 333 LOGGER.warn("Unable to reset reference index: {}", e.getMessage()); 334 throw new RepositoryRuntimeException("Unable to reset reference index", e); 335 } 336 } 337 338 /** 339 * Remove a reference. 340 * @param tx the transaction 341 * @param reference the quad with the reference, is Quad(resourceId, subjectId, propertyId, targetId) 342 */ 343 private void removeReference(final Transaction tx, final Quad reference) { 344 tx.doInTx(() -> { 345 final var parameterSource = new MapSqlParameterSource(); 346 parameterSource.addValue("resourceId", reference.getGraph().getURI()); 347 parameterSource.addValue("subjectId", reference.getSubject().getURI()); 348 parameterSource.addValue("property", reference.getPredicate().getURI()); 349 parameterSource.addValue("targetId", reference.getObject().getURI()); 350 351 if (!tx.isShortLived()) { 352 parameterSource.addValue("transactionId", tx.getId()); 353 final boolean addedInTx = !jdbcTemplate.queryForList(IS_REFERENCE_ADDED_IN_TRANSACTION, parameterSource) 354 .isEmpty(); 355 if (addedInTx) { 356 jdbcTemplate.update(UNDO_INSERT_REFERENCE_IN_TRANSACTION, parameterSource); 357 } else { 358 jdbcTemplate.update(DELETE_REFERENCE_IN_TRANSACTION, parameterSource); 359 } 360 } else { 361 jdbcTemplate.update(DELETE_REFERENCE_DIRECT, parameterSource); 362 } 363 }); 364 } 365 366 /** 367 * Add a reference 368 * @param transaction the transaction Id. 369 * @param reference the quad with the reference, is is Quad(resourceId, subjectId, propertyId, targetId) 370 * @param userPrincipal the user adding the reference. 371 */ 372 private void addReference(@Nonnull final Transaction transaction, final Quad reference, 373 final String userPrincipal) { 374 transaction.doInTx(() -> { 375 final String targetId = reference.getObject().getURI(); 376 377 final var parameterSource = new MapSqlParameterSource(); 378 parameterSource.addValue("resourceId", reference.getGraph().getURI()); 379 parameterSource.addValue("subjectId", reference.getSubject().getURI()); 380 parameterSource.addValue("property", reference.getPredicate().getURI()); 381 parameterSource.addValue("targetId", targetId); 382 383 if (!transaction.isShortLived()) { 384 parameterSource.addValue("transactionId", transaction.getId()); 385 final boolean addedInTx = !jdbcTemplate.queryForList( 386 IS_REFERENCE_DELETED_IN_TRANSACTION, parameterSource) 387 .isEmpty(); 388 if (addedInTx) { 389 jdbcTemplate.update(UNDO_DELETE_REFERENCE_IN_TRANSACTION, parameterSource); 390 } else { 391 jdbcTemplate.update(INSERT_REFERENCE_IN_TRANSACTION, parameterSource); 392 recordEvent(transaction, targetId, userPrincipal); 393 } 394 } else { 395 jdbcTemplate.update(INSERT_REFERENCE_DIRECT, parameterSource); 396 recordEvent(transaction, targetId, userPrincipal); 397 } 398 }); 399 } 400 401 /** 402 * Record the inbound reference event if the target exists. 403 * @param transaction the transaction. 404 * @param resourceId the id of the target of the inbound reference. 405 * @param userPrincipal the user making the reference. 406 */ 407 private void recordEvent(final Transaction transaction, final String resourceId, final String userPrincipal) { 408 final FedoraId fedoraId = FedoraId.create(resourceId); 409 if (this.containmentIndex.resourceExists(transaction, fedoraId, false)) { 410 this.eventAccumulator.recordEventForOperation(transaction, fedoraId, getOperation(transaction, fedoraId, 411 userPrincipal)); 412 } 413 } 414 415 /** 416 * Create a ReferenceOperation for the current add. 417 * @param tx the transaction for the current operation. 418 * @param id the target resource of the reference. 419 * @param user the user making the change 420 * @return a ReferenceOperation 421 */ 422 private static ReferenceOperation getOperation(final Transaction tx, final FedoraId id, final String user) { 423 final ReferenceOperationBuilder builder = new ReferenceOperationBuilder(tx, id); 424 builder.userPrincipal(user); 425 return builder.build(); 426 } 427 428 /** 429 * Utility to filter a RDFStream to just the URIs from subjects and objects within the repository. 430 * @param stream the provided stream 431 * @return stream of triples with internal references. 432 */ 433 private Stream<Triple> getReferencesFromRdf(final RdfStream stream) { 434 final Predicate<Triple> isInternalReference = t -> { 435 final Node s = t.getSubject(); 436 final Node o = t.getObject(); 437 return (s.isURI() && s.getURI().startsWith(FEDORA_ID_PREFIX) && o.isURI() && 438 o.getURI().startsWith(FEDORA_ID_PREFIX)); 439 }; 440 return stream.filter(isInternalReference); 441 } 442 443 /** 444 * Set the JDBC datastore. 445 * @param dataSource the dataStore. 446 */ 447 public void setDataSource(final DataSource dataSource) { 448 this.dataSource = dataSource; 449 } 450 451 /** 452 * Get the JDBC datastore. 453 * @return the dataStore. 454 */ 455 public DataSource getDataSource() { 456 return dataSource; 457 } 458}