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 + } +}