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.models;
007
008import static org.apache.jena.graph.NodeFactory.createURI;
009import static org.apache.jena.vocabulary.RDF.type;
010import static org.fcrepo.kernel.api.RdfLexicon.ARCHIVAL_GROUP;
011import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_RESOURCE;
012import static org.fcrepo.kernel.api.RdfLexicon.MEMENTO_TYPE;
013import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_ROOT;
014import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE;
015import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE;
016import static org.fcrepo.kernel.api.RdfLexicon.VERSIONING_TIMEGATE_TYPE;
017
018import static java.net.URI.create;
019import static java.util.stream.Collectors.toList;
020
021import java.net.URI;
022import java.nio.file.Path;
023import java.time.Duration;
024import java.time.Instant;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Optional;
029import java.util.stream.Stream;
030
031import org.fcrepo.kernel.api.RdfStream;
032import org.fcrepo.kernel.api.Transaction;
033import org.fcrepo.kernel.api.cache.UserTypesCache;
034import org.fcrepo.kernel.api.exception.ItemNotFoundException;
035import org.fcrepo.kernel.api.exception.PathNotFoundException;
036import org.fcrepo.kernel.api.exception.PathNotFoundRuntimeException;
037import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
038import org.fcrepo.kernel.api.identifiers.FedoraId;
039import org.fcrepo.kernel.api.models.FedoraResource;
040import org.fcrepo.kernel.api.models.ResourceFactory;
041import org.fcrepo.kernel.api.models.TimeMap;
042import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
043import org.fcrepo.persistence.api.PersistentStorageSession;
044import org.fcrepo.persistence.api.PersistentStorageSessionManager;
045import org.fcrepo.persistence.api.exceptions.PersistentItemNotFoundException;
046import org.fcrepo.persistence.api.exceptions.PersistentStorageException;
047
048import org.apache.jena.graph.Triple;
049
050/**
051 * Implementation of a Fedora resource, containing functionality common to the more concrete resource implementations.
052 *
053 * @author bbpennel
054 */
055public class FedoraResourceImpl implements FedoraResource {
056
057    private static final URI RESOURCE_URI = create(RESOURCE.toString());
058    private static final URI FEDORA_RESOURCE_URI = create(FEDORA_RESOURCE.getURI());
059    private static final URI ARCHIVAL_GROUP_URI = create(ARCHIVAL_GROUP.getURI());
060    private static final URI MEMENTO_URI = create(MEMENTO_TYPE);
061    private static final URI VERSIONED_RESOURCE_URI = create(VERSIONED_RESOURCE.getURI());
062    private static final URI VERSIONING_TIMEGATE_URI = create(VERSIONING_TIMEGATE_TYPE);
063    private static final URI REPOSITORY_ROOT_URI = create(REPOSITORY_ROOT.getURI());
064
065    private final PersistentStorageSessionManager pSessionManager;
066
067    protected final ResourceFactory resourceFactory;
068
069    private final UserTypesCache userTypesCache;
070
071    protected final FedoraId fedoraId;
072
073    private FedoraId parentId;
074
075    private FedoraId archivalGroupId;
076
077    private List<URI> types;
078
079    private List<URI> systemTypes;
080
081    private List<URI> systemTypesForRdf;
082
083    private List<URI> userTypes;
084
085    private Instant lastModifiedDate;
086
087    private String lastModifiedBy;
088
089    private Instant createdDate;
090
091    private String createdBy;
092
093    private Instant mementoDatetime;
094
095    private String stateToken;
096
097    private String etag;
098
099    private boolean isMemento;
100
101    private String interactionModel;
102
103    // The transaction this representation of the resource belongs to
104    protected final Transaction transaction;
105
106    private boolean isArchivalGroup;
107
108    private Path storageRelativePath;
109
110    protected FedoraResourceImpl(final FedoraId fedoraId,
111                                 final Transaction transaction,
112                                 final PersistentStorageSessionManager pSessionManager,
113                                 final ResourceFactory resourceFactory,
114                                 final UserTypesCache userTypesCache) {
115        this.fedoraId = fedoraId;
116        this.transaction = transaction;
117        this.pSessionManager = pSessionManager;
118        this.resourceFactory = resourceFactory;
119        this.userTypesCache = userTypesCache;
120    }
121
122    @Override
123    public String getId() {
124        return this.fedoraId.getResourceId();
125    }
126
127    @Override
128    public Stream<FedoraResource> getChildren(final Boolean recursive) {
129        return Stream.empty();
130    }
131
132    @Override
133    public FedoraResource getContainer() {
134        return resourceFactory.getContainer(transaction, fedoraId);
135    }
136
137    @Override
138    public FedoraResource getOriginalResource() {
139        if (isMemento()) {
140            try {
141                // We are in a memento so we need to create a FedoraId for just the original resource.
142                final var fedoraId = FedoraId.create(getFedoraId().getResourceId());
143                return getFedoraResource(fedoraId);
144            } catch (final PathNotFoundException e) {
145                throw new PathNotFoundRuntimeException(e.getMessage(), e);
146            }
147        }
148        return this;
149    }
150
151    private FedoraResource getFedoraResource(final FedoraId fedoraId) throws PathNotFoundException {
152        return resourceFactory.getResource(transaction, fedoraId);
153    }
154
155    @Override
156    public TimeMap getTimeMap() {
157        return new TimeMapImpl(this.getOriginalResource(), transaction, pSessionManager, resourceFactory);
158    }
159
160    @Override
161    public Instant getMementoDatetime() {
162        return mementoDatetime;
163    }
164
165    @Override
166    public boolean isMemento() {
167        return isMemento;
168    }
169
170    @Override
171    public boolean isAcl() {
172        return false;
173    }
174
175    @Override
176    public FedoraResource findMementoByDatetime(final Instant mementoDatetime) {
177        FedoraResource match = null;
178        long matchDiff = 0;
179
180        for (final var it = getTimeMap().getChildren().filter(FedoraResource::isMemento).iterator(); it.hasNext();) {
181            final var current = it.next();
182            // Negative if the memento is AFTER the requested datetime
183            // Positive if the memento is BEFORE the requested datetime
184            final var diff = Duration.between(current.getMementoDatetime(), mementoDatetime).toSeconds();
185
186            if (match == null                               // Save the first memento examined
187                    || (matchDiff < 0 && diff >= matchDiff) // Match is AFTER requested && current is closer
188                    || (diff >= 0 && diff <= matchDiff)) {  // Current memento EQUAL/BEFORE request && closer than match
189                match = current;
190                matchDiff = diff;
191            }
192        }
193
194        return match;
195    }
196
197    @Override
198    public FedoraResource getAcl() {
199        if (isAcl()) {
200            return this;
201        }
202        try {
203            final var aclId = fedoraId.asAcl();
204            return getFedoraResource(aclId);
205        } catch (final PathNotFoundException e) {
206            return null;
207        }
208    }
209
210    @Override
211    public boolean hasProperty(final String relPath) {
212        // TODO Auto-generated method stub
213        return false;
214    }
215
216    @Override
217    public Instant getCreatedDate() {
218        return createdDate;
219    }
220
221    @Override
222    public Instant getLastModifiedDate() {
223        return lastModifiedDate;
224    }
225
226    @Override
227    public boolean hasType(final String type) {
228        return getTypes().contains(create(type));
229    }
230
231    @Override
232    public List<URI> getTypes() {
233        if (types == null) {
234            types = new ArrayList<>();
235            types.addAll(getSystemTypes(false));
236            types.addAll(getUserTypes());
237        }
238        return types;
239    }
240
241    @Override
242    public List<URI> getSystemTypes(final boolean forRdf) {
243        var types = resolveSystemTypes(forRdf);
244
245        if (types == null) {
246            types = new ArrayList<>();
247            types.add(create(interactionModel));
248            // ldp:Resource is on all resources
249            types.add(RESOURCE_URI);
250            types.add(FEDORA_RESOURCE_URI);
251            if (getFedoraId().isRepositoryRoot()) {
252                types.add(REPOSITORY_ROOT_URI);
253            }
254            if (!forRdf) {
255                // These types are not exposed as RDF triples.
256                if (isArchivalGroup) {
257                    types.add(ARCHIVAL_GROUP_URI);
258                }
259                if (isMemento) {
260                    types.add(MEMENTO_URI);
261                } else {
262                    types.add(VERSIONED_RESOURCE_URI);
263                    types.add(VERSIONING_TIMEGATE_URI);
264                }
265            }
266
267            if (forRdf) {
268                systemTypesForRdf = types;
269            } else {
270                systemTypes = types;
271            }
272        }
273
274        return types;
275    }
276
277    @Override
278    public List<URI> getUserTypes() {
279        if (userTypes == null) {
280            try {
281                final var description = getDescription();
282                final var descId = description.getFedoraId().asResourceId();
283
284                // Memento types are not cached
285                if (description.getMementoDatetime() == null && userTypesCache != null) {
286                    userTypes = userTypesCache.getUserTypes(descId,
287                            getSession().getId(), () -> getSession().getTriples(descId, null));
288                } else {
289                    final var triples = getSession().getTriples(descId, description.getMementoDatetime());
290                    userTypes = triples.filter(t -> t.predicateMatches(type.asNode()))
291                            .map(Triple::getObject)
292                            .map(t -> URI.create(t.toString()))
293                            .collect(toList());
294                }
295            } catch (final PersistentItemNotFoundException e) {
296                final var headers = getSession().getHeaders(getFedoraId().asResourceId(), getMementoDatetime());
297                if (headers.isDeleted()) {
298                    userTypes = Collections.emptyList();
299                } else {
300                    throw new ItemNotFoundException("Unable to retrieve triples for " + getId(), e);
301                }
302            } catch (final PersistentStorageException e) {
303                throw new RepositoryRuntimeException(e.getMessage(), e);
304            }
305        }
306
307        return userTypes;
308    }
309
310    @Override
311    public RdfStream getTriples() {
312        try {
313            final var subject = createURI(getId());
314            final var triples = getSession().getTriples(getFedoraId().asResourceId(), getMementoDatetime());
315
316            return new DefaultRdfStream(subject, triples);
317        } catch (final PersistentItemNotFoundException e) {
318            throw new ItemNotFoundException("Unable to retrieve triples for " + getId(), e);
319        } catch (final PersistentStorageException e) {
320            throw new RepositoryRuntimeException(e.getMessage(), e);
321        }
322    }
323
324    @Override
325    public String getEtagValue() {
326        return etag;
327    }
328
329    @Override
330    public String getStateToken() {
331        // TODO Auto-generated method stub
332        return stateToken;
333    }
334
335    @Override
336    public boolean isOriginalResource() {
337        return !isMemento();
338    }
339
340    @Override
341    public FedoraResource getDescription() {
342        return this;
343    }
344
345    @Override
346    public FedoraResource getDescribedResource() {
347        return this;
348    }
349
350    protected PersistentStorageSession getSession() {
351        if (transaction.isOpen()) {
352            return pSessionManager.getSession(transaction);
353        } else {
354            return pSessionManager.getReadOnlySession();
355        }
356    }
357
358    @Override
359    public FedoraResource getParent() throws PathNotFoundException {
360        return resourceFactory.getResource(transaction, parentId);
361    }
362
363    @Override
364    public FedoraId getParentId() {
365        return parentId;
366    }
367
368    @Override
369    public String getCreatedBy() {
370        return createdBy;
371    }
372
373    @Override
374    public String getLastModifiedBy() {
375        return lastModifiedBy;
376    }
377
378    @Override
379    public FedoraId getFedoraId() {
380        return this.fedoraId;
381    }
382
383    @Override
384    public String getInteractionModel() {
385        return this.interactionModel;
386    }
387
388    @Override
389    public Path getStorageRelativePath() {
390        return this.storageRelativePath;
391    }
392
393    /**
394     * @param parentId the parentId to set
395     */
396    protected void setParentId(final FedoraId parentId) {
397        this.parentId = parentId;
398    }
399
400    /**
401     * @param types the types to set
402     */
403    protected void setTypes(final List<URI> types) {
404        this.types = types;
405    }
406
407    /**
408     * @param lastModifiedDate the lastModifiedDate to set
409     */
410    protected void setLastModifiedDate(final Instant lastModifiedDate) {
411        this.lastModifiedDate = lastModifiedDate;
412    }
413
414    /**
415     * @param lastModifiedBy the lastModifiedBy to set
416     */
417    protected void setLastModifiedBy(final String lastModifiedBy) {
418        this.lastModifiedBy = lastModifiedBy;
419    }
420
421    /**
422     * @param createdDate the createdDate to set
423     */
424    protected void setCreatedDate(final Instant createdDate) {
425        this.createdDate = createdDate;
426    }
427
428    /**
429     * @param createdBy the createdBy to set
430     */
431    protected void setCreatedBy(final String createdBy) {
432        this.createdBy = createdBy;
433    }
434
435    /**
436     * @param mementoDatetime the mementoDatetime to set
437     */
438    protected void setMementoDatetime(final Instant mementoDatetime) {
439        this.mementoDatetime = mementoDatetime;
440    }
441
442    /**
443     * @param stateToken the stateToken to set
444     */
445    protected void setStateToken(final String stateToken) {
446        this.stateToken = stateToken;
447    }
448
449    /**
450     * @param etag the etag to set
451     */
452    protected void setEtag(final String etag) {
453        this.etag = etag;
454    }
455
456    /**
457     * @param isMemento indicates if the resource is a memento
458     */
459    public void setIsMemento(final boolean isMemento) {
460        this.isMemento = isMemento;
461    }
462
463    /**
464     * @param isArchivalGroup true if the resource is an AG
465     */
466    public void setIsArchivalGroup(final boolean isArchivalGroup) {
467        this.isArchivalGroup = isArchivalGroup;
468    }
469
470    /**
471     * @param interactionModel the resource's interaction model
472     */
473    public void setInteractionModel(final String interactionModel) {
474        this.interactionModel = interactionModel;
475    }
476
477    protected List<URI> resolveSystemTypes(final boolean forRdf) {
478        return forRdf ? systemTypesForRdf : systemTypes;
479    }
480
481    /**
482     * @param archivalGroupId the FedoraId of the Archival Group for this resource
483     */
484    public void setArchivalGroupId(final FedoraId archivalGroupId) {
485        this.archivalGroupId = archivalGroupId;
486    }
487
488    public Optional<FedoraId> getArchivalGroupId() {
489        return Optional.ofNullable(archivalGroupId);
490    }
491
492    /**
493     * @param storageRelativePath the path to the content
494     */
495    protected void setStorageRelativePath(final String storageRelativePath) {
496        if (storageRelativePath != null) {
497            final var tempPath = Path.of(storageRelativePath);
498            // This returns the path to the header file in the .fcrepo content directory
499            // i.e. /path/to/resource/v1/content/.fcrepo/fcr-root.json
500            // So we move up 4 directories (names) to get to the resource directory.
501            this.storageRelativePath = tempPath.subpath(0, tempPath.getNameCount() - 4);
502        }
503    }
504}