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