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