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