Skip to content
Snippets Groups Projects
base-response-parsing.service.ts 6.30 KiB
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 { GenericConstructor } from '../shared/generic-constructor';
import { PaginatedList } from './paginated-list';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResourceType } from '../shared/resource-type';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';

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 */

export abstract class BaseResponseParsingService {
  protected abstract EnvConfig: GlobalConfig;
  protected abstract objectCache: ObjectCacheService;
  protected abstract objectFactory: any;
  protected abstract toCache: boolean;

  protected process<ObjectDomain, ObjectType>(data: any, requestHref: string): any {

    if (isNotEmpty(data)) {
      if (hasNoValue(data) || (typeof data !== 'object')) {
        return data;
      } else if (isPaginatedResponse(data)) {
        return this.processPaginatedList(data, requestHref);
      } else if (Array.isArray(data)) {
        return this.processArray(data, requestHref);
      } else if (isObjectLevel(data)) {
        data = this.fixBadEPersonRestResponse(data);
        const object = this.deserialize(data);
        if (isNotEmpty(data._embedded)) {
          Object
            .keys(data._embedded)
            .filter((property) => data._embedded.hasOwnProperty(property))
            .forEach((property) => {
              const parsedObj = this.process<ObjectDomain, ObjectType>(data._embedded[property], requestHref);
              if (isNotEmpty(parsedObj)) {
                if (isPaginatedResponse(data._embedded[property])) {
                  object[property] = parsedObj;
                  object[property].page = parsedObj.page.map((obj) => obj.self);
                } else if (isObjectLevel(data._embedded[property])) {
                  object[property] = parsedObj.self;
                } else if (Array.isArray(parsedObj)) {
                  object[property] = parsedObj.map((obj) => obj.self)
                }
              }
            });
        }

        this.cache(object, requestHref);
        return object;
      }
      const result = {};
      Object.keys(data)
        .filter((property) => data.hasOwnProperty(property))
        .filter((property) => hasValue(data[property]))
        .forEach((property) => {
          const obj = this.process(data[property], requestHref);
          result[property] = obj;
        });
      return result;

    }
  }

  protected processPaginatedList<ObjectDomain, ObjectType>(data: any, requestHref: string): PaginatedList<ObjectDomain> {
    const pageInfo: PageInfo = this.processPageInfo(data);
    let list = data._embedded;

    // Workaround for inconsistency in rest response. Issue: https://github.com/DSpace/dspace-angular/issues/238
    if (!Array.isArray(list)) {
      list = this.flattenSingleKeyObject(list);
    }
    const page: ObjectDomain[] = this.processArray(list, requestHref);
    return new PaginatedList<ObjectDomain>(pageInfo, page);
  }

  protected processArray<ObjectDomain, ObjectType>(data: any, requestHref: string): ObjectDomain[] {
    let array: ObjectDomain[] = [];
    data.forEach((datum) => {
        array = [...array, this.process(datum, requestHref)];
      }
    );
    return array;
  }

  protected deserialize<ObjectDomain, ObjectType>(obj): any {
    const type: ObjectType = obj.type;
    if (hasValue(type)) {
      const normObjConstructor = this.objectFactory.getConstructor(type) as GenericConstructor<ObjectDomain>;

      if (hasValue(normObjConstructor)) {
        const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
        const res = serializer.deserialize(obj);
        return res;
      } else {
        // TODO: move check to Validator?
        // throw new Error(`The server returned an object with an unknown a known type: ${type}`);
        return null;
      }

    } else {
      // TODO: move check to Validator
      // throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
      return null;
    }
  }

  protected cache<ObjectDomain, ObjectType>(obj, requestHref) {
    if (this.toCache) {
      this.addToObjectCache(obj, requestHref);
    }
  }

  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);
  }

  processPageInfo(payload: any): PageInfo {
    if (isNotEmpty(payload.page)) {
      const pageObj = Object.assign({}, payload.page, { _links: payload._links });
      const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
      if (pageInfoObject.currentPage >= 0) {
        Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 });
      }
      return pageInfoObject
    } 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]];
  }

  // TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
  // See https://github.com/DSpace/dspace-angular/issues/292
  private fixBadEPersonRestResponse(obj: any): any {
    if (obj.type === ResourceType.EPerson) {
      const groups = obj.groups;
      const normGroups = [];
      if (isNotEmpty(groups)) {
        groups.forEach((group) => {
            const parts = ['eperson', 'groups', group.uuid];
            const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString();
            normGroups.push(href);
          }
        )
      }
      return Object.assign({}, obj, { groups: normGroups });
    }
    return obj;
  }
}