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}