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}