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 String getPath() {
127        // TODO Auto-generated method stub
128        return null;
129    }
130
131    @Override
132    public Stream<FedoraResource> getChildren(final Boolean recursive) {
133        return Stream.empty();
134    }
135
136    @Override
137    public FedoraResource getContainer() {
138        return resourceFactory.getContainer(txId, fedoraId);
139    }
140
141    @Override
142    public FedoraResource getOriginalResource() {
143        if (isMemento()) {
144            try {
145                // We are in a memento so we need to create a FedoraId for just the original resource.
146                final var fedoraId = FedoraId.create(getFedoraId().getResourceId());
147                return getFedoraResource(fedoraId);
148            } catch (final PathNotFoundException e) {
149                throw new PathNotFoundRuntimeException(e.getMessage(), e);
150            }
151        }
152        return this;
153    }
154
155    private FedoraResource getFedoraResource(final FedoraId fedoraId) throws PathNotFoundException {
156        return resourceFactory.getResource(txId, fedoraId);
157    }
158
159    @Override
160    public TimeMap getTimeMap() {
161        return new TimeMapImpl(this.getOriginalResource(), txId, pSessionManager, resourceFactory);
162    }
163
164    @Override
165    public Instant getMementoDatetime() {
166        return mementoDatetime;
167    }
168
169    @Override
170    public boolean isMemento() {
171        return isMemento;
172    }
173
174    @Override
175    public boolean isAcl() {
176        return false;
177    }
178
179    @Override
180    public FedoraResource findMementoByDatetime(final Instant mementoDatetime) {
181        FedoraResource match = null;
182        long matchDiff = 0;
183
184        for (final var it = getTimeMap().getChildren().iterator(); it.hasNext();) {
185            final var current = it.next();
186            // Negative if the memento is AFTER the requested datetime
187            // Positive if the memento is BEFORE the requested datetime
188            final var diff = Duration.between(current.getMementoDatetime(), mementoDatetime).toSeconds();
189
190            if (match == null                               // Save the first memento examined
191                    || (matchDiff < 0 && diff >= matchDiff) // Match is AFTER requested && current is closer
192                    || (diff >= 0 && diff <= matchDiff)) {  // Current memento EQUAL/BEFORE request && closer than match
193                match = current;
194                matchDiff = diff;
195            }
196        }
197
198        return match;
199    }
200
201    @Override
202    public FedoraResource getAcl() {
203        if (isAcl()) {
204            return this;
205        }
206        try {
207            final var aclId = fedoraId.asAcl();
208            return getFedoraResource(aclId);
209        } catch (final PathNotFoundException e) {
210            return null;
211        }
212    }
213
214    @Override
215    public boolean hasProperty(final String relPath) {
216        // TODO Auto-generated method stub
217        return false;
218    }
219
220    @Override
221    public Instant getCreatedDate() {
222        return createdDate;
223    }
224
225    @Override
226    public Instant getLastModifiedDate() {
227        return lastModifiedDate;
228    }
229
230    @Override
231    public boolean hasType(final String type) {
232        return getTypes().contains(create(type));
233    }
234
235    @Override
236    public List<URI> getTypes() {
237        if (types == null) {
238            types = new ArrayList<>();
239            types.addAll(getSystemTypes(false));
240            types.addAll(getUserTypes());
241        }
242        return types;
243    }
244
245    @Override
246    public List<URI> getSystemTypes(final boolean forRdf) {
247        var types = resolveSystemTypes(forRdf);
248
249        if (types == null) {
250            types = new ArrayList<>();
251            types.add(create(interactionModel));
252            // ldp:Resource is on all resources
253            types.add(RESOURCE_URI);
254            types.add(FEDORA_RESOURCE_URI);
255            if (getFedoraId().isRepositoryRoot()) {
256                types.add(REPOSITORY_ROOT_URI);
257            }
258            if (!forRdf) {
259                // These types are not exposed as RDF triples.
260                if (isArchivalGroup) {
261                    types.add(ARCHIVAL_GROUP_URI);
262                }
263                if (isMemento) {
264                    types.add(MEMENTO_URI);
265                } else {
266                    types.add(VERSIONED_RESOURCE_URI);
267                    types.add(VERSIONING_TIMEGATE_URI);
268                }
269            }
270
271            if (forRdf) {
272                systemTypesForRdf = types;
273            } else {
274                systemTypes = types;
275            }
276        }
277
278        return types;
279    }
280
281    @Override
282    public List<URI> getUserTypes() {
283        if (userTypes == null) {
284            userTypes = new ArrayList<>();
285            try {
286                final var description = getDescription();
287                final var triples = getSession().getTriples(description.getFedoraId().asResourceId(),
288                        description.getMementoDatetime());
289                userTypes = triples.filter(t -> t.predicateMatches(type.asNode())).map(Triple::getObject)
290                        .map(t -> URI.create(t.toString())).collect(toList());
291            } catch (final PersistentItemNotFoundException e) {
292                final var headers = getSession().getHeaders(getFedoraId().asResourceId(), getMementoDatetime());
293                if (headers.isDeleted()) {
294                    userTypes = Collections.emptyList();
295                } else {
296                    throw new ItemNotFoundException("Unable to retrieve triples for " + getId(), e);
297                }
298            } catch (final PersistentStorageException e) {
299                throw new RepositoryRuntimeException(e.getMessage(), e);
300            }
301        }
302
303        return userTypes;
304    }
305
306    @Override
307    public RdfStream getTriples() {
308        try {
309            final var subject = createURI(getId());
310            final var triples = getSession().getTriples(getFedoraId().asResourceId(), getMementoDatetime());
311
312            return new DefaultRdfStream(subject, triples);
313        } catch (final PersistentItemNotFoundException e) {
314            throw new ItemNotFoundException("Unable to retrieve triples for " + getId(), e);
315        } catch (final PersistentStorageException e) {
316            throw new RepositoryRuntimeException(e.getMessage(), e);
317        }
318    }
319
320    @Override
321    public Boolean isNew() {
322        // TODO Auto-generated method stub
323        return null;
324    }
325
326    @Override
327    public String getEtagValue() {
328        return etag;
329    }
330
331    @Override
332    public String getStateToken() {
333        // TODO Auto-generated method stub
334        return stateToken;
335    }
336
337    @Override
338    public boolean isOriginalResource() {
339        return !isMemento();
340    }
341
342    @Override
343    public FedoraResource getDescription() {
344        return this;
345    }
346
347    @Override
348    public FedoraResource getDescribedResource() {
349        return this;
350    }
351
352    protected PersistentStorageSession getSession() {
353        if (txId == null) {
354            return pSessionManager.getReadOnlySession();
355        } else {
356            return pSessionManager.getSession(txId);
357        }
358    }
359
360    @Override
361    public FedoraResource getParent() throws PathNotFoundException {
362        return resourceFactory.getResource(txId, parentId);
363    }
364
365    @Override
366    public String getCreatedBy() {
367        return createdBy;
368    }
369
370    @Override
371    public String getLastModifiedBy() {
372        return lastModifiedBy;
373    }
374
375    @Override
376    public FedoraId getFedoraId() {
377        return this.fedoraId;
378    }
379
380    @Override
381    public String getInteractionModel() {
382        return this.interactionModel;
383    }
384
385    /**
386     * @param parentId the parentId to set
387     */
388    protected void setParentId(final FedoraId parentId) {
389        this.parentId = parentId;
390    }
391
392    /**
393     * @param types the types to set
394     */
395    protected void setTypes(final List<URI> types) {
396        this.types = types;
397    }
398
399    /**
400     * @param lastModifiedDate the lastModifiedDate to set
401     */
402    protected void setLastModifiedDate(final Instant lastModifiedDate) {
403        this.lastModifiedDate = lastModifiedDate;
404    }
405
406    /**
407     * @param lastModifiedBy the lastModifiedBy to set
408     */
409    protected void setLastModifiedBy(final String lastModifiedBy) {
410        this.lastModifiedBy = lastModifiedBy;
411    }
412
413    /**
414     * @param createdDate the createdDate to set
415     */
416    protected void setCreatedDate(final Instant createdDate) {
417        this.createdDate = createdDate;
418    }
419
420    /**
421     * @param createdBy the createdBy to set
422     */
423    protected void setCreatedBy(final String createdBy) {
424        this.createdBy = createdBy;
425    }
426
427    /**
428     * @param mementoDatetime the mementoDatetime to set
429     */
430    protected void setMementoDatetime(final Instant mementoDatetime) {
431        this.mementoDatetime = mementoDatetime;
432    }
433
434    /**
435     * @param stateToken the stateToken to set
436     */
437    protected void setStateToken(final String stateToken) {
438        this.stateToken = stateToken;
439    }
440
441    /**
442     * @param etag the etag to set
443     */
444    protected void setEtag(final String etag) {
445        this.etag = etag;
446    }
447
448    /**
449     * @param isMemento indicates if the resource is a memento
450     */
451    public void setIsMemento(final boolean isMemento) {
452        this.isMemento = isMemento;
453    }
454
455    /**
456     * @param isArchivalGroup true if the resource is an AG
457     */
458    public void setIsArchivalGroup(final boolean isArchivalGroup) {
459        this.isArchivalGroup = isArchivalGroup;
460    }
461
462    /**
463     * @param interactionModel the resource's interaction model
464     */
465    public void setInteractionModel(final String interactionModel) {
466        this.interactionModel = interactionModel;
467    }
468
469    protected List<URI> resolveSystemTypes(final boolean forRdf) {
470        return forRdf ? systemTypesForRdf : systemTypes;
471    }
472}