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