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