001 002/* 003 * The contents of this file are subject to the license and copyright 004 * detailed in the LICENSE and NOTICE files at the root of the source 005 * tree. 006 */ 007package org.fcrepo.kernel.impl.services; 008 009import org.apache.jena.rdf.model.Model; 010 011import org.apache.jena.rdf.model.Resource; 012import org.apache.jena.rdf.model.Statement; 013import org.fcrepo.kernel.api.RdfLexicon; 014import org.fcrepo.kernel.api.Transaction; 015import org.fcrepo.kernel.api.auth.ACLHandle; 016import org.fcrepo.kernel.api.exception.MalformedRdfException; 017import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 018import org.fcrepo.kernel.api.identifiers.FedoraId; 019import org.fcrepo.kernel.api.operations.NonRdfSourceOperationFactory; 020import org.fcrepo.kernel.api.operations.RdfSourceOperationFactory; 021import org.fcrepo.kernel.api.operations.ResourceOperation; 022import org.fcrepo.kernel.api.services.ReplacePropertiesService; 023import org.fcrepo.persistence.api.PersistentStorageSession; 024import org.fcrepo.persistence.api.PersistentStorageSessionManager; 025import org.fcrepo.persistence.api.exceptions.PersistentStorageException; 026import org.springframework.stereotype.Component; 027 028import javax.inject.Inject; 029 030import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel; 031 032import java.util.List; 033import java.util.Optional; 034 035import com.github.benmanes.caffeine.cache.Cache; 036 037/** 038 * This class mediates update operations between the kernel and persistent storage layers 039 * @author bseeger 040 */ 041@Component 042public class ReplacePropertiesServiceImpl extends AbstractService implements ReplacePropertiesService { 043 044 @Inject 045 private PersistentStorageSessionManager psManager; 046 047 @Inject 048 private RdfSourceOperationFactory factory; 049 050 @Inject 051 private NonRdfSourceOperationFactory nonRdfFactory; 052 053 @Inject 054 private Cache<String, Optional<ACLHandle>> authHandleCache; 055 056 @Override 057 public void perform(final Transaction tx, 058 final String userPrincipal, 059 final FedoraId fedoraId, 060 final Model inputModel) throws MalformedRdfException { 061 try { 062 final PersistentStorageSession pSession = psManager.getSession(tx); 063 064 final var headers = pSession.getHeaders(fedoraId, null); 065 final var interactionModel = headers.getInteractionModel(); 066 067 ensureValidDirectContainer(fedoraId, interactionModel, inputModel); 068 ensureValidACLAuthorization(inputModel); 069 // Extract triples which impact the headers of binary resources from incoming description RDF 070 final BinaryHeaderDetails binHeaders = extractNonRdfSourceHeaderTriples(fedoraId, inputModel); 071 072 final var rdfStream = fromModel( 073 inputModel.createResource(fedoraId.getFullDescribedId()).asNode(), inputModel); 074 final var serverManagedMode = fedoraPropsConfig.getServerManagedPropsMode(); 075 076 // create 2 updates -- one for the properties coming in and one for and server managed properties 077 final ResourceOperation primaryOp; 078 final Optional<ResourceOperation> secondaryOp; 079 if (fedoraId.isDescription()) { 080 primaryOp = factory.updateBuilder(tx, fedoraId, serverManagedMode) 081 .userPrincipal(userPrincipal) 082 .triples(rdfStream) 083 .build(); 084 085 // we need to use the description id until we write the headers in order to resolve properties 086 secondaryOp = Optional.of(nonRdfFactory.updateHeadersBuilder(tx, fedoraId, serverManagedMode) 087 .relaxedProperties(inputModel) 088 .userPrincipal(userPrincipal) 089 .filename(binHeaders.getFilename()) 090 .mimeType(binHeaders.getMimetype()) 091 .build()); 092 } else { 093 primaryOp = factory.updateBuilder(tx, fedoraId, serverManagedMode) 094 .relaxedProperties(inputModel) 095 .userPrincipal(userPrincipal) 096 .triples(rdfStream) 097 .build(); 098 secondaryOp = Optional.empty(); 099 } 100 101 lockArchivalGroupResource(tx, pSession, fedoraId); 102 tx.lockResource(fedoraId); 103 if (RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI.equals(interactionModel)) { 104 tx.lockResource(fedoraId.asBaseId()); 105 } 106 107 pSession.persist(primaryOp); 108 109 userTypesCache.cacheUserTypes(fedoraId, 110 fromModel(inputModel.getResource(fedoraId.getFullId()).asNode(), inputModel), pSession.getId()); 111 112 updateReferences(tx, fedoraId, userPrincipal, inputModel); 113 membershipService.resourceModified(tx, fedoraId); 114 searchIndex.addUpdateIndex(tx, pSession.getHeaders(fedoraId, null)); 115 recordEvent(tx, fedoraId, primaryOp); 116 secondaryOp.ifPresent(operation -> updateBinaryHeaders(tx, pSession, operation)); 117 if (fedoraId.isAcl()) { 118 // Flush ACL cache on any ACL creation/update/deletion. 119 authHandleCache.invalidateAll(); 120 } 121 } catch (final PersistentStorageException ex) { 122 throw new RepositoryRuntimeException(String.format("failed to replace resource %s", 123 fedoraId), ex); 124 } 125 } 126 127 private void updateBinaryHeaders(final Transaction tx, 128 final PersistentStorageSession pSession, 129 final ResourceOperation operation) { 130 pSession.persist(operation); 131 recordEvent(tx, operation.getResourceId(), operation); 132 } 133 134 protected BinaryHeaderDetails extractNonRdfSourceHeaderTriples(final FedoraId fedoraId, final Model model) { 135 if (!fedoraId.isDescription()) { 136 return null; 137 } 138 final BinaryHeaderDetails details = new BinaryHeaderDetails(); 139 final Resource binResc = model.getResource(fedoraId.getBaseId()); 140 if (binResc.hasProperty(RdfLexicon.HAS_MIME_TYPE)) { 141 final List<Statement> mimetypes = binResc.listProperties(RdfLexicon.HAS_MIME_TYPE).toList(); 142 if (mimetypes.size() > 1) { 143 throw new MalformedRdfException("Invalid RDF, cannot provided multiple values for property " 144 + RdfLexicon.HAS_MIME_TYPE); 145 } 146 details.setMimetype(mimetypes.get(0).getString()); 147 binResc.removeAll(RdfLexicon.HAS_MIME_TYPE); 148 } 149 if (binResc.hasProperty(RdfLexicon.HAS_ORIGINAL_NAME)) { 150 final List<Statement> filenames = binResc.listProperties(RdfLexicon.HAS_ORIGINAL_NAME).toList(); 151 if (filenames.size() > 1) { 152 throw new MalformedRdfException("Invalid RDF, cannot provided multiple values for property " 153 + RdfLexicon.HAS_ORIGINAL_NAME); 154 } 155 details.setFilename(filenames.get(0).getString()); 156 binResc.removeAll(RdfLexicon.HAS_ORIGINAL_NAME); 157 } 158 return details; 159 } 160 161 private static class BinaryHeaderDetails { 162 private String mimetype; 163 private String filename; 164 165 public String getMimetype() { 166 return mimetype; 167 } 168 169 public void setMimetype(final String mimetype) { 170 this.mimetype = mimetype; 171 } 172 173 public String getFilename() { 174 return filename; 175 } 176 177 public void setFilename(final String filename) { 178 this.filename = filename; 179 } 180 } 181}