diff --git a/src/app/+search-page/normalized-search-result.model.ts b/src/app/+search-page/normalized-search-result.model.ts new file mode 100644 index 0000000000..2e9a4a8b8e --- /dev/null +++ b/src/app/+search-page/normalized-search-result.model.ts @@ -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[]; + +} diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index bf069eee60..d6c6da3051 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -96,8 +96,8 @@ describe('SearchFacetFilterComponent', () => { link = comp.getSearchLink(); }); - it('should return the value of the searchLink variable in the filter service', () => { - expect(link).toEqual(filterService.searchLink); + it('should return the value of the uiSearchRoute variable in the filter service', () => { + expect(link).toEqual(filterService.uiSearchRoute); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 7371e55ee8..6dcf8d73b8 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -192,13 +192,13 @@ describe('SearchFilterService', () => { }); }); - describe('when the searchLink method is called', () => { + describe('when the uiSearchRoute method is called', () => { let link: string; beforeEach(() => { 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); }); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index f3efc19b86..8a909b6fa7 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -46,7 +46,7 @@ export class SearchFilterService { } get searchLink() { - return this.searchService.searchLink; + return this.searchService.uiSearchRoute; } isCollapsed(filterName: string): Observable { diff --git a/src/app/+search-page/search-service/search-query-response.model.ts b/src/app/+search-page/search-service/search-query-response.model.ts new file mode 100644 index 0000000000..b8948d963f --- /dev/null +++ b/src/app/+search-page/search-service/search-query-response.model.ts @@ -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; +} diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index c70fe22ce0..a0af63c28d 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -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 { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; 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 { 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 { PaginatedList } from '../../core/data/paginated-list'; +import { ResponseParsingService } from '../../core/data/parsing.service'; 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 { GenericConstructor } from '../../core/shared/generic-constructor'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Item } from '../../core/shared/item.model'; import { Metadatum } from '../../core/shared/metadatum.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 { FilterType } from './filter-type.model'; import { SearchFilterConfig } from './search-filter-config.model'; +import { SearchResponseParsingService } from '../../core/data/search-response-parsing.service'; function shuffle(array: any[]) { let i = 0; @@ -35,23 +49,11 @@ function shuffle(array: any[]) { } @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 sample abstract.', - 'This is a sample abstract. But, to fill up some space, here\'s "Hello" in several different languages : ', - 'This is a Sample HTML webpage including several images and styles (CSS).', - 'This is really just a sample abstract. But, Í’vé thrown ïn a cõuple of spëciâl charactèrs för êxtrå fuñ!', - 'This abstract is really quite great', - 'The solution structure of the bee venom neurotoxin', - 'BACKGROUND: The Open Archive Initiative (OAI) refers to a movement started around the \'90 s to guarantee free access to scientific information', - 'The collision fault detection of a XXY stage is proposed for the first time in this paper', - 'This was blank in the actual item, no abstract', - 'The QSAR DataBank (QsarDB) repository', - ); private sub; - searchLink = '/search'; + uiSearchRoute = '/search'; config: SearchFilterConfig[] = [ Object.assign(new SearchFilterConfig(), @@ -86,11 +88,16 @@ export class SearchService implements OnDestroy { // searchOptions: BehaviorSubject; searchOptions: SearchOptions; - constructor(private itemDataService: ItemDataService, - private routeService: RouteService, - private route: ActivatedRoute, - private router: Router) { - + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + private itemDataService: ItemDataService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private routeService: RouteService, + private route: ActivatedRoute, + private router: Router + ) { + super(); const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = 'search-results-pagination'; pagination.currentPage = 1; @@ -101,74 +108,18 @@ export class SearchService implements OnDestroy { } search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable>>> { - this.searchOptions = searchOptions; - let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`; - if (hasValue(scopeId)) { - self += `&scope=${scopeId}`; - } - 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 - .filter((rd: RemoteData>) => rd.hasSucceeded) - .map((rd: RemoteData>) => { - - const totalElements = rd.payload.totalElements > 20 ? 20 : rd.payload.totalElements; - - const page = shuffle(rd.payload.page) - .map((item: Item, index: number) => { - const mockResult: SearchResult = 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 - )); + const searchEndpointUrlObs = this.getEndpoint(); + searchEndpointUrlObs.pipe( + map((url: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return SearchResponseParsingService; + } + }); + }) + ).subscribe((request: RestRequest) => this.requestService.configure(request)); + return Observable.of(undefined); } getConfig(): Observable> { @@ -231,7 +182,7 @@ export class SearchService implements OnDestroy { queryParamsHandling: 'merge' }; - this.router.navigate([this.searchLink], navigationExtras); + this.router.navigate([this.uiSearchRoute], navigationExtras); } getClearFiltersQueryParams(): any { @@ -249,7 +200,7 @@ export class SearchService implements OnDestroy { } getSearchLink() { - return this.searchLink; + return this.uiSearchRoute; } ngOnDestroy(): void { diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 764107837b..2385948b2f 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -110,12 +110,12 @@ describe('BrowseService', () => { .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 linkName = 'items'; - const expectedURL = browseDefinitions[0]._links[linkName]; + const linkPath = 'items'; + 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 }); expect(result).toBeObservable(expected); @@ -123,10 +123,10 @@ describe('BrowseService', () => { 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 linkName = 'items'; - const expectedURL = browseDefinitions[1]._links[linkName]; + const linkPath = 'items'; + 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 }); expect(result).toBeObservable(expected); @@ -134,30 +134,30 @@ describe('BrowseService', () => { it('should throw an error when the key doesn\'t match', () => { const metadatumKey = 'dc.title'; // isn't in the definitions - const linkName = 'items'; + const linkPath = 'items'; - const result = service.getBrowseURLFor(metadatumKey, linkName); - const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadatumKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); it('should throw an error when the link doesn\'t match', () => { 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 expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); + const result = service.getBrowseURLFor(metadatumKey, linkPath); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); it('should configure a new BrowseEndpointRequest', () => { const metadatumKey = 'dc.date.issued'; - const linkName = 'items'; + const linkPath = 'items'; const expected = new BrowseEndpointRequest(requestService.generateRequestId(), browsesEndpointURL); - scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); + scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkPath).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); @@ -175,9 +175,9 @@ describe('BrowseService', () => { .returnValue(hot('----')); 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 }); expect(result).toBeObservable(expected); }); @@ -192,9 +192,9 @@ describe('BrowseService', () => { .returnValue(hot('--a-', { a: browsesEndpointURL })); 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`)); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index a321e14706..3b283e2d93 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -13,7 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class BrowseService extends HALEndpointService { - protected linkName = 'browses'; + protected linkPath = 'browses'; private static toSearchKeyArray(metadatumKey: string): string[] { const keyParts = metadatumKey.split('.'); @@ -35,7 +35,7 @@ export class BrowseService extends HALEndpointService { super(); } - getBrowseURLFor(metadatumKey: string, linkName: string): Observable { + getBrowseURLFor(metadatumKey: string, linkPath: string): Observable { const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); return this.getEndpoint() .filter((href: string) => isNotEmpty(href)) @@ -59,10 +59,10 @@ export class BrowseService extends HALEndpointService { return isNotEmpty(matchingKeys); }) ).map((def: BrowseDefinition) => { - if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) { - throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`); + if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { + throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`); } else { - return def._links[linkName]; + return def._links[linkPath]; } }) ); diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 06fc26aa67..fb9f10ce51 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -1,3 +1,4 @@ +import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; import { BrowseDefinition } from '../shared/browse-definition.model'; @@ -21,11 +22,21 @@ export class DSOSuccessResponse extends RestResponse { } } -export class EndpointMap { - [linkName: string]: string +export class SearchSuccessResponse extends RestResponse { + 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( public endpointMap: EndpointMap, public statusCode: string, diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index b0c364a86e..3cdb22948f 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -11,7 +11,7 @@ const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; class TestService extends ConfigService { - protected linkName = LINK_NAME; + protected linkPath = LINK_NAME; protected browseEndpoint = BROWSE; constructor( diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 9ad4684300..a8c2bc46bf 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -16,7 +16,7 @@ export abstract class ConfigService extends HALEndpointService { protected request: ConfigRequest; protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; protected abstract browseEndpoint: string; diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts index 4857569236..9655576e71 100644 --- a/src/app/core/config/submission-definitions-config.service.ts +++ b/src/app/core/config/submission-definitions-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionDefinitionsConfigService extends ConfigService { - protected linkName = 'submissiondefinitions'; + protected linkPath = 'submissiondefinitions'; protected browseEndpoint = 'search/findByCollection'; constructor( diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts index 5e992146ee..7209cd0fdf 100644 --- a/src/app/core/config/submission-forms-config.service.ts +++ b/src/app/core/config/submission-forms-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionFormsConfigService extends ConfigService { - protected linkName = 'submissionforms'; + protected linkPath = 'submissionforms'; protected browseEndpoint = ''; constructor( diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts index 96a8557e9c..108fc30259 100644 --- a/src/app/core/config/submission-sections-config.service.ts +++ b/src/app/core/config/submission-sections-config.service.ts @@ -8,7 +8,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; @Injectable() export class SubmissionSectionsConfigService extends ConfigService { - protected linkName = 'submissionsections'; + protected linkPath = 'submissionsections'; protected browseEndpoint = ''; constructor( diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 768f05f24b..2960918ea7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -17,7 +17,9 @@ import { isNotEmpty } from '../shared/empty.util'; import { ApiService } from '../shared/api.service'; import { CollectionDataService } from './data/collection-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 { SearchResponseParsingService } from './data/search-response-parsing.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { HostWindowService } from '../shared/host-window.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 { RequestService } from './data/request.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 { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; import { BrowseService } from './browse/browse.service'; @@ -67,7 +69,9 @@ const PROVIDERS = [ RemoteDataBuildService, RequestService, ResponseCacheService, - RootResponseParsingService, + EndpointMapResponseParsingService, + DebugResponseParsingService, + SearchResponseParsingService, ServerResponseService, BrowseResponseParsingService, BrowseService, diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index f9f581128b..a0f246aedc 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -13,7 +13,7 @@ import { RequestService } from './request.service'; @Injectable() export class CollectionDataService extends ComColDataService { - protected linkName = 'collections'; + protected linkPath = 'collections'; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index fefe7d3730..0476a901b4 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -22,7 +22,7 @@ class NormalizedTestObject implements CacheableObject { } class TestService extends ComColDataService { - protected linkName = LINK_NAME; + protected linkPath = LINK_NAME; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 68981121c1..cc9ef4ad60 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -17,7 +17,7 @@ export abstract class ComColDataService this.objectCache.getByUUID(scopeID, NormalizedCommunity)) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .map((nc: NormalizedCommunity) => nc._links[this.linkPath]) .filter((href) => isNotEmpty(href)) ).distinctUntilChanged(); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index bbee96ab47..dce67e936f 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -13,7 +13,7 @@ import { RequestService } from './request.service'; @Injectable() export class CommunityDataService extends ComColDataService { - protected linkName = 'communities'; + protected linkPath = 'communities'; protected cds = this; constructor( diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 2d003d6fd1..ccdbbea5b9 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -19,7 +19,7 @@ export abstract class DataService protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; constructor( diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts new file mode 100644 index 0000000000..d530948559 --- /dev/null +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -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; + } +} diff --git a/src/app/core/data/root-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts similarity index 76% rename from src/app/core/data/root-response-parsing.service.ts rename to src/app/core/data/endpoint-map-response-parsing.service.ts index a3e7fc22a3..b850e13932 100644 --- a/src/app/core/data/root-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,15 +1,14 @@ import { Inject, Injectable } from '@angular/core'; import { GLOBAL_CONFIG } from '../../../config'; 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 { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { isNotEmpty } from '../../shared/empty.util'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; @Injectable() -export class RootResponseParsingService implements ResponseParsingService { +export class EndpointMapResponseParsingService implements ResponseParsingService { constructor( @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, ) { @@ -21,7 +20,7 @@ export class RootResponseParsingService implements ResponseParsingService { for (const link of Object.keys(links)) { links[link] = links[link].href; } - return new RootSuccessResponse(links, data.statusCode); + return new EndpointMapSuccessResponse(links, data.statusCode); } else { return new ErrorResponse( Object.assign( diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 7e978e0879..155ba357c6 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -17,7 +17,7 @@ import { RequestService } from './request.service'; @Injectable() export class ItemDataService extends DataService { - protected linkName = 'items'; + protected linkPath = 'items'; constructor( protected responseCache: ResponseCacheService, @@ -34,7 +34,7 @@ export class ItemDataService extends DataService { if (isEmpty(scopeID)) { return this.getEndpoint(); } else { - return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) + return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath) .filter((href: string) => isNotEmpty(href)) .map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()) .distinctUntilChanged(); diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ee37f9c3d4..e9d200e15d 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -4,7 +4,7 @@ import { GlobalConfig } from '../../../config/global-config.interface'; import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { DSOResponseParsingService } from './dso-response-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 { ConfigResponseParsingService } from './config-response-parsing.service'; @@ -140,14 +140,24 @@ export class FindAllRequest extends GetRequest { } } -export class RootEndpointRequest extends GetRequest { - constructor(uuid: string, EnvConfig: GlobalConfig) { - const href = new RESTURLCombiner(EnvConfig, '/').toString(); - super(uuid, href); +export class EndpointMapRequest extends GetRequest { + constructor( + public uuid: string, + public href: string, + public body?: any + ) { + super(uuid, href, body); } getResponseParser(): GenericConstructor { - return RootResponseParsingService; + return EndpointMapResponseParsingService; + } +} + +export class RootEndpointRequest extends EndpointMapRequest { + constructor(uuid: string, EnvConfig: GlobalConfig) { + const href = new RESTURLCombiner(EnvConfig, '/').toString(); + super(uuid, href); } } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts new file mode 100644 index 0000000000..bd64101397 --- /dev/null +++ b/src/app/core/data/search-response-parsing.service.ts @@ -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); + } + +} diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index a47bfd745c..39652d6e7c 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -18,7 +18,7 @@ describe('HALEndpointService', () => { /* tslint:disable:no-shadowed-variable */ class TestService extends HALEndpointService { - protected linkName = 'test'; + protected linkPath = 'test'; constructor(protected responseCache: ResponseCacheService, protected requestService: RequestService, @@ -29,7 +29,7 @@ describe('HALEndpointService', () => { /* tslint:enable:no-shadowed-variable */ - describe('getEndpointMap', () => { + describe('getRootEndpointMap', () => { beforeEach(() => { responseCache = jasmine.createSpyObj('responseCache', { get: hot('--a-', { @@ -53,13 +53,13 @@ describe('HALEndpointService', () => { }); it('should configure a new RootEndpointRequest', () => { - (service as any).getEndpointMap(); + (service as any).getRootEndpointMap(); const expected = new RootEndpointRequest(requestService.generateRequestId(), envConfig); expect(requestService.configure).toHaveBeenCalledWith(expected); }); 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 }); expect(result).toBeObservable(expected); }); @@ -74,18 +74,18 @@ describe('HALEndpointService', () => { envConfig ); - spyOn(service as any, 'getEndpointMap').and + spyOn(service as any, 'getRootEndpointMap').and .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 expected = cold('--b-', { b: endpointMap.test }); expect(result).toBeObservable(expected); }); - it('should return undefined for a linkName that isn\'t in the endpoint map', () => { - (service as any).linkName = 'unknown'; + it('should return undefined for a linkPath that isn\'t in the endpoint map', () => { + (service as any).linkPath = 'unknown'; const result = service.getEndpoint(); const expected = cold('--b-', { b: undefined }); expect(result).toBeObservable(expected); @@ -103,8 +103,8 @@ describe('HALEndpointService', () => { }); - it('should return undefined as long as getEndpointMap hasn\'t fired', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return undefined as long as getRootEndpointMap hasn\'t fired', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('----')); const result = service.isEnabledOnRestApi(); @@ -112,8 +112,8 @@ describe('HALEndpointService', () => { expect(result).toBeObservable(expected); }); - it('should return true if the service\'s linkName is in the endpoint map', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return true if the service\'s linkPath is in the endpoint map', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); const result = service.isEnabledOnRestApi(); @@ -121,11 +121,11 @@ describe('HALEndpointService', () => { expect(result).toBeObservable(expected); }); - it('should return false if the service\'s linkName isn\'t in the endpoint map', () => { - spyOn(service as any, 'getEndpointMap').and + it('should return false if the service\'s linkPath isn\'t in the endpoint map', () => { + spyOn(service as any, 'getRootEndpointMap').and .returnValue(hot('--a-', { a: endpointMap })); - (service as any).linkName = 'unknown'; + (service as any).linkPath = 'unknown'; const result = service.isEnabledOnRestApi(); const expected = cold('b-c-', { b: undefined, c: false }); expect(result).toBeObservable(expected); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 84587f1eea..c6c3ff284d 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,39 +1,62 @@ import { Observable } from 'rxjs/Observable'; +import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators'; 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 { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models'; +import { EndpointMapRequest, RootEndpointRequest } from '../data/request.models'; 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 { protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; - protected abstract linkName: string; + protected abstract linkPath: string; protected abstract EnvConfig: GlobalConfig; - protected getEndpointMap(): Observable { - const request = new RootEndpointRequest(this.requestService.generateRequestId(), this.EnvConfig); + protected getRootHref(): string { + return new RESTURLCombiner(this.EnvConfig, '/').toString(); + } + + protected getRootEndpointMap(): Observable { + return this.getEndpointMapAt(this.getRootHref()); + } + + private getEndpointMapAt(href): Observable { + const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); 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) + .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) + .map((response: EndpointMapSuccessResponse) => response.endpointMap) .distinctUntilChanged(); } public getEndpoint(): Observable { - return this.getEndpointMap() - .map((map: EndpointMap) => map[this.linkName]) - .distinctUntilChanged(); + return this.getEndpointAt(...this.linkPath.split('/')); + } + + private getEndpointAt(...path: string[]): Observable { + 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 { - return this.getEndpointMap() - .map((map: EndpointMap) => isNotEmpty(map[this.linkName])) - .startWith(undefined) - .distinctUntilChanged(); + return this.getRootEndpointMap().pipe( + // TODO this only works when there's no / in linkPath + map((endpointMap: EndpointMap) => isNotEmpty(endpointMap[this.linkPath])), + startWith(undefined), + distinctUntilChanged() + ) } }