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