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