mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 11:33:04 +00:00
get browse endpoints from hal links
This commit is contained in:
@@ -22,6 +22,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
selector: 'ds-collection-page',
|
selector: 'ds-collection-page',
|
||||||
styleUrls: ['./collection-page.component.scss'],
|
styleUrls: ['./collection-page.component.scss'],
|
||||||
templateUrl: './collection-page.component.html',
|
templateUrl: './collection-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||||
collectionData: RemoteData<Collection>;
|
collectionData: RemoteData<Collection>;
|
||||||
@@ -37,8 +38,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
|
|||||||
private route: ActivatedRoute) {
|
private route: ActivatedRoute) {
|
||||||
this.paginationConfig = new PaginationComponentOptions();
|
this.paginationConfig = new PaginationComponentOptions();
|
||||||
this.paginationConfig.id = 'collection-page-pagination';
|
this.paginationConfig.id = 'collection-page-pagination';
|
||||||
this.paginationConfig.pageSizeOptions = [4];
|
this.paginationConfig.pageSize = 5;
|
||||||
this.paginationConfig.pageSize = 4;
|
|
||||||
this.paginationConfig.currentPage = 1;
|
this.paginationConfig.currentPage = 1;
|
||||||
this.sortConfig = new SortOptions();
|
this.sortConfig = new SortOptions();
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { ActivatedRoute, Params } from '@angular/router';
|
||||||
|
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
@@ -13,6 +13,7 @@ import { hasValue } from '../shared/empty.util';
|
|||||||
selector: 'ds-community-page',
|
selector: 'ds-community-page',
|
||||||
styleUrls: ['./community-page.component.scss'],
|
styleUrls: ['./community-page.component.scss'],
|
||||||
templateUrl: './community-page.component.html',
|
templateUrl: './community-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||||
communityData: RemoteData<Community>;
|
communityData: RemoteData<Community>;
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
import { Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-home',
|
selector: 'ds-home',
|
||||||
styleUrls: ['./home.component.scss'],
|
styleUrls: ['./home.component.scss'],
|
||||||
templateUrl: './home.component.html'
|
templateUrl: './home.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class HomeComponent {
|
export class HomeComponent {
|
||||||
|
|
||||||
|
@@ -40,5 +40,7 @@ export class TopLevelCommunityListComponent {
|
|||||||
elementsPerPage: data.pageSize,
|
elementsPerPage: data.pageSize,
|
||||||
sort: { field: data.sortField, direction: data.sortDirection }
|
sort: { field: data.sortField, direction: data.sortDirection }
|
||||||
});
|
});
|
||||||
|
this.cds.getScopedEndpoint('7669c72a-3f2a-451f-a3b9-9210e7a4c02f')
|
||||||
|
.subscribe((c) => console.log('communities', c))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
|||||||
selector: 'ds-item-page',
|
selector: 'ds-item-page',
|
||||||
styleUrls: ['./item-page.component.scss'],
|
styleUrls: ['./item-page.component.scss'],
|
||||||
templateUrl: './item-page.component.html',
|
templateUrl: './item-page.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class ItemPageComponent implements OnInit {
|
export class ItemPageComponent implements OnInit {
|
||||||
|
|
||||||
|
@@ -40,6 +40,7 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
// forRoot ensures the providers are only created once
|
// forRoot ensures the providers are only created once
|
||||||
IdlePreloadModule.forRoot(),
|
IdlePreloadModule.forRoot(),
|
||||||
RouterModule.forRoot([], {
|
RouterModule.forRoot([], {
|
||||||
|
// enableTracing: true,
|
||||||
useHash: false,
|
useHash: false,
|
||||||
preloadingStrategy:
|
preloadingStrategy:
|
||||||
IdlePreload
|
IdlePreload
|
||||||
|
62
src/app/core/browse/browse.service.ts
Normal file
62
src/app/core/browse/browse.service.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { BrowseEndpointRequest, RestRequest } from '../data/request.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { BrowseSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseService extends HALEndpointService {
|
||||||
|
protected linkName = 'browses';
|
||||||
|
|
||||||
|
private static toSearchKeyArray(metadatumKey: string): string[] {
|
||||||
|
const keyParts = metadatumKey.split('.');
|
||||||
|
const searchFor = [];
|
||||||
|
searchFor.push('*');
|
||||||
|
for (let i = 0; i < keyParts.length - 1; i++) {
|
||||||
|
const prevParts = keyParts.slice(0, i + 1);
|
||||||
|
const nextPart = [...prevParts, '*'].join('.');
|
||||||
|
searchFor.push(nextPart);
|
||||||
|
}
|
||||||
|
searchFor.push(metadatumKey);
|
||||||
|
return searchFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected responseCache: ResponseCacheService,
|
||||||
|
protected requestService: RequestService,
|
||||||
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseURLFor(metadatumKey: string, linkName: string): Observable<string> {
|
||||||
|
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
|
||||||
|
return this.getEndpoint()
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((endpointURL: string) => new BrowseEndpointRequest(endpointURL))
|
||||||
|
.do((request: RestRequest) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
|
})
|
||||||
|
.flatMap((request: RestRequest) => this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.filter((response: BrowseSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.browseDefinitions))
|
||||||
|
.map((response: BrowseSuccessResponse) => response.browseDefinitions)
|
||||||
|
.map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
|
||||||
|
.find((def: BrowseDefinition) => {
|
||||||
|
const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0);
|
||||||
|
return matchingKeys.length > 0
|
||||||
|
})
|
||||||
|
).map((def: BrowseDefinition) => def._links[linkName])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -66,4 +66,14 @@ export abstract class NormalizedDSpaceObject extends NormalizedObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
owner: string;
|
owner: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links to all related resources returned by the rest api.
|
||||||
|
*
|
||||||
|
* Repeated here to make the serialization work,
|
||||||
|
* inheritSerialization doesn't seem to work for more than one level
|
||||||
|
*/
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,4 +17,8 @@ export abstract class NormalizedObject implements CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
10
src/app/core/cache/response-cache.models.ts
vendored
10
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
import { RequestError } from '../data/request.models';
|
import { RequestError } from '../data/request.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -32,6 +33,15 @@ export class RootSuccessResponse extends RestResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public browseDefinitions: BrowseDefinition[],
|
||||||
|
public statusCode: string
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ErrorResponse extends RestResponse {
|
export class ErrorResponse extends RestResponse {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
|
|
||||||
|
@@ -24,6 +24,8 @@ import { HostWindowService } from '../shared/host-window.service';
|
|||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
||||||
|
|
||||||
import { ServerResponseService } from '../shared/server-response.service';
|
import { ServerResponseService } from '../shared/server-response.service';
|
||||||
|
import { BrowseService } from './browse/browse.service';
|
||||||
|
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -54,6 +56,8 @@ const PROVIDERS = [
|
|||||||
ResponseCacheService,
|
ResponseCacheService,
|
||||||
RootResponseParsingService,
|
RootResponseParsingService,
|
||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
|
BrowseResponseParsingService,
|
||||||
|
BrowseService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
28
src/app/core/data/browse-response-parsing.service.ts
Normal file
28
src/app/core/data/browse-response-parsing.service.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrowseResponseParsingService implements ResponseParsingService {
|
||||||
|
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
|
||||||
|
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
|
||||||
|
const serializer = new DSpaceRESTv2Serializer(BrowseDefinition);
|
||||||
|
const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
|
||||||
|
return new BrowseSuccessResponse(browseDefinitions, data.statusCode);
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from browse endpoint'),
|
||||||
|
{ statusText: data.statusCode }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -9,20 +9,54 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { CommunityDataService } from './community-data.service';
|
||||||
|
import { FindByIDRequest } from './request.models';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
||||||
protected linkName = 'collections';
|
protected linkName = 'collections';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/collections';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
private cds: CommunityDataService,
|
||||||
|
protected objectCache: ObjectCacheService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCollection, EnvConfig);
|
super(NormalizedCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the scoped endpoint URL by fetching the object with
|
||||||
|
* the given scopeID and returning its HAL link with this
|
||||||
|
* data-service's linkName
|
||||||
|
*
|
||||||
|
* @param {string} scopeID
|
||||||
|
* the id of the scope object
|
||||||
|
* @return { Observable<string> }
|
||||||
|
* an Observable<string> containing the scoped URL
|
||||||
|
*/
|
||||||
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
|
this.cds.getEndpoint()
|
||||||
|
.map((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID))
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.take(1)
|
||||||
|
.subscribe((href: string) => {
|
||||||
|
const request = new FindByIDRequest(href, scopeID);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.objectCache.getByUUID(scopeID, NormalizedCommunity)
|
||||||
|
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
|
||||||
|
.filter((href) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,20 +10,52 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { FindByIDRequest } from './request.models';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
||||||
protected linkName = 'communities';
|
protected linkName = 'communities';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/communities';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
protected objectCache: ObjectCacheService
|
||||||
) {
|
) {
|
||||||
super(NormalizedCommunity, EnvConfig);
|
super(NormalizedCommunity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the scoped endpoint URL by fetching the object with
|
||||||
|
* the given scopeID and returning its HAL link with this
|
||||||
|
* data-service's linkName
|
||||||
|
*
|
||||||
|
* @param {string} scopeID
|
||||||
|
* the id of the scope object
|
||||||
|
* @return { Observable<string> }
|
||||||
|
* an Observable<string> containing the scoped URL
|
||||||
|
*/
|
||||||
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
|
this.getEndpoint()
|
||||||
|
.map((endpoint: string) => this.getFindByIDHref(endpoint, scopeID))
|
||||||
|
.filter((href: string) => isNotEmpty(href))
|
||||||
|
.take(1)
|
||||||
|
.subscribe((href: string) => {
|
||||||
|
const request = new FindByIDRequest(href, scopeID);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.objectCache.getByUUID(scopeID, NormalizedCommunity)
|
||||||
|
.map((nc: NormalizedCommunity) => nc._links[this.linkName])
|
||||||
|
.filter((href) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,71 +1,41 @@
|
|||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
|
||||||
import { RemoteData } from './remote-data';
|
|
||||||
import {
|
|
||||||
FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest,
|
|
||||||
RootEndpointRequest
|
|
||||||
} from './request.models';
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RequestService } from './request.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { GenericConstructor } from '../shared/generic-constructor';
|
|
||||||
import { GlobalConfig } from '../../../config';
|
|
||||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { GlobalConfig } from '../../../config';
|
||||||
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest } from './request.models';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
|
||||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
export abstract class DataService<TNormalized extends CacheableObject, TDomain> extends HALEndpointService {
|
||||||
protected abstract responseCache: ResponseCacheService;
|
protected abstract responseCache: ResponseCacheService;
|
||||||
protected abstract requestService: RequestService;
|
protected abstract requestService: RequestService;
|
||||||
protected abstract rdbService: RemoteDataBuildService;
|
protected abstract rdbService: RemoteDataBuildService;
|
||||||
protected abstract store: Store<CoreState>;
|
protected abstract store: Store<CoreState>;
|
||||||
protected abstract linkName: string;
|
protected abstract linkName: string;
|
||||||
protected abstract browseEndpoint: string;
|
protected abstract EnvConfig: GlobalConfig
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private normalizedResourceType: GenericConstructor<TNormalized>,
|
private normalizedResourceType: GenericConstructor<TNormalized>,
|
||||||
protected EnvConfig: GlobalConfig
|
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEndpointMap(): Observable<EndpointMap> {
|
public abstract getScopedEndpoint(scope: string): Observable<string>
|
||||||
const request = new RootEndpointRequest(this.EnvConfig);
|
|
||||||
this.requestService.configure(request);
|
|
||||||
return this.responseCache.get(request.href)
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
|
||||||
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
|
||||||
.map((response: RootSuccessResponse) => response.endpointMap)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEndpoint(): Observable<string> {
|
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> {
|
||||||
const request = new RootEndpointRequest(this.EnvConfig);
|
|
||||||
this.requestService.configure(request);
|
|
||||||
return this.getEndpointMap()
|
|
||||||
.map((map: EndpointMap) => map[this.linkName])
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEnabledOnRestApi(): Observable<boolean> {
|
|
||||||
return this.getEndpointMap()
|
|
||||||
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
|
|
||||||
.startWith(undefined)
|
|
||||||
.distinctUntilChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getFindAllHref(endpoint, options: FindAllOptions = {}): string {
|
|
||||||
let result;
|
let result;
|
||||||
const args = [];
|
const args = [];
|
||||||
|
|
||||||
if (hasValue(options.scopeID)) {
|
if (hasValue(options.scopeID)) {
|
||||||
result = new RESTURLCombiner(this.EnvConfig, this.browseEndpoint).toString();
|
result = this.getScopedEndpoint(options.scopeID);
|
||||||
args.push(`scope=${options.scopeID}`);
|
|
||||||
} else {
|
} else {
|
||||||
result = endpoint;
|
result = Observable.of(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||||
@@ -86,25 +56,28 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isNotEmpty(args)) {
|
if (isNotEmpty(args)) {
|
||||||
result = `${result}?${args.join('&')}`;
|
return result.map((href: string) => `${href}?${args.join('&')}`);
|
||||||
|
} else {
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
|
findAll(options: FindAllOptions = {}): RemoteData<TDomain[]> {
|
||||||
const hrefObs = this.getEndpoint()
|
const hrefObs = this.getEndpoint()
|
||||||
.map((endpoint: string) => this.getFindAllHref(endpoint, options));
|
.flatMap((endpoint: string) => this.getFindAllHref(endpoint, options));
|
||||||
|
|
||||||
hrefObs
|
hrefObs
|
||||||
.subscribe((href: string) => {
|
.subscribe((href: string) => {
|
||||||
const request = new FindAllRequest(href, options);
|
const request = new FindAllRequest(href, options);
|
||||||
this.requestService.configure(request);
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
return this.rdbService.buildList<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getFindByIDHref(endpoint, resourceID): string {
|
getFindByIDHref(endpoint, resourceID): string {
|
||||||
return `${endpoint}/${resourceID}`;
|
return `${endpoint}/${resourceID}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,14 +88,18 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
|||||||
hrefObs
|
hrefObs
|
||||||
.subscribe((href: string) => {
|
.subscribe((href: string) => {
|
||||||
const request = new FindByIDRequest(href, id);
|
const request = new FindByIDRequest(href, id);
|
||||||
this.requestService.configure(request);
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(hrefObs, this.normalizedResourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
findByHref(href: string): RemoteData<TDomain> {
|
findByHref(href: string): RemoteData<TDomain> {
|
||||||
this.requestService.configure(new RestRequest(href));
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(new RestRequest(href));
|
||||||
|
}, 0);
|
||||||
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
|
return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
|
||||||
// return this.rdbService.buildSingle(href));
|
// return this.rdbService.buildSingle(href));
|
||||||
}
|
}
|
||||||
|
@@ -10,19 +10,29 @@ import { NormalizedItem } from '../cache/models/normalized-item.model';
|
|||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { BrowseService } from '../browse/browse.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||||
protected linkName = 'items';
|
protected linkName = 'items';
|
||||||
protected browseEndpoint = '/discover/browses/dateissued/items';
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
protected responseCache: ResponseCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected rdbService: RemoteDataBuildService,
|
protected rdbService: RemoteDataBuildService,
|
||||||
protected store: Store<CoreState>,
|
protected store: Store<CoreState>,
|
||||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
|
private bs: BrowseService
|
||||||
) {
|
) {
|
||||||
super(NormalizedItem, EnvConfig);
|
super(NormalizedItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getScopedEndpoint(scopeID: string): Observable<string> {
|
||||||
|
return this.bs.getBrowseURLFor('dc.date.issued', this.linkName)
|
||||||
|
.filter((href) => isNotEmpty(href))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
|||||||
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
import { ResponseParsingService } from './parsing.service';
|
import { ResponseParsingService } from './parsing.service';
|
||||||
import { RootResponseParsingService } from './root-response-parsing.service';
|
import { RootResponseParsingService } from './root-response-parsing.service';
|
||||||
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestRequest {
|
export class RestRequest {
|
||||||
@@ -53,6 +54,16 @@ export class RootEndpointRequest extends RestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BrowseEndpointRequest extends RestRequest {
|
||||||
|
constructor(href: string) {
|
||||||
|
super(href);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return BrowseResponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class RequestError extends Error {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
statusText: string;
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { DSOSuccessResponse } from '../cache/response-cache.models';
|
import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -47,16 +47,20 @@ export class RequestService {
|
|||||||
|
|
||||||
configure<T extends CacheableObject>(request: RestRequest): void {
|
configure<T extends CacheableObject>(request: RestRequest): void {
|
||||||
let isCached = this.objectCache.hasBySelfLink(request.href);
|
let isCached = this.objectCache.hasBySelfLink(request.href);
|
||||||
|
// console.log('request.href', request.href);
|
||||||
if (!isCached && this.responseCache.has(request.href)) {
|
if (!isCached && this.responseCache.has(request.href)) {
|
||||||
const [dsoSuccessResponse, otherSuccessResponse] = this.responseCache.get(request.href)
|
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||||
.take(1)
|
.take(1)
|
||||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.share()
|
||||||
|
.partition((response: RestResponse) => response.isSuccessful);
|
||||||
|
|
||||||
|
const [dsoSuccessResponse, otherSuccessResponse] = successResponse
|
||||||
.share()
|
.share()
|
||||||
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
|
.partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks));
|
||||||
|
|
||||||
Observable.merge(
|
Observable.merge(
|
||||||
|
errorResponse.map(() => true), // TODO add a configurable number of retries in case of an error.
|
||||||
otherSuccessResponse.map(() => true),
|
otherSuccessResponse.map(() => true),
|
||||||
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
|
dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached
|
||||||
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)
|
.map((response: DSOSuccessResponse) => response.resourceSelfLinks)
|
||||||
|
@@ -19,12 +19,7 @@ export class RootResponseParsingService implements ResponseParsingService {
|
|||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
||||||
const links = data.payload._links;
|
const links = data.payload._links;
|
||||||
for (const link of Object.keys(links)) {
|
for (const link of Object.keys(links)) {
|
||||||
let href = links[link].href;
|
links[link] = links[link].href;
|
||||||
// TODO temporary workaround as these endpoint paths are relative, but should be absolute
|
|
||||||
if (isNotEmpty(href) && !href.startsWith('http')) {
|
|
||||||
href = new RESTURLCombiner(this.EnvConfig, href.substring(this.EnvConfig.rest.nameSpace.length)).toString();
|
|
||||||
}
|
|
||||||
links[link] = href;
|
|
||||||
}
|
}
|
||||||
return new RootSuccessResponse(links, data.statusCode);
|
return new RootSuccessResponse(links, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
|
24
src/app/core/shared/browse-definition.model.ts
Normal file
24
src/app/core/shared/browse-definition.model.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||||
|
import { SortOption } from './sort-option.model';
|
||||||
|
|
||||||
|
export class BrowseDefinition {
|
||||||
|
@autoserialize
|
||||||
|
metadataBrowse: boolean;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
sortOptions: SortOption[];
|
||||||
|
|
||||||
|
@autoserializeAs('order')
|
||||||
|
defaultSortOrder: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@autoserializeAs('metadata')
|
||||||
|
metadataKeys: string[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
_links: {
|
||||||
|
[name: string]: string
|
||||||
|
}
|
||||||
|
}
|
46
src/app/core/shared/hal-endpoint.service.ts
Normal file
46
src/app/core/shared/hal-endpoint.service.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models';
|
||||||
|
import { RootEndpointRequest } from '../data/request.models';
|
||||||
|
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
export abstract class HALEndpointService {
|
||||||
|
protected abstract responseCache: ResponseCacheService;
|
||||||
|
protected abstract requestService: RequestService;
|
||||||
|
protected abstract linkName: string;
|
||||||
|
protected abstract EnvConfig: GlobalConfig;
|
||||||
|
|
||||||
|
protected getEndpointMap(): Observable<EndpointMap> {
|
||||||
|
const request = new RootEndpointRequest(this.EnvConfig);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.requestService.configure(request);
|
||||||
|
}, 0);
|
||||||
|
return this.responseCache.get(request.href)
|
||||||
|
.map((entry: ResponseCacheEntry) => entry.response)
|
||||||
|
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
||||||
|
.map((response: RootSuccessResponse) => response.endpointMap)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEndpoint(): Observable<string> {
|
||||||
|
return this.getEndpointMap()
|
||||||
|
.do((map: EndpointMap) => {
|
||||||
|
if (!this.linkName) {
|
||||||
|
console.log('map', this)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((map: EndpointMap) => map[this.linkName])
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isEnabledOnRestApi(): Observable<boolean> {
|
||||||
|
return this.getEndpointMap()
|
||||||
|
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
|
||||||
|
.startWith(undefined)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
src/app/core/shared/sort-option.model.ts
Normal file
9
src/app/core/shared/sort-option.model.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { autoserialize } from 'cerialize';
|
||||||
|
|
||||||
|
export class SortOption {
|
||||||
|
@autoserialize
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
metadata: string;
|
||||||
|
}
|
Reference in New Issue
Block a user