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}