From 01f26bd8c25194e4fa7011dc1dd9e9d558b55f93 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 26 Oct 2017 15:04:03 +0200 Subject: [PATCH 1/8] Add config endpoint service --- src/app/core/cache/response-cache.models.ts | 10 ++ src/app/core/config/config.service.ts | 137 ++++++++++++++++++ .../submission-definitions-config.service.ts | 21 +++ .../config/submission-forms-config.service.ts | 21 +++ .../submission-sections-config.service.ts | 21 +++ src/app/core/core.module.ts | 8 + .../data/config-response-parsing.service.ts | 36 +++++ src/app/core/data/request.models.ts | 11 ++ .../dspace-rest-v2-response.model.ts | 1 + .../shared/config/config-object-factory.ts | 30 ++++ .../config-submission-definitions.model.ts | 15 ++ .../config/config-submission-forms.model.ts | 10 ++ .../config/config-submission-section.model.ts | 16 ++ src/app/core/shared/config/config-type.ts | 12 ++ src/app/core/shared/config/config.model.ts | 15 ++ 15 files changed, 364 insertions(+) create mode 100644 src/app/core/config/config.service.ts create mode 100644 src/app/core/config/submission-definitions-config.service.ts create mode 100644 src/app/core/config/submission-forms-config.service.ts create mode 100644 src/app/core/config/submission-sections-config.service.ts create mode 100644 src/app/core/data/config-response-parsing.service.ts create mode 100644 src/app/core/shared/config/config-object-factory.ts create mode 100644 src/app/core/shared/config/config-submission-definitions.model.ts create mode 100644 src/app/core/shared/config/config-submission-forms.model.ts create mode 100644 src/app/core/shared/config/config-submission-section.model.ts create mode 100644 src/app/core/shared/config/config-type.ts create mode 100644 src/app/core/shared/config/config.model.ts diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 8444a86490..96c1e66f83 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -1,5 +1,6 @@ import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; +import { ConfigObject } from '../shared/config/config.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -41,4 +42,13 @@ export class ErrorResponse extends RestResponse { this.errorMessage = error.message; } } + +export class ConfigSuccessResponse extends RestResponse { + constructor( + public configDefinition: ConfigObject[], + public statusCode: string + ) { + super(true, statusCode); + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts new file mode 100644 index 0000000000..dc689c6e4e --- /dev/null +++ b/src/app/core/config/config.service.ts @@ -0,0 +1,137 @@ +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 { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { ConfigObject } from '../shared/config/config.model'; + +@Injectable() +export abstract class ConfigService { + protected request: ConfigRequest; + protected abstract responseCache: ResponseCacheService; + protected abstract requestService: RequestService; + protected abstract linkName: string; + protected abstract EnvConfig: GlobalConfig; + protected abstract browseEndpoint: string; + + protected getConfig(request: RestRequest): Observable { + return this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition)) + .map((response: ConfigSuccessResponse) => response.configDefinition) + .distinctUntilChanged(); + } + + protected getConfigByIDHref(endpoint, resourceID): string { + return `${endpoint}/${resourceID}`; + } + + protected getConfigSearchHref(endpoint, options: FindAllOptions = {}): string { + let result; + const args = []; + + if (hasValue(options.scopeID)) { + result = `${endpoint}/${this.browseEndpoint}` + args.push(`uuid=${options.scopeID}`); + } else { + result = endpoint; + } + + if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { + /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ + args.push(`page=${options.currentPage - 1}`); + } + + if (hasValue(options.elementsPerPage)) { + args.push(`size=${options.elementsPerPage}`); + } + + if (hasValue(options.sort)) { + let direction = 'asc'; + if (options.sort.direction === 1) { + direction = 'desc'; + } + args.push(`sort=${options.sort.field},${direction}`); + } + + if (isNotEmpty(args)) { + result = `${result}?${args.join('&')}`; + } + 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 { + return this.getEndpoint() + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .do((request: RestRequest) => { + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }) + .flatMap((request: RestRequest) => this.getConfig(request)) + .distinctUntilChanged(); + } + + public getConfigByHref(href: string): Observable { + const request = new ConfigRequest(href); + this.requestService.configure(request); + + return this.getConfig(request); + } + + public getConfigById(id: string): Observable { + return this.getEndpoint() + .map((endpoint: string) => this.getConfigByIDHref(endpoint, id)) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .do((request: RestRequest) => { + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }) + .flatMap((request: RestRequest) => this.getConfig(request)) + .distinctUntilChanged(); + } + + public getConfigBySearch(options: FindAllOptions = {}): Observable { + return this.getEndpoint() + .map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new ConfigRequest(endpointURL)) + .do((request: RestRequest) => { + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }) + .flatMap((request: RestRequest) => this.getConfig(request)) + .distinctUntilChanged(); + } + + public getEndpoint(): Observable { + return this.getEndpointMap() + .map((map: EndpointMap) => map[this.linkName]) + .distinctUntilChanged(); + } + +} diff --git a/src/app/core/config/submission-definitions-config.service.ts b/src/app/core/config/submission-definitions-config.service.ts new file mode 100644 index 0000000000..4857569236 --- /dev/null +++ b/src/app/core/config/submission-definitions-config.service.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ConfigService } from './config.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; + +@Injectable() +export class SubmissionDefinitionsConfigService extends ConfigService { + protected linkName = 'submissiondefinitions'; + protected browseEndpoint = 'search/findByCollection'; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + super(); + } + +} diff --git a/src/app/core/config/submission-forms-config.service.ts b/src/app/core/config/submission-forms-config.service.ts new file mode 100644 index 0000000000..5e992146ee --- /dev/null +++ b/src/app/core/config/submission-forms-config.service.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ConfigService } from './config.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; + +@Injectable() +export class SubmissionFormsConfigService extends ConfigService { + protected linkName = 'submissionforms'; + protected browseEndpoint = ''; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + super(); + } + +} diff --git a/src/app/core/config/submission-sections-config.service.ts b/src/app/core/config/submission-sections-config.service.ts new file mode 100644 index 0000000000..96a8557e9c --- /dev/null +++ b/src/app/core/config/submission-sections-config.service.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ConfigService } from './config.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; + +@Injectable() +export class SubmissionSectionsConfigService extends ConfigService { + protected linkName = 'submissionsections'; + protected browseEndpoint = ''; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + super(); + } + +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index b782f1d4fc..7289c47a80 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -30,6 +30,10 @@ import { ResponseCacheService } from './cache/response-cache.service'; import { RootResponseParsingService } from './data/root-response-parsing.service'; import { ServerResponseService } from '../shared/server-response.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; +import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; +import { ConfigResponseParsingService } from './data/config-response-parsing.service'; +import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; +import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; const IMPORTS = [ CommonModule, @@ -61,6 +65,10 @@ const PROVIDERS = [ ResponseCacheService, RootResponseParsingService, ServerResponseService, + ConfigResponseParsingService, + SubmissionDefinitionsConfigService, + SubmissionFormsConfigService, + SubmissionSectionsConfigService, { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts new file mode 100644 index 0000000000..1eb4da2131 --- /dev/null +++ b/src/app/core/data/config-response-parsing.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; + +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { 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'; + +@Injectable() +export class ConfigResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { + let configDefinition; + if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { + const type = Object.keys(data.payload._embedded)[0]; + const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(type)); + configDefinition = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + + } else { + const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(data.payload.type)); + configDefinition = serializer.deserialize(data.payload); + } + return new ConfigSuccessResponse(configDefinition, data.statusCode); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from config endpoint'), + {statusText: data.statusCode} + ) + ); + } + } +} diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 8c415e71ef..127fcd834e 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -5,6 +5,7 @@ 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 { ConfigResponseParsingService } from './config-response-parsing.service'; /* tslint:disable:max-classes-per-file */ export class RestRequest { @@ -53,6 +54,16 @@ export class RootEndpointRequest extends RestRequest { } } +export class ConfigRequest extends RestRequest { + constructor(href: string) { + super(href); + } + + getResponseParser(): GenericConstructor { + return ConfigResponseParsingService; + } +} + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts index 01af2a2c2b..cb39fc718e 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts @@ -1,5 +1,6 @@ export interface DSpaceRESTV2Response { payload: { + [name: string]: string; _embedded?: any; _links?: any; page?: any; diff --git a/src/app/core/shared/config/config-object-factory.ts b/src/app/core/shared/config/config-object-factory.ts new file mode 100644 index 0000000000..4f56a84812 --- /dev/null +++ b/src/app/core/shared/config/config-object-factory.ts @@ -0,0 +1,30 @@ + +import { GenericConstructor } from '../../shared/generic-constructor'; + +import { SubmissionSectionModel } from './config-submission-section.model'; +import { SubmissionFormsModel } from './config-submission-forms.model'; +import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; +import { ConfigType } from './config-type'; +import { ConfigObject } from './config.model'; + +export class ConfigObjectFactory { + public static getConstructor(type): GenericConstructor { + switch (type) { + case ConfigType.SubmissionDefinition: + case ConfigType.SubmissionDefinitions: { + return SubmissionDefinitionsModel + } + case ConfigType.SubmissionForm: + case ConfigType.SubmissionForms: { + return SubmissionFormsModel + } + case ConfigType.SubmissionSection: + case ConfigType.SubmissionSections: { + return SubmissionSectionModel + } + default: { + return undefined; + } + } + } +} diff --git a/src/app/core/shared/config/config-submission-definitions.model.ts b/src/app/core/shared/config/config-submission-definitions.model.ts new file mode 100644 index 0000000000..271ab7281b --- /dev/null +++ b/src/app/core/shared/config/config-submission-definitions.model.ts @@ -0,0 +1,15 @@ +import { autoserialize, 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 { + + @autoserialize + isDefault: boolean; + + @autoserialize + sections: RemoteData; + +} diff --git a/src/app/core/shared/config/config-submission-forms.model.ts b/src/app/core/shared/config/config-submission-forms.model.ts new file mode 100644 index 0000000000..0b094091a7 --- /dev/null +++ b/src/app/core/shared/config/config-submission-forms.model.ts @@ -0,0 +1,10 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { ConfigObject } from './config.model'; + +@inheritSerialization(ConfigObject) +export class SubmissionFormsModel extends ConfigObject { + + @autoserialize + fields: any[]; + +} diff --git a/src/app/core/shared/config/config-submission-section.model.ts b/src/app/core/shared/config/config-submission-section.model.ts new file mode 100644 index 0000000000..17bd6e3beb --- /dev/null +++ b/src/app/core/shared/config/config-submission-section.model.ts @@ -0,0 +1,16 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { ConfigObject } from './config.model'; + +@inheritSerialization(ConfigObject) +export class SubmissionSectionModel extends ConfigObject { + + @autoserialize + header: string; + + @autoserialize + mandatory: boolean; + + @autoserialize + sectionType: string; + +} diff --git a/src/app/core/shared/config/config-type.ts b/src/app/core/shared/config/config-type.ts new file mode 100644 index 0000000000..d4d88b8a60 --- /dev/null +++ b/src/app/core/shared/config/config-type.ts @@ -0,0 +1,12 @@ +/** + * TODO replace with actual string enum after upgrade to TypeScript 2.4: + * https://github.com/Microsoft/TypeScript/pull/15486 + */ +export enum ConfigType { + SubmissionDefinitions = 'submissiondefinitions', + SubmissionDefinition = 'submissiondefinition', + SubmissionForm = 'submissionform', + SubmissionForms = 'submissionforms', + SubmissionSections = 'submissionsections', + SubmissionSection = 'submissionsection' +} diff --git a/src/app/core/shared/config/config.model.ts b/src/app/core/shared/config/config.model.ts new file mode 100644 index 0000000000..e41e2742a2 --- /dev/null +++ b/src/app/core/shared/config/config.model.ts @@ -0,0 +1,15 @@ +import { autoserialize, autoserializeAs } from 'cerialize'; + +export abstract class ConfigObject { + + @autoserialize + public name: string; + + @autoserialize + public type: string; + + @autoserialize + public _links: { + [name: string]: string + } +} From 380faf8468ff52b88009313bc3a38cdec06dfb02 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 7 Nov 2017 14:40:57 +0100 Subject: [PATCH 2/8] Refactor config-response-parsing.service --- .../core/data/config-response-parsing.service.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 1eb4da2131..243c639149 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -14,15 +14,17 @@ export class ConfigResponseParsingService implements ResponseParsingService { 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]])) { - const type = Object.keys(data.payload._embedded)[0]; - const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(type)); - configDefinition = serializer.deserializeArray(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 { - const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(data.payload.type)); - configDefinition = serializer.deserialize(data.payload); + type = data.payload.type; + payload = [data.payload]; } + const serializer = new DSpaceRESTv2Serializer(ConfigObjectFactory.getConstructor(type)); + configDefinition = serializer.deserializeArray(payload); return new ConfigSuccessResponse(configDefinition, data.statusCode); } else { return new ErrorResponse( From 067a95fc88bf5afe1b90cb3c0dd9faf2c964f8c7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 8 Nov 2017 10:02:15 +0100 Subject: [PATCH 3/8] 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; } From 362525c6ea4469ff3b4bafae109c11297945b1d3 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 10 Nov 2017 12:04:04 +0100 Subject: [PATCH 4/8] Remove setTimeout --- src/app/core/config/config.service.ts | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index 8cc590e1ae..a1a674d10a 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -12,7 +12,6 @@ import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigData } from './config-data'; -@Injectable() export abstract class ConfigService extends HALEndpointService { protected request: ConfigRequest; protected abstract responseCache: ResponseCacheService; @@ -77,11 +76,7 @@ export abstract class ConfigService extends HALEndpointService { .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(endpointURL)) - .do((request: RestRequest) => { - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }) + .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); } @@ -99,11 +94,7 @@ export abstract class ConfigService extends HALEndpointService { .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(endpointURL)) - .do((request: RestRequest) => { - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }) + .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); } @@ -114,11 +105,7 @@ export abstract class ConfigService extends HALEndpointService { .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(endpointURL)) - .do((request: RestRequest) => { - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }) + .do((request: RestRequest) => this.requestService.configure(request)) .flatMap((request: RestRequest) => this.getConfig(request)) .distinctUntilChanged(); } From a37b2da6a660943859f4f1cc5cfe752a7b36b8e8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Sun, 12 Nov 2017 19:38:27 +0100 Subject: [PATCH 5/8] Add unit tests --- src/app/core/config/config.service.spec.ts | 103 +++++++++ src/app/core/config/config.service.ts | 12 +- src/app/core/data/comcol-data.service.spec.ts | 12 +- .../config-response-parsing.service.spec.ts | 212 ++++++++++++++++++ .../data/config-response-parsing.service.ts | 2 +- src/app/core/data/request.service.ts | 1 - 6 files changed, 328 insertions(+), 14 deletions(-) create mode 100644 src/app/core/config/config.service.spec.ts create mode 100644 src/app/core/data/config-response-parsing.service.spec.ts diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts new file mode 100644 index 0000000000..c0d02be82a --- /dev/null +++ b/src/app/core/config/config.service.spec.ts @@ -0,0 +1,103 @@ +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/Rx'; +import { GlobalConfig } from '../../../config'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { ConfigService } from './config.service'; +import { RequestService } from '../data/request.service'; +import { ConfigRequest, FindAllOptions } from '../data/request.models'; + +const LINK_NAME = 'test'; +const BROWSE = 'search/findByCollection'; + +class TestService extends ConfigService { + protected linkName = LINK_NAME; + protected browseEndpoint = BROWSE; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected EnvConfig: GlobalConfig + ) { + super(); + } +} + +describe('ConfigService', () => { + let scheduler: TestScheduler; + let service: TestService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + + const envConfig = {} as GlobalConfig; + const findOptions: FindAllOptions = new FindAllOptions(); + + const scopeName = 'traditional'; + const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; + const configEndpoint = 'https://rest.api/config'; + const serviceEndpoint = `${configEndpoint}/${LINK_NAME}`; + const scopedEndpoint = `${serviceEndpoint}/${scopeName}`; + const searchEndpoint = `${serviceEndpoint}/${BROWSE}?uuid=${scopeID}`; + + function initMockRequestService(): RequestService { + return jasmine.createSpyObj('requestService', ['configure']); + } + + function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { + return jasmine.createSpyObj('responseCache', { + get: cold('c-', { + c: { response: { isSuccessful } } + }) + }); + } + + function initTestService(): TestService { + return new TestService( + responseCache, + requestService, + envConfig + ); + } + + beforeEach(() => { + responseCache = initMockResponseCacheService(true); + requestService = initMockRequestService(); + service = initTestService(); + scheduler = getTestScheduler(); + spyOn(service, 'getEndpoint').and + .returnValue(hot('--a-', { a: serviceEndpoint })); + }); + + describe('getConfigByHref', () => { + + it('should configure a new ConfigRequest', () => { + const expected = new ConfigRequest(scopedEndpoint); + scheduler.schedule(() => service.getConfigByHref(scopedEndpoint).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('getConfigByName', () => { + + it('should configure a new ConfigRequest', () => { + const expected = new ConfigRequest(scopedEndpoint); + scheduler.schedule(() => service.getConfigByName(scopeName).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); + + describe('getConfigBySearch', () => { + + it('should configure a new ConfigRequest', () => { + findOptions.scopeID = scopeID; + const expected = new ConfigRequest(searchEndpoint); + scheduler.schedule(() => service.getConfigBySearch(findOptions).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + }); +}); diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index a1a674d10a..55c4055ed7 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -28,13 +28,13 @@ export abstract class ConfigService extends HALEndpointService { 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) => new ConfigData(response.pageInfo, response.configDefinition)) - .distinctUntilChanged()); + .filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition)) + .map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) + .distinctUntilChanged()); } - protected getConfigByIDHref(endpoint, resourceID): string { - return `${endpoint}/${resourceID}`; + protected getConfigByNameHref(endpoint, resourceName): string { + return `${endpoint}/${resourceName}`; } protected getConfigSearchHref(endpoint, options: FindAllOptions = {}): string { @@ -90,7 +90,7 @@ export abstract class ConfigService extends HALEndpointService { public getConfigByName(name: string): Observable { return this.getEndpoint() - .map((endpoint: string) => this.getConfigByIDHref(endpoint, name)) + .map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new ConfigRequest(endpointURL)) diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index be7949826f..0d78a9fa8d 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -66,7 +66,7 @@ describe('ComColDataService', () => { return jasmine.createSpyObj('requestService', ['configure']); } - function initMockResponceCacheService(isSuccessful: boolean): ResponseCacheService { + function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService { return jasmine.createSpyObj('responseCache', { get: cold('c-', { c: { response: { isSuccessful } } @@ -107,7 +107,7 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponceCacheService(true); + responseCache = initMockResponseCacheService(true); service = initTestService(); const expected = new FindByIDRequest(communityEndpoint, scopeID); @@ -123,7 +123,7 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponceCacheService(true); + responseCache = initMockResponseCacheService(true); service = initTestService(); }); @@ -146,7 +146,7 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponceCacheService(false); + responseCache = initMockResponseCacheService(false); service = initTestService(); }); @@ -163,12 +163,12 @@ describe('ComColDataService', () => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); - responseCache = initMockResponceCacheService(true); + responseCache = initMockResponseCacheService(true); service = initTestService(); }); it('should return this.getEndpoint()', () => { - spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint })) + spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint })); const result = service.getScopedEndpoint(undefined); const expected = cold('--f-', { f: serviceEndpoint }); diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts new file mode 100644 index 0000000000..62291548a7 --- /dev/null +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -0,0 +1,212 @@ +import { ConfigSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; +import { ConfigResponseParsingService } from './config-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ConfigRequest } from './request.models'; + +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model'; +import { SubmissionSectionModel } from '../shared/config/config-submission-section.model'; + +describe('ConfigResponseParsingService', () => { + let service: ConfigResponseParsingService; + + const EnvConfig = {} as GlobalConfig; + const store = {} as Store; + const objectCacheService = new ObjectCacheService(store); + + beforeEach(() => { + service = new ConfigResponseParsingService(EnvConfig, objectCacheService); + }); + + describe('parse', () => { + const validRequest = new ConfigRequest('https://rest.api/config/submissiondefinitions/traditional'); + + const validResponse = { + payload: { + id:'traditional', + name:'traditional', + type:'submissiondefinition', + isDefault:true, + _links:{ + sections:{ + href:'https://rest.api/config/submissiondefinitions/traditional/sections' + },self:{ + href:'https://rest.api/config/submissiondefinitions/traditional' + } + }, + _embedded:{ + sections:{ + page:{ + number:0, + size:4, + totalPages:1,totalElements:4 + }, + _embedded:[ + { + id:'traditionalpageone',header:'submit.progressbar.describe.stepone', + mandatory:true, + sectionType:'submission-form', + type:'submissionsection', + _links:{ + self:{ + href:'https://rest.api/config/submissionsections/traditionalpageone' + }, + config:{ + href:'https://rest.api/config/submissionforms/traditionalpageone' + } + } + }, { + id:'traditionalpagetwo', + header:'submit.progressbar.describe.steptwo', + mandatory:true, + sectionType:'submission-form', + type:'submissionsection', + _links:{ + self:{ + href:'https://rest.api/config/submissionsections/traditionalpagetwo' + }, + config:{ + href:'https://rest.api/config/submissionforms/traditionalpagetwo' + } + } + }, { + id:'upload', + header:'submit.progressbar.upload', + mandatory:false, + sectionType:'upload', + type:'submissionsection', + _links:{ + self:{ + href:'https://rest.api/config/submissionsections/upload' + }, + config: { + href:'https://rest.api/config/submissionuploads/upload' + } + } + }, { + id:'license', + header:'submit.progressbar.license', + mandatory:true, + sectionType:'license', + visibility:{ + main:null, + other:'READONLY' + }, + type:'submissionsection', + _links:{ + self:{ + href:'https://rest.api/config/submissionsections/license' + } + } + } + ], + _links:{ + self:'https://rest.api/config/submissiondefinitions/traditional/sections' + } + } + } + }, + statusCode:'200' + }; + + const invalidResponse1 = { + payload: {}, + statusCode:'200' + }; + + const invalidResponse2 = { + payload: { + id:'traditional', + name:'traditional', + type:'submissiondefinition', + isDefault:true, + _links:{}, + _embedded:{ + sections:{ + page:{ + number:0, + size:4, + totalPages:1,totalElements:4 + }, + _embedded:[{},{}], + _links:{ + self:'https://rest.api/config/submissiondefinitions/traditional/sections' + } + } + } + }, + statusCode:'200' + }; + + const invalidResponse3 = { + payload: { + _links: { self: { href: 'https://rest.api/config/submissiondefinitions/traditional' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '500' + }; + + const definitions = [ + Object.assign(new SubmissionDefinitionsModel(), { + isDefault: true, + name: 'traditional', + type: 'submissiondefinition', + _links: {}, + sections: [ + Object.assign(new SubmissionSectionModel(), { + header: 'submit.progressbar.describe.stepone', + mandatory: true, + sectionType: 'submission-form', + type: 'submissionsection', + _links: {} + }), + Object.assign(new SubmissionSectionModel(), { + header: 'submit.progressbar.describe.steptwo', + mandatory: true, + sectionType: 'submission-form', + type: 'submissionsection', + _links: {} + }), + Object.assign(new SubmissionSectionModel(), { + header: 'submit.progressbar.upload', + mandatory: false, + sectionType: 'upload', + type: 'submissionsection', + _links: {} + }), + Object.assign(new SubmissionSectionModel(), { + header: 'submit.progressbar.license', + mandatory: true, + sectionType: 'license', + type: 'submissionsection', + _links: {} + }) + ] + }) + ]; + + it('should return a ConfigSuccessResponse if data contains a valid config endpoint response', () => { + const response = service.parse(validRequest, validResponse); + expect(response.constructor).toBe(ConfigSuccessResponse); + }); + + it('should return an ErrorResponse if data contains an invalid config endpoint response', () => { + const response1 = service.parse(validRequest, invalidResponse1); + const response2 = service.parse(validRequest, invalidResponse2); + expect(response1.constructor).toBe(ErrorResponse); + expect(response2.constructor).toBe(ErrorResponse); + }); + + it('should return an ErrorResponse if data contains a statuscode other than 200', () => { + const response = service.parse(validRequest, invalidResponse3); + expect(response.constructor).toBe(ErrorResponse); + }); + + it('should return a ConfigSuccessResponse with the ConfigDefinitions in data', () => { + const response = service.parse(validRequest, validResponse); + expect((response as any).configDefinition).toEqual(definitions); + }); + + }); +}); diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 90e6dc055e..69be4bbc02 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -27,7 +27,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') { const configDefinition = this.process(data.payload, request.href); return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page)); } else { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 3036c7be21..0eee771a52 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -26,7 +26,6 @@ export function requestStateSelector(): MemoizedSelector Date: Fri, 24 Nov 2017 15:47:11 +0100 Subject: [PATCH 6/8] Update config-submission-section.model --- .../core/shared/config/config-submission-section.model.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/core/shared/config/config-submission-section.model.ts b/src/app/core/shared/config/config-submission-section.model.ts index 17bd6e3beb..0eb9daaeab 100644 --- a/src/app/core/shared/config/config-submission-section.model.ts +++ b/src/app/core/shared/config/config-submission-section.model.ts @@ -13,4 +13,10 @@ export class SubmissionSectionModel extends ConfigObject { @autoserialize sectionType: string; + @autoserialize + visibility: { + main: any, + other: any + } + } From b36498727f5209296085a007c7f7cdb80c73024f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 24 Nov 2017 15:49:16 +0100 Subject: [PATCH 7/8] Remove empty line --- src/app/core/core.module.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index e001d83bd7..f4c7e2bbcc 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -32,7 +32,6 @@ import { ServerResponseService } from '../shared/server-response.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; import { BrowseService } from './browse/browse.service'; import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; - import { ConfigResponseParsingService } from './data/config-response-parsing.service'; import { RouteService } from '../shared/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; @@ -71,7 +70,6 @@ const PROVIDERS = [ ServerResponseService, BrowseResponseParsingService, BrowseService, - ConfigResponseParsingService, RouteService, SubmissionDefinitionsConfigService, From ad6d42cd42a84c7d3aba0eeac02ed56fcf9dba10 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 27 Nov 2017 12:39:36 +0100 Subject: [PATCH 8/8] Fix config-response-parsing.service.spec --- .../config-response-parsing.service.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/app/core/data/config-response-parsing.service.spec.ts b/src/app/core/data/config-response-parsing.service.spec.ts index 62291548a7..46e6d61f8f 100644 --- a/src/app/core/data/config-response-parsing.service.spec.ts +++ b/src/app/core/data/config-response-parsing.service.spec.ts @@ -48,6 +48,10 @@ describe('ConfigResponseParsingService', () => { id:'traditionalpageone',header:'submit.progressbar.describe.stepone', mandatory:true, sectionType:'submission-form', + visibility:{ + main:null, + other:'READONLY' + }, type:'submissionsection', _links:{ self:{ @@ -62,6 +66,10 @@ describe('ConfigResponseParsingService', () => { header:'submit.progressbar.describe.steptwo', mandatory:true, sectionType:'submission-form', + visibility:{ + main:null, + other:'READONLY' + }, type:'submissionsection', _links:{ self:{ @@ -76,6 +84,10 @@ describe('ConfigResponseParsingService', () => { header:'submit.progressbar.upload', mandatory:false, sectionType:'upload', + visibility:{ + main:null, + other:'READONLY' + }, type:'submissionsection', _links:{ self:{ @@ -158,6 +170,10 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.describe.stepone', mandatory: true, sectionType: 'submission-form', + visibility:{ + main:null, + other:'READONLY' + }, type: 'submissionsection', _links: {} }), @@ -165,6 +181,10 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.describe.steptwo', mandatory: true, sectionType: 'submission-form', + visibility:{ + main:null, + other:'READONLY' + }, type: 'submissionsection', _links: {} }), @@ -172,6 +192,10 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.upload', mandatory: false, sectionType: 'upload', + visibility:{ + main:null, + other:'READONLY' + }, type: 'submissionsection', _links: {} }), @@ -179,6 +203,10 @@ describe('ConfigResponseParsingService', () => { header: 'submit.progressbar.license', mandatory: true, sectionType: 'license', + visibility:{ + main:null, + other:'READONLY' + }, type: 'submissionsection', _links: {} })