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