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}