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                    fedoraPropsConfig.getServerManagedPropsMode()
157                ).userPrincipal(userPrincipal)
158                .parentId(binaryId)
159                .build();
160
161        tx.lockResource(descId);
162
163        try {
164            pSession.persist(createOp);
165        } catch (final PersistentStorageException exc) {
166            throw new RepositoryRuntimeException(String.format("failed to create description %s", descId), exc);
167        }
168    }
169
170    @Override
171    public void perform(final Transaction tx, final String userPrincipal, final FedoraId fedoraId,
172            final List<String> linkHeaders, final Model model) {
173        final var txId = tx.getId();
174        final PersistentStorageSession pSession = this.psManager.getSession(txId);
175        checkAclLinkHeader(linkHeaders);
176        // Locate a containment parent of fedoraId, if exists.
177        final FedoraId parentId = containmentIndex.getContainerIdByPath(txId, fedoraId, true);
178        checkParent(pSession, parentId);
179
180        final List<String> rdfTypes = isEmpty(linkHeaders) ? emptyList() : getTypes(linkHeaders);
181        final String interactionModel = determineInteractionModel(rdfTypes, true,
182                model != null, false);
183
184        final RdfStream stream = fromModel(model.getResource(fedoraId.getFullId()).asNode(), model);
185
186        ensureValidDirectContainer(fedoraId, interactionModel, model);
187
188        final RdfSourceOperation createOp = rdfSourceOperationFactory
189                .createBuilder(fedoraId, interactionModel, fedoraPropsConfig.getServerManagedPropsMode())
190                .parentId(parentId)
191                .triples(stream)
192                .relaxedProperties(model)
193                .archivalGroup(rdfTypes.contains(ARCHIVAL_GROUP.getURI()))
194                .userPrincipal(userPrincipal)
195                .build();
196
197        lockArchivalGroupResourceFromParent(tx, pSession, parentId);
198        tx.lockResource(fedoraId);
199
200        try {
201            pSession.persist(createOp);
202            updateReferences(txId, fedoraId, userPrincipal, model);
203            addToContainmentIndex(txId, parentId, fedoraId);
204            membershipService.resourceCreated(txId, fedoraId);
205            recordEvent(txId, fedoraId, createOp);
206        } catch (final PersistentStorageException exc) {
207            throw new RepositoryRuntimeException(String.format("failed to create resource %s", fedoraId), exc);
208        }
209
210    }
211
212    /**
213     * Check the parent to contain the new resource exists and can have a child.
214     *
215     * @param pSession a persistence session.
216     * @param fedoraId Id of parent.
217     */
218    private void checkParent(final PersistentStorageSession pSession, final FedoraId fedoraId)
219        throws RepositoryRuntimeException {
220
221        if (fedoraId != null && !fedoraId.isRepositoryRoot()) {
222            final ResourceHeaders parent;
223            try {
224                // Make sure the parent exists.
225                // TODO: object existence can be from the index, but we don't have interaction model. Should we add it?
226                parent = pSession.getHeaders(fedoraId.asResourceId(), null);
227            } catch (final PersistentItemNotFoundException exc) {
228                throw new ItemNotFoundException(String.format("Item %s was not found", fedoraId), exc);
229            } catch (final PersistentStorageException exc) {
230                throw new RepositoryRuntimeException(String.format("Failed to find storage headers for %s", fedoraId),
231                    exc);
232            }
233            if (parent.isDeleted()) {
234                throw new CannotCreateResourceException(
235                        String.format("Cannot create resource as child of a tombstone. Tombstone found at %s",
236                                fedoraId.getFullIdPath()));
237            }
238            final boolean isParentBinary = NON_RDF_SOURCE.toString().equals(parent.getInteractionModel());
239            if (isParentBinary) {
240                // Binary is not a container, can't have children.
241                throw new InteractionModelViolationException("NonRdfSource resources cannot contain other resources");
242            }
243            // TODO: Will this type still be needed?
244            final boolean isPairTree = FEDORA_PAIR_TREE.toString().equals(parent.getInteractionModel());
245            if (isPairTree) {
246                throw new CannotCreateResourceException("Objects cannot be created under pairtree nodes");
247            }
248        }
249    }
250
251    /**
252     * Get the rel="type" link headers from a list of them.
253     * @param headers a list of string LINK headers.
254     * @return a list of LINK headers with rel="type"
255     */
256    private List<String> getTypes(final List<String> headers) {
257        final List<Link> hdrobjs = getLinkHeaders(headers);
258        try {
259            return hdrobjs == null ? emptyList() : hdrobjs.stream()
260                    .filter(p -> p.getRel().equalsIgnoreCase("type")).map(Link::getUri)
261                    .map(URI::toString).collect(Collectors.toList());
262        } catch (final Exception e ) {
263            throw new BadRequestException("Invalid Link header type found",e);
264        }
265    }
266
267    /**
268     * Converts a list of string LINK headers to actual LINK objects.
269     * @param headers the list of string link headers.
270     * @return the list of LINK headers.
271     */
272    private List<Link> getLinkHeaders(final List<String> headers) {
273        return headers == null ? null : headers.stream().map(Link::valueOf).collect(Collectors.toList());
274    }
275
276    /**
277     * Add this pairing to the containment index.
278     * @param txId The transaction ID or null if no transaction.
279     * @param parentId The parent ID.
280     * @param id The child ID.
281     */
282    private void addToContainmentIndex(final String txId, final FedoraId parentId, final FedoraId id) {
283        containmentIndex.addContainedBy(txId, parentId, id);
284    }
285}