Skip to content
Snippets Groups Projects
Commit 6bf086b7 authored by Michael W Spalti's avatar Michael W Spalti
Browse files

Updated work on routing by id.

Fixed unit tests.

Updated to use pid REST endpoint.

Minor change in data.service and unit test update.

Updated the objectnotfound page with new text and go home button.
parent eea25066
No related branches found
No related tags found
No related merge requests found
Showing
with 393 additions and 34 deletions
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 {
}
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 {
}
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
};
}
}
<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>
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;
}
}
}
......@@ -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' },
......
......@@ -128,7 +128,7 @@ const EXPORTS = [
...PROVIDERS
],
declarations: [
...DECLARATIONS,
...DECLARATIONS
],
exports: [
...EXPORTS
......
......@@ -44,6 +44,7 @@ export abstract class TypedObject {
*/
export class CacheableObject extends TypedObject {
uuid?: string;
handle?: string;
self: string;
// isNew: boolean;
// dirtyType: DirtyType;
......
......@@ -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));
......
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,
......
......@@ -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', () => {
......
......@@ -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))
);
......
......@@ -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);
});
......
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
});
});
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 '';
}
}
}
......@@ -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', () => {
......
......@@ -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);
}
......
......@@ -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;
}
......
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 };
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment