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}