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