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.modeshape.services; 019 020import static java.util.Arrays.asList; 021import static java.util.Arrays.stream; 022import static org.apache.jena.graph.NodeFactory.createURI; 023import static org.apache.jena.rdf.model.ResourceFactory.createResource; 024import static org.fcrepo.kernel.api.FedoraExternalContent.PROXY; 025import static org.fcrepo.kernel.api.FedoraExternalContent.REDIRECT; 026import static org.fcrepo.kernel.api.FedoraTypes.CONTENT_DIGEST; 027import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER; 028import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_RESOURCE; 029import static org.fcrepo.kernel.api.FedoraTypes.MEMENTO; 030import static org.fcrepo.kernel.api.FedoraTypes.MEMENTO_DATETIME; 031import static org.fcrepo.kernel.api.RdfLexicon.HAS_FIXITY_SERVICE; 032import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_CONTAINMENT; 033import static org.fcrepo.kernel.api.RequiredRdfContext.LDP_MEMBERSHIP; 034import static org.fcrepo.kernel.api.RequiredRdfContext.PROPERTIES; 035import static org.fcrepo.kernel.api.RequiredRdfContext.SERVER_MANAGED; 036import static org.fcrepo.kernel.modeshape.FedoraResourceImpl.LDPCV_TIME_MAP; 037import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession; 038import static org.fcrepo.kernel.modeshape.rdf.impl.RequiredPropertiesUtil.assertRequiredContainerTriples; 039import static org.fcrepo.kernel.modeshape.rdf.impl.RequiredPropertiesUtil.assertRequiredDescriptionTriples; 040import static org.fcrepo.kernel.modeshape.utils.FedoraTypesUtils.getJcrNode; 041import static org.modeshape.jcr.api.JcrConstants.NT_FOLDER; 042import static org.slf4j.LoggerFactory.getLogger; 043import static org.fcrepo.kernel.api.utils.SubjectMappingUtil.mapSubject; 044import java.io.InputStream; 045import java.net.URI; 046import java.time.Instant; 047import java.time.ZoneId; 048import java.time.ZonedDateTime; 049import java.util.Calendar; 050import java.util.Collection; 051import java.util.GregorianCalendar; 052import java.util.HashSet; 053import java.util.List; 054import java.util.Set; 055import java.util.stream.Stream; 056import java.util.stream.Collectors; 057 058import javax.inject.Inject; 059import javax.jcr.ItemExistsException; 060import javax.jcr.Node; 061import javax.jcr.Property; 062import javax.jcr.RepositoryException; 063import javax.jcr.Session; 064 065import org.apache.jena.graph.Triple; 066import org.apache.jena.rdf.model.Model; 067import org.apache.jena.rdf.model.ModelFactory; 068import org.apache.jena.rdf.model.Resource; 069import org.apache.jena.riot.Lang; 070import org.fcrepo.kernel.api.FedoraSession; 071import org.fcrepo.kernel.api.RdfStream; 072import org.fcrepo.kernel.api.TripleCategory; 073import org.fcrepo.kernel.api.exception.ConstraintViolationException; 074import org.fcrepo.kernel.api.exception.InvalidChecksumException; 075import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; 076import org.fcrepo.kernel.api.identifiers.IdentifierConverter; 077import org.fcrepo.kernel.api.models.Container; 078import org.fcrepo.kernel.api.models.FedoraBinary; 079import org.fcrepo.kernel.api.models.FedoraResource; 080import org.fcrepo.kernel.api.rdf.DefaultRdfStream; 081import org.fcrepo.kernel.api.rdf.RdfNamespaceRegistry; 082import org.fcrepo.kernel.api.services.BinaryService; 083import org.fcrepo.kernel.api.services.VersionService; 084import org.fcrepo.kernel.api.services.policy.StoragePolicyDecisionPoint; 085import org.fcrepo.kernel.modeshape.ContainerImpl; 086import org.fcrepo.kernel.modeshape.rdf.impl.InternalIdentifierTranslator; 087import org.fcrepo.kernel.modeshape.utils.iterators.RelaxedRdfAdder; 088import org.slf4j.Logger; 089import org.springframework.stereotype.Component; 090 091import com.google.common.annotations.VisibleForTesting; 092 093/** 094 * This service exposes management of node versioning for resources and binaries. 095 * 096 * @author Mike Durbin 097 * @author bbpennel 098 */ 099 100@Component 101public class VersionServiceImpl extends AbstractService implements VersionService { 102 103 private static final Logger LOGGER = getLogger(VersionService.class); 104 105 @VisibleForTesting 106 public static final Set<TripleCategory> VERSION_TRIPLES = new HashSet<>(asList( 107 PROPERTIES, SERVER_MANAGED, LDP_MEMBERSHIP, LDP_CONTAINMENT)); 108 109 /** 110 * The bitstream service 111 */ 112 @Inject 113 private BinaryService binaryService; 114 115 @Inject 116 private RdfNamespaceRegistry namespaceRegistry; 117 118 @Override 119 public FedoraResource createVersion(final FedoraSession session, final FedoraResource resource, 120 final IdentifierConverter<Resource, FedoraResource> idTranslator, final Instant dateTime) { 121 return createVersion(session, resource, idTranslator, dateTime, null, null); 122 } 123 124 @Override 125 public FedoraResource createVersion(final FedoraSession session, 126 final FedoraResource resource, 127 final IdentifierConverter<Resource, FedoraResource> idTranslator, 128 final Instant dateTime, 129 final InputStream rdfInputStream, 130 final Lang rdfFormat) { 131 132 final String mementoPath = makeMementoPath(resource, dateTime); 133 assertMementoDoesNotExist(session, mementoPath); 134 135 // Construct an unpopulated resource of the appropriate type for new memento 136 final FedoraResource mementoResource; 137 if (resource instanceof Container) { 138 mementoResource = createContainer(session, mementoPath); 139 } else { 140 mementoResource = binaryService.findOrCreateDescription(session, mementoPath); 141 } 142 143 final String mementoUri = getUri(mementoResource, idTranslator); 144 final String resourceUri = getUri(resource.getDescribedResource(), idTranslator); 145 146 final RdfStream mementoRdfStream; 147 if (rdfInputStream == null) { 148 // With no rdf body provided, create version from current resource state. 149 mementoRdfStream = resource.getTriples(idTranslator, VERSION_TRIPLES); 150 } else { 151 final Model inputModel = ModelFactory.createDefaultModel(); 152 inputModel.read(rdfInputStream, mementoUri, rdfFormat.getName()); 153 154 if (inputModel.isEmpty()) { 155 throw new ConstraintViolationException( 156 "Cannot create historic memento from an empty body"); 157 } 158 159 // Validate server managed triples are provided 160 if (resource instanceof Container) { 161 assertRequiredContainerTriples(inputModel); 162 } else { 163 assertRequiredDescriptionTriples(inputModel); 164 165 // Remove fixity service reference due to disallowed fcr prefix 166 inputModel.removeAll(null, HAS_FIXITY_SERVICE, null); 167 } 168 169 mementoRdfStream = DefaultRdfStream.fromModel(createURI(mementoUri), inputModel); 170 } 171 172 final Session jcrSession = getJcrSession(session); 173 final RdfStream mappedStream = remapResourceUris(resourceUri, mementoUri, mementoRdfStream, 174 idTranslator, jcrSession); 175 176 new RelaxedRdfAdder(idTranslator, jcrSession, mappedStream, namespaceRegistry.getNamespaces()).consume(); 177 178 decorateWithMementoProperties(session, mementoPath, dateTime); 179 180 return mementoResource; 181 } 182 183 /* 184 * Creates a minimal container node for further population elsewhere 185 */ 186 private Container createContainer(final FedoraSession session, final String path) { 187 try { 188 final Node node = findOrCreateNode(session, path, NT_FOLDER); 189 190 if (node.canAddMixin(FEDORA_RESOURCE)) { 191 node.addMixin(FEDORA_RESOURCE); 192 node.addMixin(FEDORA_CONTAINER); 193 } 194 195 return new ContainerImpl(node); 196 } catch (final RepositoryException e) { 197 throw new RepositoryRuntimeException(e); 198 } 199 } 200 201 /** 202 * Remaps the subjects of triples in rdfStream from the original resource URL to the URL of the new memento, and 203 * converts objects which reference resources to an internal identifier to prevent enforcement of referential 204 * integrity constraints. 205 * 206 * @param resourceUri uri of the original resource 207 * @param mementoUri uri of the memento resource 208 * @param rdfStream rdf stream 209 * @param idTranslator translator for producing URI of resources 210 * @param jcrSession jcr session 211 * @return RdfStream 212 */ 213 private RdfStream remapResourceUris(final String resourceUri, 214 final String mementoUri, 215 final RdfStream rdfStream, 216 final IdentifierConverter<Resource, FedoraResource> idTranslator, 217 final Session jcrSession) { 218 final IdentifierConverter<Resource, FedoraResource> internalIdTranslator = new InternalIdentifierTranslator( 219 jcrSession); 220 221 final org.apache.jena.graph.Node mementoNode = createURI(mementoUri); 222 final Stream<Triple> mappedStream = rdfStream.map(t -> mapSubject(t, resourceUri, mementoUri)) 223 .map(t -> convertToInternalReference(t, idTranslator, internalIdTranslator)); 224 return new DefaultRdfStream(mementoNode, mappedStream); 225 } 226 227 /** 228 * Convert the referencing resource uri to un-dereferenceable internal identifier. 229 * 230 * @param t the Triple to convert 231 * @param idTranslator the Converter that convert the resource uri to a path 232 * @param internalIdTranslator the Converter that convert a path to internal identifier 233 * @return Triple a triple with referencing resource uri converted to internal identifier 234 */ 235 private Triple convertToInternalReference(final Triple t, 236 final IdentifierConverter<Resource, FedoraResource> idTranslator, 237 final IdentifierConverter<Resource, FedoraResource> internalIdTranslator) { 238 if (t.getObject().isURI()) { 239 final Resource object = createResource(t.getObject().getURI()); 240 if (idTranslator.inDomain(object)) { 241 final String path = idTranslator.convert(object).getPath(); 242 final Resource obj = createResource(internalIdTranslator.toDomain(path).getURI()); 243 LOGGER.debug("Converting referencing resource uri {} to internal identifier {}.", 244 t.getObject().getURI(), obj.getURI()); 245 246 return new Triple(t.getSubject(), t.getPredicate(), obj.asNode()); 247 } 248 } 249 250 return t; 251 } 252 253 @Override 254 public FedoraBinary createExternalBinaryVersion(final FedoraSession session, 255 final FedoraBinary resource, 256 final Instant dateTime, 257 final Collection<URI> checksums, 258 final String externalHandling, 259 final String externalUrl) 260 throws InvalidChecksumException { 261 final String mementoPath = makeMementoPath(resource, dateTime); 262 assertMementoDoesNotExist(session, mementoPath); 263 264 final FedoraBinary memento = binaryService.findOrCreateBinary(session, mementoPath); 265 decorateWithMementoProperties(session, mementoPath, dateTime); 266 267 memento.setExternalContent(null, checksums, null, externalHandling, externalUrl); 268 269 return memento; 270 } 271 272 @Override 273 public FedoraBinary createBinaryVersion(final FedoraSession session, 274 final FedoraBinary resource, 275 final Instant dateTime, 276 final StoragePolicyDecisionPoint storagePolicyDecisionPoint) 277 throws InvalidChecksumException { 278 return createBinaryVersion(session, resource, dateTime, null, null, storagePolicyDecisionPoint); 279 } 280 281 @Override 282 public FedoraBinary createBinaryVersion(final FedoraSession session, 283 final FedoraBinary resource, 284 final Instant dateTime, 285 final InputStream contentStream, 286 final Collection<URI> checksums, 287 final StoragePolicyDecisionPoint storagePolicyDecisionPoint) throws InvalidChecksumException { 288 289 final String mementoPath = makeMementoPath(resource, dateTime); 290 assertMementoDoesNotExist(session, mementoPath); 291 292 final FedoraBinary memento = binaryService.findOrCreateBinary(session, mementoPath); 293 decorateWithMementoProperties(session, mementoPath, dateTime); 294 295 if (contentStream == null) { 296 // Creating memento from existing resource 297 populateBinaryMementoFromExisting(resource, memento, storagePolicyDecisionPoint); 298 } else { 299 memento.setContent(contentStream, null, checksums, null, storagePolicyDecisionPoint); 300 } 301 302 return memento; 303 } 304 305 private void populateBinaryMementoFromExisting(final FedoraBinary resource, final FedoraBinary memento, 306 final StoragePolicyDecisionPoint storagePolicyDecisionPoint) throws InvalidChecksumException { 307 308 final Node contentNode = getJcrNode(resource); 309 List<URI> checksums = null; 310 // Retrieve all existing digests from the original 311 try { 312 if (contentNode.hasProperty(CONTENT_DIGEST)) { 313 final Property digestProperty = contentNode.getProperty(CONTENT_DIGEST); 314 checksums = stream(digestProperty.getValues()) 315 .map(d -> { 316 try { 317 return URI.create(d.getString()); 318 } catch (final RepositoryException e) { 319 throw new RepositoryRuntimeException(e); 320 } 321 }).collect(Collectors.toList()); 322 } 323 324 // if current binary is external, gather details 325 String handling = null; 326 String externalUrl = null; 327 if (resource.isProxy()) { 328 handling = PROXY; 329 externalUrl = resource.getProxyURL(); 330 } else if (resource.isRedirect()) { 331 handling = REDIRECT; 332 externalUrl = resource.getRedirectURL(); 333 } 334 335 // Create memento as external or internal based on state of original 336 if (handling != null && externalUrl != null) { 337 memento.setExternalContent(null, checksums, null, handling, externalUrl); 338 } else { 339 memento.setContent(resource.getContent(), null, checksums, 340 null, storagePolicyDecisionPoint); 341 } 342 } catch (final RepositoryException e) { 343 throw new RepositoryRuntimeException(e); 344 } 345 } 346 347 private String makeMementoPath(final FedoraResource resource, final Instant datetime) { 348 return resource.getPath() + "/" + LDPCV_TIME_MAP + "/" + MEMENTO_LABEL_FORMATTER.format(datetime); 349 } 350 351 private String getUri(final FedoraResource resource, 352 final IdentifierConverter<Resource, FedoraResource> idTranslator) { 353 if (idTranslator == null) { 354 return resource.getPath(); 355 } 356 return idTranslator.reverse().convert(resource).getURI(); 357 } 358 359 private void decorateWithMementoProperties(final FedoraSession session, final String mementoPath, 360 final Instant dateTime) { 361 try { 362 final Node mementoNode = findNode(session, mementoPath); 363 if (mementoNode.canAddMixin(MEMENTO)) { 364 mementoNode.addMixin(MEMENTO); 365 } 366 final Calendar mementoDatetime = GregorianCalendar.from( 367 ZonedDateTime.ofInstant(dateTime, ZoneId.of("UTC"))); 368 mementoNode.setProperty(MEMENTO_DATETIME, mementoDatetime); 369 } catch (final RepositoryException e) { 370 throw new RepositoryRuntimeException(e); 371 } 372 } 373 374 private void assertMementoDoesNotExist(final FedoraSession session, final String mementoPath) { 375 if (exists(session, mementoPath)) { 376 throw new RepositoryRuntimeException(new ItemExistsException( 377 "Memento " + mementoPath + " already exists")); 378 } 379 } 380}