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