intermediate commit

This commit is contained in:
Art Lowel
2018-02-13 10:13:02 +01:00
committed by Lotte Hofstede
parent 605c3524c8
commit 4c2cbc55e0
27 changed files with 291 additions and 178 deletions

View File

@@ -0,0 +1,13 @@
import { autoserialize } from 'cerialize';
import { Metadatum } from '../core/shared/metadatum.model';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
export class NormalizedSearchResult implements ListableObject {
@autoserialize
dspaceObject: string;
@autoserialize
hitHighlights: Metadatum[];
}

View File

@@ -96,8 +96,8 @@ describe('SearchFacetFilterComponent', () => {
link = comp.getSearchLink(); link = comp.getSearchLink();
}); });
it('should return the value of the searchLink variable in the filter service', () => { it('should return the value of the uiSearchRoute variable in the filter service', () => {
expect(link).toEqual(filterService.searchLink); expect(link).toEqual(filterService.uiSearchRoute);
}); });
}); });

View File

@@ -192,13 +192,13 @@ describe('SearchFilterService', () => {
}); });
}); });
describe('when the searchLink method is called', () => { describe('when the uiSearchRoute method is called', () => {
let link: string; let link: string;
beforeEach(() => { beforeEach(() => {
link = service.searchLink; link = service.searchLink;
}); });
it('should return the value of searchLink in the search service', () => { it('should return the value of uiSearchRoute in the search service', () => {
expect(link).toEqual(searchServiceStub.searchLink); expect(link).toEqual(searchServiceStub.searchLink);
}); });
}); });

View File

@@ -46,7 +46,7 @@ export class SearchFilterService {
} }
get searchLink() { get searchLink() {
return this.searchService.searchLink; return this.searchService.uiSearchRoute;
} }
isCollapsed(filterName: string): Observable<boolean> { isCollapsed(filterName: string): Observable<boolean> {

View File

@@ -0,0 +1,47 @@
import { autoserialize, autoserializeAs } from 'cerialize';
import { PageInfo } from '../../core/shared/page-info.model';
import { NormalizedSearchResult } from '../normalized-search-result.model';
export class SearchQueryResponse {
@autoserialize
scope: string;
@autoserialize
query: string;
@autoserialize
appliedFilters: any[]; // TODO
@autoserialize
sort: any; // TODO
@autoserialize
configurationName: string;
@autoserialize
public type: string;
@autoserialize
page: PageInfo;
@autoserializeAs(NormalizedSearchResult)
objects: NormalizedSearchResult[];
@autoserialize
facets: any; // TODO
@autoserialize
self: string;
@autoserialize
next: string;
@autoserialize
previous: string;
@autoserialize
first: string;
@autoserialize
last: string;
}

View File

@@ -1,12 +1,25 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Inject, Injectable, OnDestroy } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { ViewMode } from '../../+search-page/search-options.model'; import { ViewMode } from '../../+search-page/search-options.model';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model';
import { RestResponse } from '../../core/cache/response-cache.models';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { DebugResponseParsingService } from '../../core/data/debug-response-parsing.service';
import { DSOResponseParsingService } from '../../core/data/dso-response-parsing.service';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ResponseParsingService } from '../../core/data/parsing.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { GetRequest, EndpointMapRequest, RestRequest } from '../../core/data/request.models';
import { RequestService } from '../../core/data/request.service';
import { DSpaceRESTV2Response } from '../../core/dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { Metadatum } from '../../core/shared/metadatum.model'; import { Metadatum } from '../../core/shared/metadatum.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
@@ -19,6 +32,7 @@ import { SearchResult } from '../search-result.model';
import { FacetValue } from './facet-value.model'; import { FacetValue } from './facet-value.model';
import { FilterType } from './filter-type.model'; import { FilterType } from './filter-type.model';
import { SearchFilterConfig } from './search-filter-config.model'; import { SearchFilterConfig } from './search-filter-config.model';
import { SearchResponseParsingService } from '../../core/data/search-response-parsing.service';
function shuffle(array: any[]) { function shuffle(array: any[]) {
let i = 0; let i = 0;
@@ -35,23 +49,11 @@ function shuffle(array: any[]) {
} }
@Injectable() @Injectable()
export class SearchService implements OnDestroy { export class SearchService extends HALEndpointService implements OnDestroy {
protected linkPath = 'discover/search/objects';
totalPages = 5;
mockedHighlights: string[] = new Array(
'This is a <em>sample abstract</em>.',
'This is a sample abstract. But, to fill up some space, here\'s <em>"Hello"</em> in several different languages : ',
'This is a Sample HTML webpage including several <em>images</em> and styles (CSS).',
'This is <em>really</em> just a sample abstract. But, Ívé thrown ïn a cõuple of spëciâl charactèrs för êxtrå fuñ!',
'This abstract is <em>really quite great</em>',
'The solution structure of the <em>bee</em> venom neurotoxin',
'BACKGROUND: The <em>Open Archive Initiative (OAI)</em> refers to a movement started around the \'90 s to guarantee free access to scientific information',
'The collision fault detection of a <em>XXY</em> stage is proposed for the first time in this paper',
'<em>This was blank in the actual item, no abstract</em>',
'<em>The QSAR DataBank (QsarDB) repository</em>',
);
private sub; private sub;
searchLink = '/search'; uiSearchRoute = '/search';
config: SearchFilterConfig[] = [ config: SearchFilterConfig[] = [
Object.assign(new SearchFilterConfig(), Object.assign(new SearchFilterConfig(),
@@ -86,11 +88,16 @@ export class SearchService implements OnDestroy {
// searchOptions: BehaviorSubject<SearchOptions>; // searchOptions: BehaviorSubject<SearchOptions>;
searchOptions: SearchOptions; searchOptions: SearchOptions;
constructor(private itemDataService: ItemDataService, constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
private itemDataService: ItemDataService,
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private routeService: RouteService, private routeService: RouteService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router) { private router: Router
) {
super();
const pagination: PaginationComponentOptions = new PaginationComponentOptions(); const pagination: PaginationComponentOptions = new PaginationComponentOptions();
pagination.id = 'search-results-pagination'; pagination.id = 'search-results-pagination';
pagination.currentPage = 1; pagination.currentPage = 1;
@@ -101,74 +108,18 @@ export class SearchService implements OnDestroy {
} }
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> { search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
this.searchOptions = searchOptions; const searchEndpointUrlObs = this.getEndpoint();
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`; searchEndpointUrlObs.pipe(
if (hasValue(scopeId)) { map((url: string) => {
self += `&scope=${scopeId}`; const request = new GetRequest(this.requestService.generateRequestId(), url);
return Object.assign(request, {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return SearchResponseParsingService;
} }
if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.currentPage)) {
self += `&page=${searchOptions.pagination.currentPage}`;
}
if (isNotEmpty(searchOptions) && hasValue(searchOptions.pagination.pageSize)) {
self += `&pageSize=${searchOptions.pagination.pageSize}`;
}
if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.direction)) {
self += `&sortDirection=${searchOptions.sort.direction}`;
}
if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.field)) {
self += `&sortField=${searchOptions.sort.field}`;
}
const error = undefined;
const returningPageInfo = new PageInfo();
if (isNotEmpty(searchOptions)) {
returningPageInfo.elementsPerPage = searchOptions.pagination.pageSize;
returningPageInfo.currentPage = searchOptions.pagination.currentPage;
} else {
returningPageInfo.elementsPerPage = 10;
returningPageInfo.currentPage = 1;
}
const itemsObs = this.itemDataService.findAll({
scopeID: scopeId,
currentPage: returningPageInfo.currentPage,
elementsPerPage: returningPageInfo.elementsPerPage
}); });
})
return itemsObs ).subscribe((request: RestRequest) => this.requestService.configure(request));
.filter((rd: RemoteData<PaginatedList<Item>>) => rd.hasSucceeded) return Observable.of(undefined);
.map((rd: RemoteData<PaginatedList<Item>>) => {
const totalElements = rd.payload.totalElements > 20 ? 20 : rd.payload.totalElements;
const page = shuffle(rd.payload.page)
.map((item: Item, index: number) => {
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
mockResult.dspaceObject = item;
const highlight = new Metadatum();
highlight.key = 'dc.description.abstract';
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
mockResult.hitHighlights = new Array(highlight);
return mockResult;
});
const payload = Object.assign({}, rd.payload, { totalElements: totalElements, page });
return new RemoteData(
rd.isRequestPending,
rd.isResponsePending,
rd.hasSucceeded,
error,
payload
)
}).startWith(new RemoteData(
true,
false,
undefined,
undefined,
undefined
));
} }
getConfig(): Observable<RemoteData<SearchFilterConfig[]>> { getConfig(): Observable<RemoteData<SearchFilterConfig[]>> {
@@ -231,7 +182,7 @@ export class SearchService implements OnDestroy {
queryParamsHandling: 'merge' queryParamsHandling: 'merge'
}; };
this.router.navigate([this.searchLink], navigationExtras); this.router.navigate([this.uiSearchRoute], navigationExtras);
} }
getClearFiltersQueryParams(): any { getClearFiltersQueryParams(): any {
@@ -249,7 +200,7 @@ export class SearchService implements OnDestroy {
} }
getSearchLink() { getSearchLink() {
return this.searchLink; return this.uiSearchRoute;
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -110,12 +110,12 @@ describe('BrowseService', () => {
.returnValue(hot('--a-', { a: browsesEndpointURL })); .returnValue(hot('--a-', { a: browsesEndpointURL }));
}); });
it('should return the URL for the given metadatumKey and linkName', () => { it('should return the URL for the given metadatumKey and linkPath', () => {
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'items'; const linkPath = 'items';
const expectedURL = browseDefinitions[0]._links[linkName]; const expectedURL = browseDefinitions[0]._links[linkPath];
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-d-', { c: undefined, d: expectedURL }); const expected = cold('c-d-', { c: undefined, d: expectedURL });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
@@ -123,10 +123,10 @@ describe('BrowseService', () => {
it('should work when the definition uses a wildcard in the metadatumKey', () => { it('should work when the definition uses a wildcard in the metadatumKey', () => {
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
const linkName = 'items'; const linkPath = 'items';
const expectedURL = browseDefinitions[1]._links[linkName]; const expectedURL = browseDefinitions[1]._links[linkPath];
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-d-', { c: undefined, d: expectedURL }); const expected = cold('c-d-', { c: undefined, d: expectedURL });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
@@ -134,30 +134,30 @@ describe('BrowseService', () => {
it('should throw an error when the key doesn\'t match', () => { it('should throw an error when the key doesn\'t match', () => {
const metadatumKey = 'dc.title'; // isn't in the definitions const metadatumKey = 'dc.title'; // isn't in the definitions
const linkName = 'items'; const linkPath = 'items';
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should throw an error when the link doesn\'t match', () => { it('should throw an error when the link doesn\'t match', () => {
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'collections'; // isn't in the definitions const linkPath = 'collections'; // isn't in the definitions
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should configure a new BrowseEndpointRequest', () => { it('should configure a new BrowseEndpointRequest', () => {
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'items'; const linkPath = 'items';
const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL); const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL);
scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.configure).toHaveBeenCalledWith(expected);
@@ -175,9 +175,9 @@ describe('BrowseService', () => {
.returnValue(hot('----')); .returnValue(hot('----'));
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'items'; const linkPath = 'items';
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('b---', { b: undefined }); const expected = cold('b---', { b: undefined });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
@@ -192,9 +192,9 @@ describe('BrowseService', () => {
.returnValue(hot('--a-', { a: browsesEndpointURL })); .returnValue(hot('--a-', { a: browsesEndpointURL }));
const metadatumKey = 'dc.date.issued'; const metadatumKey = 'dc.date.issued';
const linkName = 'items'; const linkPath = 'items';
const result = service.getBrowseURLFor(metadatumKey, linkName); const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`)); const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`));
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });

View File

@@ -13,7 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable() @Injectable()
export class BrowseService extends HALEndpointService { export class BrowseService extends HALEndpointService {
protected linkName = 'browses'; protected linkPath = 'browses';
private static toSearchKeyArray(metadatumKey: string): string[] { private static toSearchKeyArray(metadatumKey: string): string[] {
const keyParts = metadatumKey.split('.'); const keyParts = metadatumKey.split('.');
@@ -35,7 +35,7 @@ export class BrowseService extends HALEndpointService {
super(); super();
} }
getBrowseURLFor(metadatumKey: string, linkName: string): Observable<string> { getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
return this.getEndpoint() return this.getEndpoint()
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
@@ -59,10 +59,10 @@ export class BrowseService extends HALEndpointService {
return isNotEmpty(matchingKeys); return isNotEmpty(matchingKeys);
}) })
).map((def: BrowseDefinition) => { ).map((def: BrowseDefinition) => {
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`); throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`);
} else { } else {
return def._links[linkName]; return def._links[linkPath];
} }
}) })
); );

View File

@@ -1,3 +1,4 @@
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
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'; import { BrowseDefinition } from '../shared/browse-definition.model';
@@ -21,11 +22,21 @@ export class DSOSuccessResponse extends RestResponse {
} }
} }
export class EndpointMap { export class SearchSuccessResponse extends RestResponse {
[linkName: string]: string constructor(
public results: SearchQueryResponse,
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
} }
export class RootSuccessResponse extends RestResponse { export class EndpointMap {
[linkPath: string]: string
}
export class EndpointMapSuccessResponse extends RestResponse {
constructor( constructor(
public endpointMap: EndpointMap, public endpointMap: EndpointMap,
public statusCode: string, public statusCode: string,

View File

@@ -11,7 +11,7 @@ const LINK_NAME = 'test';
const BROWSE = 'search/findByCollection'; const BROWSE = 'search/findByCollection';
class TestService extends ConfigService { class TestService extends ConfigService {
protected linkName = LINK_NAME; protected linkPath = LINK_NAME;
protected browseEndpoint = BROWSE; protected browseEndpoint = BROWSE;
constructor( constructor(

View File

@@ -16,7 +16,7 @@ export abstract class ConfigService extends HALEndpointService {
protected request: ConfigRequest; protected request: ConfigRequest;
protected abstract responseCache: ResponseCacheService; protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract linkName: string; protected abstract linkPath: string;
protected abstract EnvConfig: GlobalConfig; protected abstract EnvConfig: GlobalConfig;
protected abstract browseEndpoint: string; protected abstract browseEndpoint: string;

View File

@@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface';
@Injectable() @Injectable()
export class SubmissionDefinitionsConfigService extends ConfigService { export class SubmissionDefinitionsConfigService extends ConfigService {
protected linkName = 'submissiondefinitions'; protected linkPath = 'submissiondefinitions';
protected browseEndpoint = 'search/findByCollection'; protected browseEndpoint = 'search/findByCollection';
constructor( constructor(

View File

@@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface';
@Injectable() @Injectable()
export class SubmissionFormsConfigService extends ConfigService { export class SubmissionFormsConfigService extends ConfigService {
protected linkName = 'submissionforms'; protected linkPath = 'submissionforms';
protected browseEndpoint = ''; protected browseEndpoint = '';
constructor( constructor(

View File

@@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface';
@Injectable() @Injectable()
export class SubmissionSectionsConfigService extends ConfigService { export class SubmissionSectionsConfigService extends ConfigService {
protected linkName = 'submissionsections'; protected linkPath = 'submissionsections';
protected browseEndpoint = ''; protected browseEndpoint = '';
constructor( constructor(

View File

@@ -17,7 +17,9 @@ import { isNotEmpty } from '../shared/empty.util';
import { ApiService } from '../shared/api.service'; import { ApiService } from '../shared/api.service';
import { CollectionDataService } from './data/collection-data.service'; import { CollectionDataService } from './data/collection-data.service';
import { CommunityDataService } from './data/community-data.service'; import { CommunityDataService } from './data/community-data.service';
import { DebugResponseParsingService } from './data/debug-response-parsing.service';
import { DSOResponseParsingService } from './data/dso-response-parsing.service'; import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { ItemDataService } from './data/item-data.service'; import { ItemDataService } from './data/item-data.service';
@@ -27,7 +29,7 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
import { RequestService } from './data/request.service'; import { RequestService } from './data/request.service';
import { ResponseCacheService } from './cache/response-cache.service'; import { ResponseCacheService } from './cache/response-cache.service';
import { RootResponseParsingService } from './data/root-response-parsing.service'; import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service';
import { ServerResponseService } from '../shared/server-response.service'; import { ServerResponseService } from '../shared/server-response.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
import { BrowseService } from './browse/browse.service'; import { BrowseService } from './browse/browse.service';
@@ -67,7 +69,9 @@ const PROVIDERS = [
RemoteDataBuildService, RemoteDataBuildService,
RequestService, RequestService,
ResponseCacheService, ResponseCacheService,
RootResponseParsingService, EndpointMapResponseParsingService,
DebugResponseParsingService,
SearchResponseParsingService,
ServerResponseService, ServerResponseService,
BrowseResponseParsingService, BrowseResponseParsingService,
BrowseService, BrowseService,

View File

@@ -13,7 +13,7 @@ import { RequestService } from './request.service';
@Injectable() @Injectable()
export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> { export class CollectionDataService extends ComColDataService<NormalizedCollection, Collection> {
protected linkName = 'collections'; protected linkPath = 'collections';
constructor( constructor(
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,

View File

@@ -22,7 +22,7 @@ class NormalizedTestObject implements CacheableObject {
} }
class TestService extends ComColDataService<NormalizedTestObject, any> { class TestService extends ComColDataService<NormalizedTestObject, any> {
protected linkName = LINK_NAME; protected linkPath = LINK_NAME;
constructor( constructor(
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,

View File

@@ -17,7 +17,7 @@ export abstract class ComColDataService<TNormalized extends CacheableObject, TDo
/** /**
* Get the scoped endpoint URL by fetching the object with * Get the scoped endpoint URL by fetching the object with
* the given scopeID and returning its HAL link with this * the given scopeID and returning its HAL link with this
* data-service's linkName * data-service's linkPath
* *
* @param {string} scopeID * @param {string} scopeID
* the id of the scope object * the id of the scope object
@@ -48,7 +48,7 @@ export abstract class ComColDataService<TNormalized extends CacheableObject, TDo
Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))),
successResponse successResponse
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity))
.map((nc: NormalizedCommunity) => nc._links[this.linkName]) .map((nc: NormalizedCommunity) => nc._links[this.linkPath])
.filter((href) => isNotEmpty(href)) .filter((href) => isNotEmpty(href))
).distinctUntilChanged(); ).distinctUntilChanged();
} }

View File

@@ -13,7 +13,7 @@ import { RequestService } from './request.service';
@Injectable() @Injectable()
export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> { export class CommunityDataService extends ComColDataService<NormalizedCommunity, Community> {
protected linkName = 'communities'; protected linkPath = 'communities';
protected cds = this; protected cds = this;
constructor( constructor(

View File

@@ -19,7 +19,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
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 linkPath: string;
protected abstract EnvConfig: GlobalConfig; protected abstract EnvConfig: GlobalConfig;
constructor( constructor(

View File

@@ -0,0 +1,13 @@
import { Injectable } from '@angular/core';
import { RestResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
@Injectable()
export class DebugResponseParsingService implements ResponseParsingService {
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
console.log('request', request, 'data', data);
return undefined;
}
}

View File

@@ -1,15 +1,14 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { ErrorResponse, RestResponse, RootSuccessResponse } from '../cache/response-cache.models'; import { ErrorResponse, RestResponse, EndpointMapSuccessResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { ResponseParsingService } from './parsing.service'; import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models'; import { RestRequest } from './request.models';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
@Injectable() @Injectable()
export class RootResponseParsingService implements ResponseParsingService { export class EndpointMapResponseParsingService implements ResponseParsingService {
constructor( constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
) { ) {
@@ -21,7 +20,7 @@ export class RootResponseParsingService implements ResponseParsingService {
for (const link of Object.keys(links)) { for (const link of Object.keys(links)) {
links[link] = links[link].href; links[link] = links[link].href;
} }
return new RootSuccessResponse(links, data.statusCode); return new EndpointMapSuccessResponse(links, data.statusCode);
} else { } else {
return new ErrorResponse( return new ErrorResponse(
Object.assign( Object.assign(

View File

@@ -17,7 +17,7 @@ import { RequestService } from './request.service';
@Injectable() @Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> { export class ItemDataService extends DataService<NormalizedItem, Item> {
protected linkName = 'items'; protected linkPath = 'items';
constructor( constructor(
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
@@ -34,7 +34,7 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
if (isEmpty(scopeID)) { if (isEmpty(scopeID)) {
return this.getEndpoint(); return this.getEndpoint();
} else { } else {
return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath)
.filter((href: string) => isNotEmpty(href)) .filter((href: string) => isNotEmpty(href))
.map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()) .map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString())
.distinctUntilChanged(); .distinctUntilChanged();

View File

@@ -4,7 +4,7 @@ import { GlobalConfig } from '../../../config/global-config.interface';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; 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 { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service';
import { ConfigResponseParsingService } from './config-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service';
@@ -140,14 +140,24 @@ export class FindAllRequest extends GetRequest {
} }
} }
export class RootEndpointRequest extends GetRequest { export class EndpointMapRequest extends GetRequest {
constructor(uuid: string, EnvConfig: GlobalConfig) { constructor(
const href = new RESTURLCombiner(EnvConfig, '/').toString(); public uuid: string,
super(uuid, href); public href: string,
public body?: any
) {
super(uuid, href, body);
} }
getResponseParser(): GenericConstructor<ResponseParsingService> { getResponseParser(): GenericConstructor<ResponseParsingService> {
return RootResponseParsingService; return EndpointMapResponseParsingService;
}
}
export class RootEndpointRequest extends EndpointMapRequest {
constructor(uuid: string, EnvConfig: GlobalConfig) {
const href = new RESTURLCombiner(EnvConfig, '/').toString();
super(uuid, href);
} }
} }

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core';
import {
DSOSuccessResponse, RestResponse,
SearchSuccessResponse
} from '../cache/response-cache.models';
import { DSOResponseParsingService } from './dso-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { PageInfo } from '../shared/page-info.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
@Injectable()
export class SearchResponseParsingService implements ResponseParsingService {
constructor(private dsoParser: DSOResponseParsingService) {}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload;
const dsoSelfLinks = payload._embedded.objects
.map((object) => object._embedded.dspaceObject)
// we don't need embedded collections, bitstreamformats, etc for search results.
// And parsing them all takes up a lot of time. Throw them away to improve performance
// until objs until partial results are supported by the rest api
.map((dso) => Object.assign({}, dso, { _embedded: undefined }))
.map((dso) => this.dsoParser.parse(request, {
payload: dso,
statusCode: data.statusCode
}))
.map((obj) => obj.resourceSelfLinks)
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
const objects = payload._embedded.objects
.map((object, index) => Object.assign({}, object, { dspaceObject: dsoSelfLinks[index] }));
payload.objects = objects;
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
return new SearchSuccessResponse(deserialized, data.statusCode, undefined);
}
}

View File

@@ -18,7 +18,7 @@ describe('HALEndpointService', () => {
/* tslint:disable:no-shadowed-variable */ /* tslint:disable:no-shadowed-variable */
class TestService extends HALEndpointService { class TestService extends HALEndpointService {
protected linkName = 'test'; protected linkPath = 'test';
constructor(protected responseCache: ResponseCacheService, constructor(protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
@@ -29,7 +29,7 @@ describe('HALEndpointService', () => {
/* tslint:enable:no-shadowed-variable */ /* tslint:enable:no-shadowed-variable */
describe('getEndpointMap', () => { describe('getRootEndpointMap', () => {
beforeEach(() => { beforeEach(() => {
responseCache = jasmine.createSpyObj('responseCache', { responseCache = jasmine.createSpyObj('responseCache', {
get: hot('--a-', { get: hot('--a-', {
@@ -53,13 +53,13 @@ describe('HALEndpointService', () => {
}); });
it('should configure a new RootEndpointRequest', () => { it('should configure a new RootEndpointRequest', () => {
(service as any).getEndpointMap(); (service as any).getRootEndpointMap();
const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig); const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig);
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.configure).toHaveBeenCalledWith(expected);
}); });
it('should return an Observable of the endpoint map', () => { it('should return an Observable of the endpoint map', () => {
const result = (service as any).getEndpointMap(); const result = (service as any).getRootEndpointMap();
const expected = cold('--b-', { b: endpointMap }); const expected = cold('--b-', { b: endpointMap });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
@@ -74,18 +74,18 @@ describe('HALEndpointService', () => {
envConfig envConfig
); );
spyOn(service as any, 'getEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('--a-', { a: endpointMap })); .returnValue(hot('--a-', { a: endpointMap }));
}); });
it('should return the endpoint URL for the service\'s linkName', () => { it('should return the endpoint URL for the service\'s linkPath', () => {
const result = service.getEndpoint(); const result = service.getEndpoint();
const expected = cold('--b-', { b: endpointMap.test }); const expected = cold('--b-', { b: endpointMap.test });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should return undefined for a linkName that isn\'t in the endpoint map', () => { it('should return undefined for a linkPath that isn\'t in the endpoint map', () => {
(service as any).linkName = 'unknown'; (service as any).linkPath = 'unknown';
const result = service.getEndpoint(); const result = service.getEndpoint();
const expected = cold('--b-', { b: undefined }); const expected = cold('--b-', { b: undefined });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
@@ -103,8 +103,8 @@ describe('HALEndpointService', () => {
}); });
it('should return undefined as long as getEndpointMap hasn\'t fired', () => { it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => {
spyOn(service as any, 'getEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('----')); .returnValue(hot('----'));
const result = service.isEnabledOnRestApi(); const result = service.isEnabledOnRestApi();
@@ -112,8 +112,8 @@ describe('HALEndpointService', () => {
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should return true if the service\'s linkName is in the endpoint map', () => { it('should return true if the service\'s linkPath is in the endpoint map', () => {
spyOn(service as any, 'getEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('--a-', { a: endpointMap })); .returnValue(hot('--a-', { a: endpointMap }));
const result = service.isEnabledOnRestApi(); const result = service.isEnabledOnRestApi();
@@ -121,11 +121,11 @@ describe('HALEndpointService', () => {
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
}); });
it('should return false if the service\'s linkName isn\'t in the endpoint map', () => { it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => {
spyOn(service as any, 'getEndpointMap').and spyOn(service as any, 'getRootEndpointMap').and
.returnValue(hot('--a-', { a: endpointMap })); .returnValue(hot('--a-', { a: endpointMap }));
(service as any).linkName = 'unknown'; (service as any).linkPath = 'unknown';
const result = service.isEnabledOnRestApi(); const result = service.isEnabledOnRestApi();
const expected = cold('b-c-', { b: undefined, c: false }); const expected = cold('b-c-', { b: undefined, c: false });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);

View File

@@ -1,39 +1,62 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models'; import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models';
import { RootEndpointRequest } from '../data/request.models'; import { EndpointMapRequest, RootEndpointRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { isNotEmpty } from '../../shared/empty.util'; import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
export abstract class HALEndpointService { export abstract class HALEndpointService {
protected abstract responseCache: ResponseCacheService; protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
protected abstract linkName: string; protected abstract linkPath: string;
protected abstract EnvConfig: GlobalConfig; protected abstract EnvConfig: GlobalConfig;
protected getEndpointMap(): Observable<EndpointMap> { protected getRootHref(): string {
const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig); return new RESTURLCombiner(this.EnvConfig, '/').toString();
}
protected getRootEndpointMap(): Observable<EndpointMap> {
return this.getEndpointMapAt(this.getRootHref());
}
private getEndpointMapAt(href): Observable<EndpointMap> {
const request = new EndpointMapRequest(this.requestService.generateRequestId(), href);
this.requestService.configure(request); this.requestService.configure(request);
return this.responseCache.get(request.href) return this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response) .map((entry: ResponseCacheEntry) => entry.response)
.filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
.map((response: RootSuccessResponse) => response.endpointMap) .map((response: EndpointMapSuccessResponse) => response.endpointMap)
.distinctUntilChanged(); .distinctUntilChanged();
} }
public getEndpoint(): Observable<string> { public getEndpoint(): Observable<string> {
return this.getEndpointMap() return this.getEndpointAt(...this.linkPath.split('/'));
.map((map: EndpointMap) => map[this.linkName]) }
.distinctUntilChanged();
private getEndpointAt(...path: string[]): Observable<string> {
if (isEmpty(path)) {
path = ['/'];
}
const pipeArguments = path
.map((subPath: string) => [
flatMap((href: string) => this.getEndpointMapAt(href)),
map((endpointMap: EndpointMap) => endpointMap[subPath]),
])
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged());
} }
public isEnabledOnRestApi(): Observable<boolean> { public isEnabledOnRestApi(): Observable<boolean> {
return this.getEndpointMap() return this.getRootEndpointMap().pipe(
.map((map: EndpointMap) => isNotEmpty(map[this.linkName])) // TODO this only works when there's no / in linkPath
.startWith(undefined) map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[this.linkPath])),
.distinctUntilChanged(); startWith(undefined),
distinctUntilChanged()
)
} }
} }