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 org.apache.jena.rdf.model.Model;
021
022import org.fcrepo.kernel.api.RdfStream;
023import org.fcrepo.kernel.api.Transaction;
024import org.fcrepo.kernel.api.exception.CannotCreateResourceException;
025import org.fcrepo.kernel.api.exception.InteractionModelViolationException;
026import org.fcrepo.kernel.api.exception.ItemNotFoundException;
027import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
028import org.fcrepo.kernel.api.identifiers.FedoraId;
029import org.fcrepo.kernel.api.models.ExternalContent;
030import org.fcrepo.kernel.api.models.ResourceHeaders;
031import org.fcrepo.kernel.api.operations.CreateNonRdfSourceOperationBuilder;
032import org.fcrepo.kernel.api.operations.NonRdfSourceOperationFactory;
033import org.fcrepo.kernel.api.operations.RdfSourceOperation;
034import org.fcrepo.kernel.api.operations.RdfSourceOperationFactory;
035import org.fcrepo.kernel.api.operations.ResourceOperation;
036import org.fcrepo.kernel.api.services.CreateResourceService;
037import org.fcrepo.persistence.api.PersistentStorageSession;
038import org.fcrepo.persistence.api.PersistentStorageSessionManager;
039import org.fcrepo.persistence.api.exceptions.PersistentItemNotFoundException;
040import org.fcrepo.persistence.api.exceptions.PersistentStorageException;
041import org.fcrepo.persistence.common.MultiDigestInputStreamWrapper;
042import org.slf4j.Logger;
043import org.springframework.stereotype.Component;
044
045import javax.inject.Inject;
046import javax.ws.rs.BadRequestException;
047import javax.ws.rs.core.Link;
048import java.io.InputStream;
049import java.net.URI;
050import java.util.Collection;
051import java.util.Collections;
052import java.util.List;
053import java.util.stream.Collectors;
054
055import static java.util.Collections.emptyList;
056import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP;
057import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI;
058import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_PAIR_TREE;
059import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
060import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel;
061import static org.slf4j.LoggerFactory.getLogger;
062import static org.springframework.util.CollectionUtils.isEmpty;
063
064/**
065 * Create a RdfSource resource.
066 * @author whikloj
067 * TODO: bbpennel has thoughts about moving this to HTTP layer.
068 */
069@Component
070public class CreateResourceServiceImpl extends AbstractService implements CreateResourceService {
071
072    private static final Logger LOGGER = getLogger(CreateResourceServiceImpl.class);
073
074    @Inject
075    private PersistentStorageSessionManager psManager;
076
077    @Inject
078    private RdfSourceOperationFactory rdfSourceOperationFactory;
079
080    @Inject
081    private NonRdfSourceOperationFactory nonRdfSourceOperationFactory;
082
083    @Override
084    public void perform(final Transaction tx, final String userPrincipal, final FedoraId fedoraId,
085                        final String contentType, final String filename,
086                        final long contentSize, final List<String> linkHeaders, final Collection<URI> digest,
087                        final InputStream requestBody, final ExternalContent externalContent) {
088        final PersistentStorageSession pSession = this.psManager.getSession(tx);
089        checkAclLinkHeader(linkHeaders);
090        // Locate a containment parent of fedoraId, if exists.
091        final FedoraId parentId = containmentIndex.getContainerIdByPath(tx, fedoraId, true);
092        checkParent(pSession, parentId);
093
094        final CreateNonRdfSourceOperationBuilder builder;
095        String mimeType = contentType;
096        long size = contentSize;
097        if (externalContent == null || externalContent.isCopy()) {
098            var contentInputStream = requestBody;
099            if (externalContent != null) {
100                LOGGER.debug("External content COPY '{}', '{}'", fedoraId, externalContent.getURL());
101                contentInputStream = externalContent.fetchExternalContent();
102            }
103
104            builder = nonRdfSourceOperationFactory.createInternalBinaryBuilder(tx, fedoraId, contentInputStream);
105        } else {
106            builder = nonRdfSourceOperationFactory.createExternalBinaryBuilder(tx, fedoraId,
107                    externalContent.getHandling(), externalContent.getURI());
108            if (contentSize == -1L) {
109                size = externalContent.getContentSize();
110            }
111            if (!digest.isEmpty()) {
112                final var multiDigestWrapper = new MultiDigestInputStreamWrapper(
113                        externalContent.fetchExternalContent(),
114                        digest,
115                        Collections.emptyList());
116                multiDigestWrapper.checkFixity();
117            }
118        }
119
120        if (externalContent != null && externalContent.getContentType() != null) {
121            mimeType = externalContent.getContentType();
122        }
123
124        final ResourceOperation createOp = builder
125                .parentId(parentId)
126                .userPrincipal(userPrincipal)
127                .contentDigests(digest)
128                .mimeType(mimeType)
129                .contentSize(size)
130                .filename(filename)
131                .build();
132
133        lockArchivalGroupResourceFromParent(tx, pSession, parentId);
134        tx.lockResource(fedoraId);
135
136        try {
137            pSession.persist(createOp);
138        } catch (final PersistentStorageException exc) {
139            throw new RepositoryRuntimeException(String.format("failed to create resource %s", fedoraId), exc);
140        }
141
142        // Populate the description for the new binary
143        createDescription(tx, pSession, userPrincipal, fedoraId);
144        addToContainmentIndex(tx, parentId, fedoraId);
145        membershipService.resourceCreated(tx, fedoraId);
146        addToSearchIndex(tx, fedoraId, pSession);
147        recordEvent(tx, fedoraId, createOp);
148    }
149
150    private void createDescription(final Transaction tx,
151                                   final PersistentStorageSession pSession,
152                                   final String userPrincipal,
153                                   final FedoraId binaryId) {
154        final var descId = binaryId.asDescription();
155        final var createOp = rdfSourceOperationFactory.createBuilder(
156                    tx,
157                    descId,
158                    FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI,
159                    fedoraPropsConfig.getServerManagedPropsMode()
160                ).userPrincipal(userPrincipal)
161                .parentId(binaryId)
162                .build();
163
164        tx.lockResource(descId);
165
166        try {
167            pSession.persist(createOp);
168        } catch (final PersistentStorageException exc) {
169            throw new RepositoryRuntimeException(String.format("failed to create description %s", descId), exc);
170        }
171    }
172
173    @Override
174    public void perform(final Transaction tx, final String userPrincipal, final FedoraId fedoraId,
175            final List<String> linkHeaders, final Model model) {
176        final PersistentStorageSession pSession = this.psManager.getSession(tx);
177        checkAclLinkHeader(linkHeaders);
178        // Locate a containment parent of fedoraId, if exists.
179        final FedoraId parentId = containmentIndex.getContainerIdByPath(tx, fedoraId, true);
180        checkParent(pSession, parentId);
181
182        final List<String> rdfTypes = isEmpty(linkHeaders) ? emptyList() : getTypes(linkHeaders);
183        final String interactionModel = determineInteractionModel(rdfTypes, true,
184                model != null, false);
185
186        final RdfStream stream = fromModel(model.getResource(fedoraId.getFullId()).asNode(), model);
187
188        ensureValidDirectContainer(fedoraId, interactionModel, model);
189
190        final RdfSourceOperation createOp = rdfSourceOperationFactory
191                .createBuilder(tx, fedoraId, interactionModel, fedoraPropsConfig.getServerManagedPropsMode())
192                .parentId(parentId)
193                .triples(stream)
194                .relaxedProperties(model)
195                .archivalGroup(rdfTypes.contains(ARCHIVAL_GROUP.getURI()))
196                .userPrincipal(userPrincipal)
197                .build();
198
199        lockArchivalGroupResourceFromParent(tx, pSession, parentId);
200        tx.lockResource(fedoraId);
201
202        try {
203            pSession.persist(createOp);
204        } catch (final PersistentStorageException exc) {
205            throw new RepositoryRuntimeException(String.format("failed to create resource %s", fedoraId), exc);
206        }
207
208        updateReferences(tx, fedoraId, userPrincipal, model);
209        addToContainmentIndex(tx, parentId, fedoraId);
210        membershipService.resourceCreated(tx, fedoraId);
211        addToSearchIndex(tx, fedoraId, pSession);
212        recordEvent(tx, fedoraId, createOp);
213    }
214
215    private void addToSearchIndex(final Transaction tx, final FedoraId fedoraId,
216                                  final PersistentStorageSession persistentStorageSession) {
217        final var resourceHeaders = persistentStorageSession.getHeaders(fedoraId, null);
218        this.searchIndex.addUpdateIndex(tx, resourceHeaders);
219    }
220
221    /**
222     * Check the parent to contain the new resource exists and can have a child.
223     *
224     * @param pSession a persistence session.
225     * @param fedoraId Id of parent.
226     */
227    private void checkParent(final PersistentStorageSession pSession, final FedoraId fedoraId)
228        throws RepositoryRuntimeException {
229
230        if (fedoraId != null && !fedoraId.isRepositoryRoot()) {
231            final ResourceHeaders parent;
232            try {
233                // Make sure the parent exists.
234                // TODO: object existence can be from the index, but we don't have interaction model. Should we add it?
235                parent = pSession.getHeaders(fedoraId.asResourceId(), null);
236            } catch (final PersistentItemNotFoundException exc) {
237                throw new ItemNotFoundException(String.format("Item %s was not found", fedoraId), exc);
238            } catch (final PersistentStorageException exc) {
239                throw new RepositoryRuntimeException(String.format("Failed to find storage headers for %s", fedoraId),
240                    exc);
241            }
242            if (parent.isDeleted()) {
243                throw new CannotCreateResourceException(
244                        String.format("Cannot create resource as child of a tombstone. Tombstone found at %s",
245                                fedoraId.getFullIdPath()));
246            }
247            final boolean isParentBinary = NON_RDF_SOURCE.toString().equals(parent.getInteractionModel());
248            if (isParentBinary) {
249                // Binary is not a container, can't have children.
250                throw new InteractionModelViolationException("NonRdfSource resources cannot contain other resources");
251            }
252            // TODO: Will this type still be needed?
253            final boolean isPairTree = FEDORA_PAIR_TREE.toString().equals(parent.getInteractionModel());
254            if (isPairTree) {
255                throw new CannotCreateResourceException("Objects cannot be created under pairtree nodes");
256            }
257        }
258    }
259
260    /**
261     * Get the rel="type" link headers from a list of them.
262     * @param headers a list of string LINK headers.
263     * @return a list of LINK headers with rel="type"
264     */
265    private List<String> getTypes(final List<String> headers) {
266        final List<Link> hdrobjs = getLinkHeaders(headers);
267        try {
268            return hdrobjs == null ? emptyList() : hdrobjs.stream()
269                    .filter(p -> p.getRel().equalsIgnoreCase("type")).map(Link::getUri)
270                    .map(URI::toString).collect(Collectors.toList());
271        } catch (final Exception e ) {
272            throw new BadRequestException("Invalid Link header type found",e);
273        }
274    }
275
276    /**
277     * Converts a list of string LINK headers to actual LINK objects.
278     * @param headers the list of string link headers.
279     * @return the list of LINK headers.
280     */
281    private List<Link> getLinkHeaders(final List<String> headers) {
282        return headers == null ? null : headers.stream().map(Link::valueOf).collect(Collectors.toList());
283    }
284
285    /**
286     * Add this pairing to the containment index.
287     * @param tx The transaction.
288     * @param parentId The parent ID.
289     * @param id The child ID.
290     */
291    private void addToContainmentIndex(final Transaction tx, final FedoraId parentId, final FedoraId id) {
292        containmentIndex.addContainedBy(tx, parentId, id);
293    }
294}