diff --git a/src/app/+lookup-by-id/lookup-by-id-routing.module.ts b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..012345e791e5af3f244ddfe6036e96e5a59f79c8 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-by-id-routing.module.ts @@ -0,0 +1,19 @@ +import { LookupGuard } from './lookup-guard'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: ':idType/:id', canActivate: [LookupGuard], component: ObjectNotFoundComponent } + ]) + ], + providers: [ + LookupGuard + ] +}) + +export class LookupRoutingModule { + +} diff --git a/src/app/+lookup-by-id/lookup-by-id.module.ts b/src/app/+lookup-by-id/lookup-by-id.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..4620f5782496b19a34d4b317d37a4b7fdbc89091 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-by-id.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { LookupRoutingModule } from './lookup-by-id-routing.module'; +import { ObjectNotFoundComponent } from './objectnotfound/objectnotfound.component'; +import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; + +@NgModule({ + imports: [ + LookupRoutingModule, + CommonModule, + SharedModule, + ], + declarations: [ + ObjectNotFoundComponent + ], + providers: [ + DsoDataRedirectService + ] +}) +export class LookupIdModule { + +} diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..61e3688ee2bd5b926a55101f4ed19f38bad54321 --- /dev/null +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -0,0 +1,52 @@ +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { DsoDataRedirectService } from '../core/data/dso-data-redirect.service'; +import { Injectable } from '@angular/core'; +import { IdentifierType } from '../core/index/index.reducer'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { RemoteData } from '../core/data/remote-data'; +import { FindByIDRequest } from '../core/data/request.models'; + +interface LookupParams { + type: IdentifierType; + id: string; +} + +@Injectable() +export class LookupGuard implements CanActivate { + constructor(private dsoService: DsoDataRedirectService, private router: Router) { + } + + canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> { + const params = this.getLookupParams(route); + return this.dsoService.findById(params.id, params.type).pipe( + map((response: RemoteData<FindByIDRequest>) => response.hasFailed) + ); + } + + private getLookupParams(route: ActivatedRouteSnapshot): LookupParams { + let type; + let id; + const idType = route.params.idType; + + // If the idType is not recognized, assume a legacy handle request (handle/prefix/id) + if (idType !== IdentifierType.HANDLE && idType !== IdentifierType.UUID) { + type = IdentifierType.HANDLE; + const prefix = route.params.idType; + const handleId = route.params.id; + id = `${prefix}%2F${handleId}`; + + } else if (route.params.idType === IdentifierType.HANDLE) { + type = IdentifierType.HANDLE; + id = route.params.id; + + } else { + type = IdentifierType.UUID; + id = route.params.id; + } + return { + type: type, + id: id + }; + } +} diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html new file mode 100644 index 0000000000000000000000000000000000000000..662d3cde52c58019ce397748a0b2f41cd7b3d850 --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.html @@ -0,0 +1,8 @@ +<div class="object-not-found container"> + <h1>{{"error.item" | translate}}</h1> + <h2><small><em>{{missingItem}}</em></small></h2> + <br /> + <p class="text-center"> + <a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a> + </p> +</div> diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.scss b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..01165751542db98bbb31da29a0d649addd4d9ec4 --- /dev/null +++ b/src/app/+lookup-by-id/objectnotfound/objectnotfound.component.ts @@ -0,0 +1,45 @@ + +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +/** + * This component representing the `PageNotFound` DSpace page. + */ +@Component({ + selector: 'ds-objnotfound', + styleUrls: ['./objectnotfound.component.scss'], + templateUrl: './objectnotfound.component.html', + changeDetection: ChangeDetectionStrategy.Default +}) +export class ObjectNotFoundComponent implements OnInit { + + idType: string; + + id: string; + + missingItem: string; + + /** + * Initialize instance variables + * + * @param {AuthService} authservice + * @param {ServerResponseService} responseService + */ + constructor(private route: ActivatedRoute) { + route.params.subscribe((params) => { + this.idType = params.idType; + this.id = params.id; + }) + } + + ngOnInit(): void { + if (this.idType.startsWith('handle')) { + this.missingItem = 'handle: ' + this.id; + } else if (this.idType.startsWith('uuid')) { + this.missingItem = 'uuid: ' + this.id; + } else { + this.missingItem = 'handle: ' + this.idType + '/' + this.id; + } + } + +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index e1ddc2b8895752bbe1002933ae58072677b068f3..5085633a5b02c4cd50b5b12ca5ec04178340d20a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -27,6 +27,8 @@ export function getAdminModulePath() { RouterModule.forRoot([ { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' }, + { path: 'id', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, + { path: 'handle', loadChildren: './+lookup-by-id/lookup-by-id.module#LookupIdModule' }, { path: COMMUNITY_MODULE_PATH, loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 916788df8cf3f08fa5ca3e4da0275bd79aca1ff3..3d8bf0ed430c85adbfc47aab5835144ebdcb4cb3 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -128,7 +128,7 @@ const EXPORTS = [ ...PROVIDERS ], declarations: [ - ...DECLARATIONS, + ...DECLARATIONS ], exports: [ ...EXPORTS diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index f41151fd90a9d6d11bc65cba612edb7bf815b2a3..afc040bf5973867ea5ac3865347f6b8911c12053 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -44,6 +44,7 @@ export abstract class TypedObject { */ export class CacheableObject extends TypedObject { uuid?: string; + handle?: string; self: string; // isNew: boolean; // dirtyType: DirtyType; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 11f3a6ce3e62b786acdfaf4dbe62b10835911834..95e96db0c88d629bc31368a4af0372a7d6be97d2 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -4,7 +4,7 @@ import { applyPatch, Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; import { RestRequestMethod } from '../data/rest-request-method'; @@ -21,6 +21,7 @@ import { import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; import { AddToSSBAction } from './server-sync-buffer.actions'; import { getMapsToType } from './builders/build-decorators'; +import { IdentifierType } from '../index/index.reducer'; /** * The base selector function to select the object cache in the store @@ -75,14 +76,15 @@ export class ObjectCacheService { /** * Get an observable of the object with the specified UUID * - * @param uuid + * @param id * The UUID of the object to get * @return Observable<NormalizedObject<T>> * An observable of the requested object in normalized form */ - getObjectByUUID<T extends CacheableObject>(uuid: string): Observable<NormalizedObject<T>> { + getObjectByID<T extends CacheableObject>(id: string, identifierType: IdentifierType = IdentifierType.UUID): + Observable<NormalizedObject<T>> { return this.store.pipe( - select(selfLinkFromUuidSelector(uuid)), + select(selfLinkFromUuidSelector(id, identifierType)), mergeMap((selfLink: string) => this.getObjectBySelfLink(selfLink) ) ) @@ -188,17 +190,17 @@ export class ObjectCacheService { /** * Check whether the object with the specified UUID is cached * - * @param uuid + * @param id * The UUID of the object to check * @return boolean * true if the object with the specified UUID is cached, * false otherwise */ - hasByUUID(uuid: string): boolean { + hasById(id: string, identifierType: IdentifierType = IdentifierType.UUID): boolean { let result: boolean; this.store.pipe( - select(selfLinkFromUuidSelector(uuid)), + select(selfLinkFromUuidSelector(id, identifierType)), take(1) ).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 0eabfc5dc82df7da1c861c3fe793b265275d0208..ae6f7f3cfaa50aba76bfdff56bca149615cb6b8c 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,5 +1,5 @@ import { ObjectCacheEffects } from './cache/object-cache.effects'; -import { UUIDIndexEffects } from './index/index.effects'; +import { IdentifierIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; import { AuthEffects } from './auth/auth.effects'; import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; @@ -10,7 +10,7 @@ import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, ObjectCacheEffects, - UUIDIndexEffects, + IdentifierIndexEffects, AuthEffects, JsonPatchOperationsEffects, ServerSyncBufferEffects, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5232b0bff25233b37862cd7388a77604457da02..de17d7a39f2151e1aeea0a733cabca6691f1cdb0 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -95,7 +95,7 @@ describe('ComColDataService', () => { function initMockObjectCacheService(): ObjectCacheService { return jasmine.createSpyObj('objectCache', { - getObjectByUUID: cold('d-', { + getObjectByID: cold('d-', { d: { _links: { [LINK_NAME]: scopedEndpoint @@ -160,7 +160,7 @@ describe('ComColDataService', () => { it('should fetch the scope Community from the cache', () => { scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe()); scheduler.flush(); - expect(objectCache.getObjectByUUID).toHaveBeenCalledWith(scopeID); + expect(objectCache.getObjectByID).toHaveBeenCalledWith(scopeID); }); it('should return the endpoint to fetch resources within the given scope', () => { diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 68eb3e488065b8852a7d607e2e5ea6acf8cea5c1..3059d568dfaff1c824f0cbcd48730aea799a7511 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -49,7 +49,7 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS ); const successResponses = responses.pipe( filter((response) => response.isSuccessful), - mergeMap(() => this.objectCache.getObjectByUUID(options.scopeID)), + mergeMap(() => this.objectCache.getObjectByID(options.scopeID)), map((nc: NormalizedCommunity) => nc._links[linkPath]), filter((href) => isNotEmpty(href)) ); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ad0db51980b2b804edd6f3ca4061da087bde8aff..0f7ca74d1526524716352724e9a09514c8c965e2 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -37,6 +37,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ChangeAnalyzer } from './change-analyzer'; import { RestRequestMethod } from './rest-request-method'; import { getMapsToType } from '../cache/builders/build-decorators'; +import { IdentifierType } from '../index/index.reducer'; export abstract class DataService<T extends CacheableObject> { protected abstract requestService: RequestService; @@ -146,14 +147,21 @@ export abstract class DataService<T extends CacheableObject> { return `${endpoint}/${resourceID}`; } - findById(id: string): Observable<RemoteData<T>> { - const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getIDHref(endpoint, id))); - + findById(id: string, identifierType: IdentifierType = IdentifierType.UUID): Observable<RemoteData<T>> { + let hrefObs; + if (identifierType === IdentifierType.UUID) { + hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIDHref(endpoint, id))); + } else if (identifierType === IdentifierType.HANDLE) { + hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => { + return this.getIDHref(endpoint, encodeURIComponent(id)); + })); + } hrefObs.pipe( find((href: string) => hasValue(href))) .subscribe((href: string) => { - const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); + const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id, identifierType); this.requestService.configure(request, this.forceBypassCache); }); diff --git a/src/app/core/data/dso-data-redirect.service.spec.ts b/src/app/core/data/dso-data-redirect.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ece3c242fc2722807b3f2aeb431dd851b46fdc03 --- /dev/null +++ b/src/app/core/data/dso-data-redirect.service.spec.ts @@ -0,0 +1,112 @@ +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { FindByIDRequest } from './request.models'; +import { RequestService } from './request.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { IdentifierType } from '../index/index.reducer'; +import { DsoDataRedirectService } from './dso-data-redirect.service'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; + +describe('DsoDataRedirectService', () => { + let scheduler: TestScheduler; + let service: DsoDataRedirectService; + let halService: HALEndpointService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let router: Router; + let remoteData; + const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746'; + const dsoHandle = '1234567789/22'; + const encodedHandle = encodeURIComponent(dsoHandle); + const pidLink = 'https://rest.api/rest/api/pid/find{?id}'; + const requestHandleURL = `https://rest.api/rest/api/pid/find?id=${encodedHandle}`; + const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`; + const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; + const testObject = { + uuid: dsoUUID + } as DSpaceObject; + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: pidLink }) + }); + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: cold('a', { + a: { + payload: testObject + } + }) + }); + router = jasmine.createSpyObj('router', { + navigate: () => true + }); + remoteData = { + isSuccessful: true, + error: undefined, + hasSucceeded: true, + payload: { + type: 'item', + id: '123456789' + } + }; + objectCache = {} as ObjectCacheService; + const store = {} as Store<CoreState>; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + const dataBuildService = {} as NormalizedObjectBuildService; + + service = new DsoDataRedirectService( + requestService, + rdbService, + dataBuildService, + store, + objectCache, + halService, + notificationsService, + http, + comparator, + router + ); + }); + + describe('findById', () => { + it('should call HALEndpointService with the path to the dso endpoint', () => { + scheduler.schedule(() => service.findById(dsoUUID)); + scheduler.flush(); + + expect(halService.getEndpoint).toHaveBeenCalledWith('pid'); + }); + + it('should configure the proper FindByIDRequest for uuid', () => { + scheduler.schedule(() => service.findById(dsoUUID, IdentifierType.UUID)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestUUIDURL, dsoUUID, IdentifierType.UUID), false); + }); + + it('should configure the proper FindByIDRequest for handle', () => { + scheduler.schedule(() => service.findById(dsoHandle, IdentifierType.HANDLE)); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestHandleURL, dsoHandle, IdentifierType.HANDLE), false); + }); + + // TODO: test for router.navigate + }); +}); diff --git a/src/app/core/data/dso-data-redirect.service.ts b/src/app/core/data/dso-data-redirect.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..b568a23c8ac4144a8647c58c818526acb348196f --- /dev/null +++ b/src/app/core/data/dso-data-redirect.service.ts @@ -0,0 +1,78 @@ +import { DataService } from './data.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HttpClient } from '@angular/common/http'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestService } from './request.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { FindAllOptions, FindByIDRequest } from './request.models'; +import { Observable, of } from 'rxjs'; +import { IdentifierType } from '../index/index.reducer'; +import { RemoteData } from './remote-data'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { Injectable } from '@angular/core'; +import { map, tap } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { getFinishedRemoteData, getSucceededRemoteData } from '../shared/operators'; +import { Router } from '@angular/router'; + +@Injectable() +export class DsoDataRedirectService extends DataService<any> { + + protected linkPath = 'pid'; + protected forceBypassCache = false; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store<CoreState>, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer<any>, + private router: Router) { + super(); + } + + getBrowseEndpoint(options: FindAllOptions = {}, linkPath: string = this.linkPath): Observable<string> { + return this.halService.getEndpoint(linkPath); + } + + getIDHref(endpoint, resourceID): string { + return endpoint.replace(/\{\?id\}/,`?id=${resourceID}`); + } + + findById(id: string, identifierType = IdentifierType.UUID): Observable<RemoteData<FindByIDRequest>> { + return super.findById(id, identifierType).pipe( + getFinishedRemoteData(), + tap((response) => { + if (response.hasSucceeded) { + const uuid = response.payload.uuid; + // Is there an existing method somewhere that converts dso type to endpoint? + // This will not work for all endpoints! + const dsoType = this.getEndpointFromDSOType(response.payload.type); + if (hasValue(uuid) && hasValue(dsoType)) { + this.router.navigate([dsoType + '/' + uuid]); + } + } + }) + ); + } + + getEndpointFromDSOType(dsoType: string): string { + if (dsoType.startsWith('item')) { + return 'items' + } else if (dsoType.startsWith('community')) { + return 'communities'; + } else if (dsoType.startsWith('collection')) { + return 'collections' + } else { + return ''; + } + } +} diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index a0bba214aeb57f27f856fbfb6c0c34c19f6a3d59..b411028420192fab88f4f6f2f9eedfaaaa4bd0fc 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -10,6 +10,7 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpClient } from '@angular/common/http'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { IdentifierType } from '../index/index.reducer'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -72,7 +73,7 @@ describe('DSpaceObjectDataService', () => { scheduler.schedule(() => service.findById(testObject.uuid)); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid), false); + expect(requestService.configure).toHaveBeenCalledWith(new FindByIDRequest(requestUUID, requestURL, testObject.uuid, IdentifierType.UUID), false); }); it('should return a RemoteData<DSpaceObject> for the object with the given ID', () => { diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index a2b34239609ee15ea33d0cdc00a485357047b2af..d3117d94b286a89bc1220ab3ebcb152a342e27ab 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service'; import { MetadatafieldParsingService } from './metadatafield-parsing.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; +import { IdentifierType } from '../index/index.reducer'; /* tslint:disable:max-classes-per-file */ @@ -48,7 +49,7 @@ export class GetRequest extends RestRequest { public uuid: string, public href: string, public body?: any, - public options?: HttpOptions, + public options?: HttpOptions ) { super(uuid, href, RestRequestMethod.GET, body, options) } @@ -124,7 +125,8 @@ export class FindByIDRequest extends GetRequest { constructor( uuid: string, href: string, - public resourceID: string + public resourceID: string, + public identifierType?: IdentifierType ) { super(uuid, href); } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 0980d48537649d00576bb1f6d59e4a0a7b5d22e7..ac65042238815659d93963ea323b07a18df086b9 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -11,7 +11,7 @@ import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; -import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer'; +import { IdentifierType, IndexState, MetaIndexState, REQUEST, UUID_MAPPING } from '../index/index.reducer'; import { originalRequestUUIDFromRequestUUIDSelector, requestIndexSelector, @@ -19,7 +19,7 @@ import { } from '../index/index.selectors'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; -import { GetRequest, RestRequest } from './request.models'; +import { FindByIDRequest, GetRequest, RestRequest } from './request.models'; import { RequestEntry, RequestState } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; @@ -162,7 +162,7 @@ export class RequestService { filter((entry) => hasValue(entry)), take(1) ).subscribe((entry) => { - return this.store.dispatch(new AddToIndexAction(IndexName.UUID_MAPPING, request.uuid, entry.request.uuid)) + return this.store.dispatch(new AddToIndexAction(UUID_MAPPING, request.uuid, entry.request.uuid)) } ) } @@ -206,7 +206,7 @@ export class RequestService { } }); this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0); - this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); + this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(REQUEST, href)); } /** @@ -225,8 +225,14 @@ export class RequestService { private isCachedOrPending(request: GetRequest): boolean { const inReqCache = this.hasByHref(request.href); const inObjCache = this.objectCache.hasBySelfLink(request.href); - const isCached = inReqCache || inObjCache; - + let inObjIdCache = false; + if (request instanceof FindByIDRequest) { + const req = request as FindByIDRequest; + if (hasValue(req.identifierType && hasValue(req.resourceID))) { + inObjIdCache = this.objectCache.hasById(req.resourceID, req.identifierType) + } + } + const isCached = inReqCache || inObjCache || inObjIdCache; const isPending = this.isPending(request); return isCached || isPending; } diff --git a/src/app/core/index/index.actions.ts b/src/app/core/index/index.actions.ts index 42804dbe26a93814cf6435c100324fba86f1ce6c..24f031b33c4a25e7d3aed517903896a368f15154 100644 --- a/src/app/core/index/index.actions.ts +++ b/src/app/core/index/index.actions.ts @@ -1,7 +1,7 @@ import { Action } from '@ngrx/store'; import { type } from '../../shared/ngrx/type'; -import { IndexName } from './index.reducer'; +import { } from './index.reducer'; /** * The list of HrefIndexAction type definitions @@ -19,7 +19,7 @@ export const IndexActionTypes = { export class AddToIndexAction implements Action { type = IndexActionTypes.ADD; payload: { - name: IndexName; + name: string; value: string; key: string; }; @@ -34,7 +34,7 @@ export class AddToIndexAction implements Action { * @param value * the self link of the resource the key belongs to */ - constructor(name: IndexName, key: string, value: string) { + constructor(name: string, key: string, value: string) { this.payload = { name, key, value }; } } @@ -45,7 +45,7 @@ export class AddToIndexAction implements Action { export class RemoveFromIndexByValueAction implements Action { type = IndexActionTypes.REMOVE_BY_VALUE; payload: { - name: IndexName, + name: string, value: string }; @@ -57,7 +57,7 @@ export class RemoveFromIndexByValueAction implements Action { * @param value * the value to remove the UUID for */ - constructor(name: IndexName, value: string) { + constructor(name: string, value: string) { this.payload = { name, value }; } @@ -69,7 +69,7 @@ export class RemoveFromIndexByValueAction implements Action { export class RemoveFromIndexBySubstringAction implements Action { type = IndexActionTypes.REMOVE_BY_SUBSTRING; payload: { - name: IndexName, + name: string, value: string }; @@ -81,7 +81,7 @@ export class RemoveFromIndexBySubstringAction implements Action { * @param value * the value to remove the UUID for */ - constructor(name: IndexName, value: string) { + constructor(name: string, value: string) { this.payload = { name, value }; } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 61cf313ab1a8fc785b582406ec0ef405b6f2b56a..0cdf6bea6c889619a4a180dbd4b946900f806184 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -10,43 +10,67 @@ import { import { RequestActionTypes, RequestConfigureAction } from '../data/request.actions'; import { AddToIndexAction, RemoveFromIndexByValueAction } from './index.actions'; import { hasValue } from '../../shared/empty.util'; -import { IndexName } from './index.reducer'; +import { getIdentiferByIndexName, IdentifierType, REQUEST } from './index.reducer'; import { RestRequestMethod } from '../data/rest-request-method'; @Injectable() -export class UUIDIndexEffects { +export class IdentifierIndexEffects { - @Effect() addObject$ = this.actions$ + @Effect() addObjectByUUID$ = this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( - IndexName.OBJECT, + getIdentiferByIndexName(IdentifierType.UUID), action.payload.objectToCache.uuid, action.payload.objectToCache.self ); }) ); - @Effect() removeObject$ = this.actions$ + @Effect() addObjectByHandle$ = this.actions$ + .pipe( + ofType(ObjectCacheActionTypes.ADD), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.handle)), + map((action: AddToObjectCacheAction) => { + return new AddToIndexAction( + getIdentiferByIndexName(IdentifierType.HANDLE), + action.payload.objectToCache.handle, + action.payload.objectToCache.self + ); + }) + ); + + @Effect() removeObjectByUUID$ = this.actions$ .pipe( ofType(ObjectCacheActionTypes.REMOVE), map((action: RemoveFromObjectCacheAction) => { return new RemoveFromIndexByValueAction( - IndexName.OBJECT, + getIdentiferByIndexName(IdentifierType.UUID), action.payload ); }) ); + @Effect() removeObjectByHandle$ = this.actions$ + .pipe( + ofType(ObjectCacheActionTypes.REMOVE), + map((action: RemoveFromObjectCacheAction) => { + return new RemoveFromIndexByValueAction( + getIdentiferByIndexName(IdentifierType.HANDLE), + action.payload + ); + }) + ); + @Effect() addRequest$ = this.actions$ .pipe( ofType(RequestActionTypes.CONFIGURE), filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.GET), map((action: RequestConfigureAction) => { return new AddToIndexAction( - IndexName.REQUEST, + REQUEST, action.payload.href, action.payload.uuid ); diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts index ef46c760c6ee7680a2872092ffcc94548433306e..35460a9ef53dc08101ebd27efc875b5787349a99 100644 --- a/src/app/core/index/index.reducer.spec.ts +++ b/src/app/core/index/index.reducer.spec.ts @@ -1,6 +1,6 @@ import * as deepFreeze from 'deep-freeze'; -import { IndexName, indexReducer, MetaIndexState } from './index.reducer'; +import { getIdentiferByIndexName, IdentifierType, indexReducer, MetaIndexState, REQUEST, } from './index.reducer'; import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions'; class NullAction extends AddToIndexAction { @@ -15,14 +15,19 @@ class NullAction extends AddToIndexAction { describe('requestReducer', () => { const key1 = '567a639f-f5ff-4126-807c-b7d0910808c8'; const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const key3 = '123456789/22'; const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8'; const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb'; + const uuidIndex = getIdentiferByIndexName(IdentifierType.UUID); + const handleIndex = getIdentiferByIndexName(IdentifierType.HANDLE); const testState: MetaIndexState = { - [IndexName.OBJECT]: { + 'object/uuid-to-self-link/uuid': { [key1]: val1 - },[IndexName.REQUEST]: { + },'object/uuid-to-self-link/handle': { + [key3]: val1 + },'get-request/href-to-uuid': { [key1]: val1 - },[IndexName.UUID_MAPPING]: { + },'get-request/configured-to-cache-uuid': { [key1]: val1 } }; @@ -45,27 +50,38 @@ describe('requestReducer', () => { it('should add the \'key\' with the corresponding \'value\' to the correct substate, in response to an ADD action', () => { const state = testState; - const action = new AddToIndexAction(IndexName.REQUEST, key2, val2); + const action = new AddToIndexAction(REQUEST, key2, val2); const newState = indexReducer(state, action); - expect(newState[IndexName.REQUEST][key2]).toEqual(val2); + expect(newState[REQUEST][key2]).toEqual(val2); }); it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_VALUE action', () => { const state = testState; - const action = new RemoveFromIndexByValueAction(IndexName.OBJECT, val1); - const newState = indexReducer(state, action); + let action = new RemoveFromIndexByValueAction(uuidIndex, val1); + let newState = indexReducer(state, action); + + expect(newState[uuidIndex][key1]).toBeUndefined(); + + action = new RemoveFromIndexByValueAction(handleIndex, val1); + newState = indexReducer(state, action); + + expect(newState[handleIndex][key3]).toBeUndefined(); - expect(newState[IndexName.OBJECT][key1]).toBeUndefined(); }); it('should remove the given \'value\' from its corresponding \'key\' in the correct substate, in response to a REMOVE_BY_SUBSTRING action', () => { const state = testState; - const action = new RemoveFromIndexBySubstringAction(IndexName.OBJECT, key1); - const newState = indexReducer(state, action); + let action = new RemoveFromIndexBySubstringAction(uuidIndex, key1); + let newState = indexReducer(state, action); + + expect(newState[uuidIndex][key1]).toBeUndefined(); + + action = new RemoveFromIndexBySubstringAction(handleIndex, key3); + newState = indexReducer(state, action); - expect(newState[IndexName.OBJECT][key1]).toBeUndefined(); + expect(newState[uuidIndex][key3]).toBeUndefined(); }); }); diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts index b4cd8aa84b7425d6c76829f0a5c227983d1911fb..631d57991103449c65f5717c9a1abf3bf2fe63f2 100644 --- a/src/app/core/index/index.reducer.ts +++ b/src/app/core/index/index.reducer.ts @@ -6,23 +6,25 @@ import { RemoveFromIndexByValueAction } from './index.actions'; +export enum IdentifierType { + UUID ='uuid', + HANDLE = 'handle' +} + /** - * An enum containing all index names + * Contains the UUIDs of requests that were sent to the server and + * have their responses cached, indexed by the UUIDs of requests that + * weren't sent because the response they requested was already cached */ -export enum IndexName { - // Contains all objects in the object cache indexed by UUID - OBJECT = 'object/uuid-to-self-link', +export const UUID_MAPPING = 'get-request/configured-to-cache-uuid'; - // contains all requests in the request cache indexed by UUID - REQUEST = 'get-request/href-to-uuid', +// contains all requests in the request cache indexed by UUID +export const REQUEST = 'get-request/href-to-uuid'; - /** - * Contains the UUIDs of requests that were sent to the server and - * have their responses cached, indexed by the UUIDs of requests that - * weren't sent because the response they requested was already cached - */ - UUID_MAPPING = 'get-request/configured-to-cache-uuid' -} +// returns the index for the provided id type (uuid, handle) +export const getIdentiferByIndexName = (idType: IdentifierType): string => { + return `object/uuid-to-self-link/${idType}`; +}; /** * The state of a single index @@ -34,8 +36,11 @@ export interface IndexState { /** * The state that contains all indices */ -export type MetaIndexState = { - [name in IndexName]: IndexState +export interface MetaIndexState { + 'get-request/configured-to-cache-uuid': IndexState, + 'get-request/href-to-uuid': IndexState, + 'object/uuid-to-self-link/uuid': IndexState, + 'object/uuid-to-self-link/handle': IndexState } // Object.create(null) ensures the object has no default js properties (e.g. `__proto__`) diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts index 3c7b331a926484963009c2cda85245bf49d3eb7c..80a7c0d46a16729fcbdc068229b2c0675db3b3b0 100644 --- a/src/app/core/index/index.selectors.ts +++ b/src/app/core/index/index.selectors.ts @@ -3,7 +3,14 @@ import { AppState } from '../../app.reducer'; import { hasValue } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; -import { IndexName, IndexState, MetaIndexState } from './index.reducer'; +import { + getIdentiferByIndexName, + IdentifierType, + IndexState, + MetaIndexState, + REQUEST, + UUID_MAPPING +} from './index.reducer'; /** * Return the MetaIndexState based on the CoreSate @@ -20,13 +27,17 @@ export const metaIndexSelector: MemoizedSelector<AppState, MetaIndexState> = cre * Return the object index based on the MetaIndexState * It contains all objects in the object cache indexed by UUID * + * @param identifierType the type of index, used to select index from state + * * @returns * a MemoizedSelector to select the object index */ -export const objectIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( - metaIndexSelector, - (state: MetaIndexState) => state[IndexName.OBJECT] -); +export const objectIndexSelector = (identifierType: IdentifierType = IdentifierType.UUID): MemoizedSelector<AppState, IndexState> => { + return createSelector( + metaIndexSelector, + (state: MetaIndexState) => state[getIdentiferByIndexName(identifierType)] + ); +} /** * Return the request index based on the MetaIndexState @@ -36,7 +47,7 @@ export const objectIndexSelector: MemoizedSelector<AppState, IndexState> = creat */ export const requestIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( metaIndexSelector, - (state: MetaIndexState) => state[IndexName.REQUEST] + (state: MetaIndexState) => state[REQUEST] ); /** @@ -47,21 +58,22 @@ export const requestIndexSelector: MemoizedSelector<AppState, IndexState> = crea */ export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( metaIndexSelector, - (state: MetaIndexState) => state[IndexName.UUID_MAPPING] + (state: MetaIndexState) => state[UUID_MAPPING] ); /** * Return the self link of an object in the object-cache based on its UUID * - * @param uuid + * @param id * the UUID for which you want to find the matching self link + * @param identifierType the type of index, used to select index from state * @returns * a MemoizedSelector to select the self link */ export const selfLinkFromUuidSelector = - (uuid: string): MemoizedSelector<AppState, string> => createSelector( - objectIndexSelector, - (state: IndexState) => hasValue(state) ? state[uuid] : undefined + (id: string, identifierType: IdentifierType = IdentifierType.UUID): MemoizedSelector<AppState, string> => createSelector( + objectIndexSelector(identifierType), + (state: IndexState) => hasValue(state) ? state[id] : undefined ); /**