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