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}