From 067a95fc88bf5afe1b90cb3c0dd9faf2c964f8c7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 8 Nov 2017 10:02:15 +0100 Subject: [PATCH] Parse embedded data --- src/app/core/cache/response-cache.models.ts | 3 +- src/app/core/config/config-data.ts | 13 ++ src/app/core/config/config.service.ts | 53 +++---- .../data/base-response-parsing.service.ts | 135 +++++++++++++++++ .../data/config-response-parsing.service.ts | 37 +++-- .../core/data/dso-response-parsing.service.ts | 143 ++---------------- .../config-submission-definitions.model.ts | 7 +- src/app/core/shared/config/config-type.ts | 2 + src/app/core/shared/config/config.model.ts | 6 + 9 files changed, 217 insertions(+), 182 deletions(-) create mode 100644 src/app/core/config/config-data.ts create mode 100644 src/app/core/data/base-response-parsing.service.ts diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index ed2bd3ab77..06fc26aa67 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -56,7 +56,8 @@ export class ErrorResponse extends RestResponse { export class ConfigSuccessResponse extends RestResponse { constructor( public configDefinition: ConfigObject[], - public statusCode: string + public statusCode: string, + public pageInfo?: PageInfo ) { super(true, statusCode); } diff --git a/src/app/core/config/config-data.ts b/src/app/core/config/config-data.ts new file mode 100644 index 0000000000..efcdb7eed4 --- /dev/null +++ b/src/app/core/config/config-data.ts @@ -0,0 +1,13 @@ +import { PageInfo } from '../shared/page-info.model'; +import { ConfigObject } from '../shared/config/config.model'; + +/** + * A class to represent the data retrieved by a configuration service + */ +export class ConfigData { + constructor( + public pageInfo: PageInfo, + public payload: ConfigObject[] + ) { + } +} diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index dc689c6e4e..8cc590e1ae 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -1,17 +1,19 @@ import { Injectable } from '@angular/core'; + 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 { ConfigSuccessResponse, EndpointMap, RootSuccessResponse } from '../cache/response-cache.models'; -import { ConfigRequest, FindAllOptions, RestRequest, RootEndpointRequest } from '../data/request.models'; +import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { ConfigObject } from '../shared/config/config.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigData } from './config-data'; @Injectable() -export abstract class ConfigService { +export abstract class ConfigService extends HALEndpointService { protected request: ConfigRequest; protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; @@ -19,12 +21,17 @@ export abstract class ConfigService { protected abstract EnvConfig: GlobalConfig; protected abstract browseEndpoint: string; - protected getConfig(request: RestRequest): Observable { - return this.responseCache.get(request.href) + protected getConfig(request: RestRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't retrieve the config`))), + successResponse .filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition)) - .map((response: ConfigSuccessResponse) => response.configDefinition) - .distinctUntilChanged(); + .map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) + .distinctUntilChanged()); } protected getConfigByIDHref(endpoint, resourceID): string { @@ -36,7 +43,7 @@ export abstract class ConfigService { const args = []; if (hasValue(options.scopeID)) { - result = `${endpoint}/${this.browseEndpoint}` + result = `${endpoint}/${this.browseEndpoint}`; args.push(`uuid=${options.scopeID}`); } else { result = endpoint; @@ -65,19 +72,7 @@ export abstract class ConfigService { return result; } - protected getEndpointMap(): Observable { - 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 getConfigAll(): Observable { + public getConfigAll(): Observable { return this.getEndpoint() .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() @@ -91,16 +86,16 @@ export abstract class ConfigService { .distinctUntilChanged(); } - public getConfigByHref(href: string): Observable { + public getConfigByHref(href: string): Observable { const request = new ConfigRequest(href); this.requestService.configure(request); return this.getConfig(request); } - public getConfigById(id: string): Observable { + public getConfigByName(name: string): Observable { return this.getEndpoint() - .map((endpoint: string) => this.getConfigByIDHref(endpoint, id)) + .map((endpoint: string) => this.getConfigByIDHref(endpoint, name)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(endpointURL)) @@ -113,7 +108,7 @@ export abstract class ConfigService { .distinctUntilChanged(); } - public getConfigBySearch(options: FindAllOptions = {}): Observable { + public getConfigBySearch(options: FindAllOptions = {}): Observable { return this.getEndpoint() .map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) .filter((href: string) => isNotEmpty(href)) @@ -128,10 +123,4 @@ export abstract class ConfigService { .distinctUntilChanged(); } - public getEndpoint(): Observable { - return this.getEndpointMap() - .map((map: EndpointMap) => map[this.linkName]) - .distinctUntilChanged(); - } - } diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts new file mode 100644 index 0000000000..d8a4221420 --- /dev/null +++ b/src/app/core/data/base-response-parsing.service.ts @@ -0,0 +1,135 @@ +import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { PageInfo } from '../shared/page-info.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { GenericConstructor } from '../shared/generic-constructor'; + +function isObjectLevel(halObj: any) { + return isNotEmpty(halObj._links) && hasValue(halObj._links.self); +} + +function isPaginatedResponse(halObj: any) { + return isNotEmpty(halObj.page) && hasValue(halObj._embedded); +} + +/* tslint:disable:max-classes-per-file */ + +class ProcessRequestDTO { + [key: string]: ObjectDomain[] +} + +export abstract class BaseResponseParsingService { + protected abstract EnvConfig: GlobalConfig; + protected abstract objectCache: ObjectCacheService; + protected abstract objectFactory: any; + protected abstract toCache: boolean; + + protected process(data: any, requestHref: string): ProcessRequestDTO { + + if (isNotEmpty(data)) { + if (isPaginatedResponse(data)) { + return this.process(data._embedded, requestHref); + } else if (isObjectLevel(data)) { + return { topLevel: this.deserializeAndCache(data, requestHref) }; + } else { + const result = new ProcessRequestDTO(); + if (Array.isArray(data)) { + result.topLevel = []; + data.forEach((datum) => { + if (isPaginatedResponse(datum)) { + const obj = this.process(datum, requestHref); + result.topLevel = [...result.topLevel, ...this.flattenSingleKeyObject(obj)]; + } else { + result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)]; + } + }); + } else { + Object.keys(data) + .filter((property) => data.hasOwnProperty(property)) + .filter((property) => hasValue(data[property])) + .forEach((property) => { + if (isPaginatedResponse(data[property])) { + const obj = this.process(data[property], requestHref); + result[property] = this.flattenSingleKeyObject(obj); + } else { + result[property] = this.deserializeAndCache(data[property], requestHref); + } + }); + } + return result; + } + } + } + + protected deserializeAndCache(obj, requestHref: string): ObjectDomain[] { + if (Array.isArray(obj)) { + let result = []; + obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)]); + return result; + } + + const type: ObjectType = obj.type; + if (hasValue(type)) { + const normObjConstructor = this.objectFactory.getConstructor(type) as GenericConstructor; + + if (hasValue(normObjConstructor)) { + const serializer = new DSpaceRESTv2Serializer(normObjConstructor); + + let processed; + if (isNotEmpty(obj._embedded)) { + processed = this.process(obj._embedded, requestHref); + } + const normalizedObj: any = serializer.deserialize(obj); + + if (isNotEmpty(processed)) { + const processedList = {}; + Object.keys(processed).forEach((key) => { + processedList[key] = processed[key].map((no: NormalizedObject) => (this.toCache) ? no.self : no); + }); + Object.assign(normalizedObj, processedList); + } + + if (this.toCache) { + this.addToObjectCache(normalizedObj, requestHref); + } + return [normalizedObj] as any; + + } else { + // TODO: move check to Validator? + // throw new Error(`The server returned an object with an unknown a known type: ${type}`); + return []; + } + + } else { + // TODO: move check to Validator + // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`); + return []; + } + } + + protected addToObjectCache(co: CacheableObject, requestHref: string): void { + if (hasNoValue(co) || hasNoValue(co.self)) { + throw new Error('The server returned an invalid object'); + } + this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref); + } + + protected processPageInfo(pageObj: any): PageInfo { + if (isNotEmpty(pageObj)) { + return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); + } else { + return undefined; + } + } + + protected flattenSingleKeyObject(obj: any): any { + const keys = Object.keys(obj); + if (keys.length !== 1) { + throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`); + } + return obj[keys[0]]; + } +} diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 243c639149..90e6dc055e 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -1,31 +1,35 @@ -import { Injectable } from '@angular/core'; +import { Inject, 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 { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { isNotEmpty } from '../../shared/empty.util'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { ConfigObjectFactory } from '../shared/config/config-object-factory'; +import { ConfigObject } from '../shared/config/config.model'; +import { ConfigType } from '../shared/config/config-type'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; + @Injectable() -export class ConfigResponseParsingService implements ResponseParsingService { +export class ConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = ConfigObjectFactory; + protected toCache = false; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - let configDefinition; - let payload; - let type; - if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - type = Object.keys(data.payload._embedded)[0]; - payload = data.payload._embedded[Object.keys(data.payload._embedded)[0]]; - } else { - type = data.payload.type; - payload = [data.payload]; - } - const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(type)); - configDefinition = serializer.deserializeArray(payload); - return new ConfigSuccessResponse(configDefinition, data.statusCode); + const configDefinition = this.process(data.payload, request.href); + return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page)); } else { return new ErrorResponse( Object.assign( @@ -35,4 +39,5 @@ export class ConfigResponseParsingService implements ResponseParsingService { ); } } + } diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index b7929498f1..11590d0431 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -1,149 +1,34 @@ -import { ObjectCacheService } from '../cache/object-cache.service'; import { Inject, Injectable } from '@angular/core'; + +import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../config'; import { NormalizedObject } from '../cache/models/normalized-object.model'; -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { ResourceType } from '../shared/resource-type'; import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory'; -import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { CacheableObject } from '../cache/object-cache.reducer'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { RestResponse, DSOSuccessResponse } from '../cache/response-cache.models'; import { RestRequest } from './request.models'; -import { PageInfo } from '../shared/page-info.model'; + import { ResponseParsingService } from './parsing.service'; - -function isObjectLevel(halObj: any) { - return isNotEmpty(halObj._links) && hasValue(halObj._links.self); -} - -function isPaginatedResponse(halObj: any) { - return isNotEmpty(halObj.page) && hasValue(halObj._embedded); -} - -function flattenSingleKeyObject(obj: any): any { - const keys = Object.keys(obj); - if (keys.length !== 1) { - throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`); - } - return obj[keys[0]]; -} - -/* tslint:disable:max-classes-per-file */ -class ProcessRequestDTO { - [key: string]: NormalizedObject[] -} +import { BaseResponseParsingService } from './base-response-parsing.service'; @Injectable() -export class DSOResponseParsingService implements ResponseParsingService { +export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = NormalizedObjectFactory; + protected toCache = true; + constructor( - @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig, - private objectCache: ObjectCacheService, - ) { + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const processRequestDTO = this.process(data.payload, request.href); - const selfLinks = flattenSingleKeyObject(processRequestDTO).map((no) => no.self); + const processRequestDTO = this.process(data.payload, request.href); + const selfLinks = this.flattenSingleKeyObject(processRequestDTO).map((no) => no.self); return new DSOSuccessResponse(selfLinks, data.statusCode, this.processPageInfo(data.payload.page)) } - protected process(data: any, requestHref: string): ProcessRequestDTO { - - if (isNotEmpty(data)) { - if (isPaginatedResponse(data)) { - return this.process(data._embedded, requestHref); - } else if (isObjectLevel(data)) { - return { topLevel: this.deserializeAndCache(data, requestHref) }; - } else { - const result = new ProcessRequestDTO(); - if (Array.isArray(data)) { - result.topLevel = []; - data.forEach((datum) => { - if (isPaginatedResponse(datum)) { - const obj = this.process(datum, requestHref); - result.topLevel = [...result.topLevel, ...flattenSingleKeyObject(obj)]; - } else { - result.topLevel = [...result.topLevel, ...this.deserializeAndCache(datum, requestHref)]; - } - }); - } else { - Object.keys(data) - .filter((property) => data.hasOwnProperty(property)) - .filter((property) => hasValue(data[property])) - .forEach((property) => { - if (isPaginatedResponse(data[property])) { - const obj = this.process(data[property], requestHref); - result[property] = flattenSingleKeyObject(obj); - } else { - result[property] = this.deserializeAndCache(data[property], requestHref); - } - }); - } - return result; - } - } - } - - protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] { - if (Array.isArray(obj)) { - let result = []; - obj.forEach((o) => result = [...result, ...this.deserializeAndCache(o, requestHref)]) - return result; - } - - const type: ResourceType = obj.type; - if (hasValue(type)) { - const normObjConstructor = NormalizedObjectFactory.getConstructor(type); - - if (hasValue(normObjConstructor)) { - const serializer = new DSpaceRESTv2Serializer(normObjConstructor); - - let processed; - if (isNotEmpty(obj._embedded)) { - processed = this.process(obj._embedded, requestHref); - } - const normalizedObj = serializer.deserialize(obj); - - if (isNotEmpty(processed)) { - const linksOnly = {}; - Object.keys(processed).forEach((key) => { - linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self); - }); - Object.assign(normalizedObj, linksOnly); - } - - this.addToObjectCache(normalizedObj, requestHref); - return [normalizedObj]; - - } else { - // TODO: move check to Validator? - // throw new Error(`The server returned an object with an unknown a known type: ${type}`); - return []; - } - - } else { - // TODO: move check to Validator - // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`); - return []; - } - } - - protected addToObjectCache(co: CacheableObject, requestHref: string): void { - if (hasNoValue(co) || hasNoValue(co.self)) { - throw new Error('The server returned an invalid object'); - } - this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref); - } - - protected processPageInfo(pageObj: any): PageInfo { - if (isNotEmpty(pageObj)) { - return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); - } else { - return undefined; - } - } - } -/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/shared/config/config-submission-definitions.model.ts b/src/app/core/shared/config/config-submission-definitions.model.ts index 271ab7281b..8249d2b118 100644 --- a/src/app/core/shared/config/config-submission-definitions.model.ts +++ b/src/app/core/shared/config/config-submission-definitions.model.ts @@ -1,7 +1,6 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { ConfigObject } from './config.model'; import { SubmissionSectionModel } from './config-submission-section.model'; -import { RemoteData } from '../../data/remote-data'; @inheritSerialization(ConfigObject) export class SubmissionDefinitionsModel extends ConfigObject { @@ -9,7 +8,7 @@ export class SubmissionDefinitionsModel extends ConfigObject { @autoserialize isDefault: boolean; - @autoserialize - sections: RemoteData; + @autoserializeAs(SubmissionSectionModel) + sections: SubmissionSectionModel[]; } diff --git a/src/app/core/shared/config/config-type.ts b/src/app/core/shared/config/config-type.ts index d4d88b8a60..ab0a18e516 100644 --- a/src/app/core/shared/config/config-type.ts +++ b/src/app/core/shared/config/config-type.ts @@ -2,6 +2,8 @@ * TODO replace with actual string enum after upgrade to TypeScript 2.4: * https://github.com/Microsoft/TypeScript/pull/15486 */ +import { ResourceType } from '../resource-type'; + export enum ConfigType { SubmissionDefinitions = 'submissiondefinitions', SubmissionDefinition = 'submissiondefinition', diff --git a/src/app/core/shared/config/config.model.ts b/src/app/core/shared/config/config.model.ts index e41e2742a2..8d86f317e1 100644 --- a/src/app/core/shared/config/config.model.ts +++ b/src/app/core/shared/config/config.model.ts @@ -12,4 +12,10 @@ export abstract class ConfigObject { public _links: { [name: string]: string } + + /** + * The link to the rest endpoint where this config object can be found + */ + @autoserialize + self: string; }