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