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