diff --git a/src/app/core/cache/models/collection-builder.ts b/src/app/core/cache/models/collection-builder.ts new file mode 100644 index 0000000000..8dbb960c19 --- /dev/null +++ b/src/app/core/cache/models/collection-builder.ts @@ -0,0 +1,93 @@ +import { Collection } from "../../shared/collection.model"; +import { hasValue } from "../../../shared/empty.util"; +import { Item } from "../../shared/item.model"; +import { RequestConfigureAction, RequestExecuteAction } from "../../data/request.actions"; +import { ObjectCacheService } from "../object-cache.service"; +import { ResponseCacheService } from "../response-cache.service"; +import { RequestService } from "../../data/request.service"; +import { Store } from "@ngrx/store"; +import { CoreState } from "../../core.reducers"; +import { NormalizedCollection } from "./normalized-collection.model"; +import { Request } from "../../data/request.models"; +import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder"; +import { ItemRDBuilder } from "./item-builder"; + +export class CollectionBuilder { + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected href: string, + protected nc: NormalizedCollection + ) { + } + + build(): Collection { + let links: any = {}; + + if (hasValue(this.nc.items)) { + this.nc.items.forEach((href: string) => { + const isCached = this.objectCache.hasBySelfLink(href); + const isPending = this.requestService.isPending(href); + + console.log('href', href, 'isCached', isCached, "isPending", isPending); + + if (!(isCached || isPending)) { + const request = new Request(href, Item); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + }); + + links.items = this.nc.items.map((href: string) => { + return new ItemRDBuilder( + this.objectCache, + this.responseCache, + this.requestService, + this.store, + href + ).build(); + }); + } + + return Object.assign(new Collection(), this.nc, links); + } +} + +export class CollectionRDBuilder extends SingleRemoteDataBuilder { + + constructor( + objectCache: ObjectCacheService, + responseCache: ResponseCacheService, + requestService: RequestService, + store: Store, + href: string + ) { + super(objectCache, responseCache, requestService, store, href, NormalizedCollection); + } + + protected normalizedToDomain(normalized: NormalizedCollection): Collection { + return new CollectionBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build(); + } + +} + +export class CollectionListRDBuilder extends ListRemoteDataBuilder { + + constructor( + objectCache: ObjectCacheService, + responseCache: ResponseCacheService, + requestService: RequestService, + store: Store, + href: string + ) { + super(objectCache, responseCache, requestService, store, href, NormalizedCollection); + } + + protected normalizedToDomain(normalized: NormalizedCollection): Collection { + return new CollectionBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build(); + } + +} diff --git a/src/app/core/cache/models/item-builder.ts b/src/app/core/cache/models/item-builder.ts new file mode 100644 index 0000000000..b812ef0430 --- /dev/null +++ b/src/app/core/cache/models/item-builder.ts @@ -0,0 +1,61 @@ +import { Item } from "../../shared/item.model"; +import { ObjectCacheService } from "../object-cache.service"; +import { ResponseCacheService } from "../response-cache.service"; +import { RequestService } from "../../data/request.service"; +import { Store } from "@ngrx/store"; +import { CoreState } from "../../core.reducers"; +import { NormalizedItem } from "./normalized-item.model"; +import { ListRemoteDataBuilder, SingleRemoteDataBuilder } from "./remote-data-builder"; + +export class ItemBuilder { + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected href: string, + protected nc: NormalizedItem + ) { + } + + build(): Item { + //TODO + return Object.assign(new Item(), this.nc); + } +} + +export class ItemRDBuilder extends SingleRemoteDataBuilder { + + constructor( + objectCache: ObjectCacheService, + responseCache: ResponseCacheService, + requestService: RequestService, + store: Store, + href: string + ) { + super(objectCache, responseCache, requestService, store, href, NormalizedItem); + } + + protected normalizedToDomain(normalized: NormalizedItem): Item { + return new ItemBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build(); + } + +} + +export class ItemListRDBuilder extends ListRemoteDataBuilder { + constructor( + objectCache: ObjectCacheService, + responseCache: ResponseCacheService, + requestService: RequestService, + store: Store, + href: string + ) { + super(objectCache, responseCache, requestService, store, href, NormalizedItem); + } + + protected normalizedToDomain(normalized: NormalizedItem): Item { + return new ItemBuilder(this.objectCache, this.responseCache, this.requestService, this.store, this.href, normalized).build(); + } + +} diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts new file mode 100644 index 0000000000..b6b1302f79 --- /dev/null +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -0,0 +1,36 @@ +import { inheritSerialization } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; + +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedBitstream extends NormalizedDSpaceObject { + + /** + * The size of this bitstream in bytes(?) + */ + size: number; + + /** + * The relative path to this Bitstream's file + */ + url: string; + + /** + * The mime type of this Bitstream + */ + mimetype: string; + + /** + * The description of this Bitstream + */ + description: string; + + /** + * An array of Bundles that are direct parents of this Bitstream + */ + parents: Array; + + /** + * The Bundle that owns this Bitstream + */ + owner: string; +} diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts new file mode 100644 index 0000000000..73c31990d0 --- /dev/null +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -0,0 +1,21 @@ +import { inheritSerialization } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; + +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedBundle extends NormalizedDSpaceObject { + /** + * The primary bitstream of this Bundle + */ + primaryBitstream: string; + + /** + * An array of Items that are direct parents of this Bundle + */ + parents: Array; + + /** + * The Item that owns this Bundle + */ + owner: string; + +} diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts new file mode 100644 index 0000000000..74f29150b2 --- /dev/null +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -0,0 +1,31 @@ +import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; + +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedCollection extends NormalizedDSpaceObject { + + /** + * A string representing the unique handle of this Collection + */ + @autoserialize + handle: string; + + /** + * The Bitstream that represents the logo of this Collection + */ + logo: string; + + /** + * An array of Collections that are direct parents of this Collection + */ + parents: Array; + + /** + * The Collection that owns this Collection + */ + owner: string; + + @autoserialize + items: Array; + +} diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts new file mode 100644 index 0000000000..c3b782e6df --- /dev/null +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -0,0 +1,51 @@ +import { autoserialize, autoserializeAs } from "cerialize"; +import { CacheableObject } from "../object-cache.reducer"; +import { Metadatum } from "../../shared/metadatum.model"; + +/** + * An abstract model class for a DSpaceObject. + */ +export abstract class NormalizedDSpaceObject implements CacheableObject { + + @autoserialize + self: string; + + /** + * The human-readable identifier of this DSpaceObject + */ + @autoserialize + id: string; + + /** + * The universally unique identifier of this DSpaceObject + */ + @autoserialize + uuid: string; + + /** + * A string representing the kind of DSpaceObject, e.g. community, item, … + */ + type: string; + + /** + * The name for this DSpaceObject + */ + @autoserialize + name: string; + + /** + * An array containing all metadata of this DSpaceObject + */ + @autoserializeAs(Metadatum) + metadata: Array; + + /** + * An array of DSpaceObjects that are direct parents of this DSpaceObject + */ + parents: Array; + + /** + * The DSpaceObject that owns this DSpaceObject + */ + owner: string; +} diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts new file mode 100644 index 0000000000..69fb83afcc --- /dev/null +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -0,0 +1,38 @@ +import { inheritSerialization, autoserialize } from "cerialize"; +import { NormalizedDSpaceObject } from "./normalized-dspace-object.model"; + +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedItem extends NormalizedDSpaceObject { + + /** + * A string representing the unique handle of this Item + */ + @autoserialize + handle: string; + + /** + * The Date of the last modification of this Item + */ + lastModified: Date; + + /** + * A boolean representing if this Item is currently archived or not + */ + isArchived: boolean; + + /** + * A boolean representing if this Item is currently withdrawn or not + */ + isWithdrawn: boolean; + + /** + * An array of Collections that are direct parents of this Item + */ + parents: Array; + + /** + * The Collection that owns this Item + */ + owner: string; + +} diff --git a/src/app/core/shared/pagination-options.model.ts b/src/app/core/cache/models/pagination-options.model.ts similarity index 100% rename from src/app/core/shared/pagination-options.model.ts rename to src/app/core/cache/models/pagination-options.model.ts diff --git a/src/app/core/cache/models/remote-data-builder.ts b/src/app/core/cache/models/remote-data-builder.ts new file mode 100644 index 0000000000..28ea91ac54 --- /dev/null +++ b/src/app/core/cache/models/remote-data-builder.ts @@ -0,0 +1,131 @@ +import { RemoteData } from "../../data/remote-data"; +import { Observable } from "rxjs/Observable"; +import { RequestEntry } from "../../data/request.reducer"; +import { ResponseCacheEntry } from "../response-cache.reducer"; +import { ErrorResponse, SuccessResponse } from "../response-cache.models"; +import { Store } from "@ngrx/store"; +import { CoreState } from "../../core.reducers"; +import { ResponseCacheService } from "../response-cache.service"; +import { ObjectCacheService } from "../object-cache.service"; +import { RequestService } from "../../data/request.service"; +import { CacheableObject } from "../object-cache.reducer"; +import { GenericConstructor } from "../../shared/generic-constructor"; +import { hasValue, isNotEmpty } from "../../../shared/empty.util"; + +export interface RemoteDataBuilder { + build(): RemoteData +} + +export abstract class SingleRemoteDataBuilder implements RemoteDataBuilder { + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected href: string, + protected normalizedType: GenericConstructor + ) { + } + + protected abstract normalizedToDomain(normalized: TNormalized): TDomain; + + build(): RemoteData { + const requestObs = this.store.select('core', 'data', 'request', this.href); + const responseCacheObs = this.responseCache.get(this.href); + + const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); + + const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); + + const isSuccessFul = responseCacheObs + .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); + + const errorMessage = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => ( entry.response).errorMessage) + .distinctUntilChanged(); + + const payload = + Observable.race( + this.objectCache.getBySelfLink(this.href, this.normalizedType), + responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs) + .flatMap((resourceUUIDs: Array) => { + if (isNotEmpty(resourceUUIDs)) { + return this.objectCache.get(resourceUUIDs[0], this.normalizedType); + } + else { + return Observable.of(undefined); + } + }) + .distinctUntilChanged() + ).map((normalized: TNormalized) => this.normalizedToDomain(normalized)); + + return new RemoteData( + this.href, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + payload + ); + } + +} + +export abstract class ListRemoteDataBuilder implements RemoteDataBuilder { + + constructor( + protected objectCache: ObjectCacheService, + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected href: string, + protected normalizedType: GenericConstructor + ) { + } + + protected abstract normalizedToDomain(normalized: TNormalized): TDomain; + + build(): RemoteData { + const requestObs = this.store.select('core', 'data', 'request', this.href); + const responseCacheObs = this.responseCache.get(this.href); + + const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged(); + + const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged(); + + const isSuccessFul = responseCacheObs + .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged(); + + const errorMessage = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => ( entry.response).errorMessage) + .distinctUntilChanged(); + + const payload = responseCacheObs + .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs) + .flatMap((resourceUUIDs: Array) => { + return this.objectCache.getList(resourceUUIDs, this.normalizedType) + .map((normList: TNormalized[]) => { + return normList.map((normalized: TNormalized) => { + return this.normalizedToDomain(normalized); + }); + }); + }) + .distinctUntilChanged(); + + return new RemoteData( + this.href, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + payload + ); + } + +} diff --git a/src/app/core/shared/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts similarity index 100% rename from src/app/core/shared/self-link.model.ts rename to src/app/core/cache/models/self-link.model.ts diff --git a/src/app/core/shared/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts similarity index 100% rename from src/app/core/shared/sort-options.model.ts rename to src/app/core/cache/models/sort-options.model.ts diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9093093f50..ec0bea4a97 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -60,6 +60,11 @@ export class ObjectCacheService { .map((entry: ObjectCacheEntry) => Object.assign(new type(), entry.data)); } + getBySelfLink(href: string, type: GenericConstructor): Observable { + return this.store.select('core', 'index', 'href', href) + .flatMap((uuid: string) => this.get(uuid, type)) + } + /** * Get an observable for an array of objects of the same type * with the specified UUIDs @@ -104,6 +109,25 @@ export class ObjectCacheService { return result; } + /** + * Check whether the object with the specified self link is cached + * + * @param href + * The self link of the object to check + * @return boolean + * true if the object with the specified self link is cached, + * false otherwise + */ + hasBySelfLink(href: string): boolean { + let result: boolean = false; + + this.store.select('core', 'index', 'href', href) + .take(1) + .subscribe((uuid: string) => result = this.has(uuid)); + + return result; + } + /** * Check whether an ObjectCacheEntry should still be cached * diff --git a/src/app/core/cache/response-cache.actions.ts b/src/app/core/cache/response-cache.actions.ts index 4a55b3fe0c..45f78f10b7 100644 --- a/src/app/core/cache/response-cache.actions.ts +++ b/src/app/core/cache/response-cache.actions.ts @@ -6,9 +6,9 @@ import { Response } from "./response-cache.models"; * The list of ResponseCacheAction type definitions */ export const ResponseCacheActionTypes = { - ADD: type('dspace/core/cache/request/ADD'), - REMOVE: type('dspace/core/cache/request/REMOVE'), - RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS') + ADD: type('dspace/core/cache/response/ADD'), + REMOVE: type('dspace/core/cache/response/REMOVE'), + RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS') }; export class ResponseCacheAddAction implements Action { diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index 14f206638a..17d9ed1091 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -56,7 +56,9 @@ export class ResponseCacheService { this.store.select('core', 'cache', 'response', key) .take(1) - .subscribe(entry => result = this.isValid(entry)); + .subscribe(entry => { + result = this.isValid(entry); + }); return result; } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index f1e24d15d8..827cf9deb7 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -8,6 +8,7 @@ import { ObjectCacheService } from "./cache/object-cache.service"; import { ResponseCacheService } from "./cache/response-cache.service"; import { CollectionDataService } from "./data/collection-data.service"; import { ItemDataService } from "./data/item-data.service"; +import { RequestService } from "./data/request.service"; const IMPORTS = [ CommonModule, @@ -27,7 +28,8 @@ const PROVIDERS = [ ItemDataService, DSpaceRESTv2Service, ObjectCacheService, - ResponseCacheService + ResponseCacheService, + RequestService ]; @NgModule({ diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 5107ac1bf3..4abffa909c 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -3,34 +3,43 @@ import { DataService } from "./data.service"; import { Collection } from "../shared/collection.model"; import { ObjectCacheService } from "../cache/object-cache.service"; import { ResponseCacheService } from "../cache/response-cache.service"; -import { RemoteData } from "./remote-data"; -import { ItemDataService } from "./item-data.service"; import { Store } from "@ngrx/store"; -import { RequestState } from "./request.reducer"; +import { NormalizedCollection } from "../cache/models/normalized-collection.model"; +import { CoreState } from "../core.reducers"; +import { RequestService } from "./request.service"; +import { CollectionListRDBuilder, CollectionRDBuilder } from "../cache/models/collection-builder"; @Injectable() -export class CollectionDataService extends DataService { +export class CollectionDataService extends DataService { protected endpoint = '/collections'; constructor( protected objectCache: ObjectCacheService, protected responseCache: ResponseCacheService, - protected store: Store, - protected ids: ItemDataService + protected requestService: RequestService, + protected store: Store ) { - super(Collection); + super(NormalizedCollection); + } + + protected getListDataBuilder(href: string): CollectionListRDBuilder { + return new CollectionListRDBuilder ( + this.objectCache, + this.responseCache, + this.requestService, + this.store, + href, + ); + } + + protected getSingleDataBuilder(href: string): CollectionRDBuilder { + return new CollectionRDBuilder ( + this.objectCache, + this.responseCache, + this.requestService, + this.store, + href, + ); } - // findAll(scopeID?: string): RemoteData> { - // let remoteData = super.findAll(scopeID); - // remoteData.payload = remoteData.payload.map(collections => { - // return collections.map(collection => { - // collection.items = collection.itemLinks.map(item => { - // return this.ids.findById(item.self); - // }); - // return collection - // }); - // }); - // return remoteData; - // } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 7789381eaa..bcbd5fd599 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,27 +1,30 @@ -import { Observable } from "rxjs"; import { ObjectCacheService } from "../cache/object-cache.service"; import { ResponseCacheService } from "../cache/response-cache.service"; import { CacheableObject } from "../cache/object-cache.reducer"; -import { isNotEmpty, hasValue } from "../../shared/empty.util"; +import { hasValue } from "../../shared/empty.util"; import { GenericConstructor } from "../shared/generic-constructor"; import { RemoteData } from "./remote-data"; -import { SuccessResponse, ErrorResponse } from "../cache/response-cache.models"; -import { FindAllRequest, FindByIDRequest } from "./request.models"; -import { RequestState, RequestEntry } from "./request.reducer"; +import { FindAllRequest, FindByIDRequest, Request } from "./request.models"; import { Store } from "@ngrx/store"; import { RequestConfigureAction, RequestExecuteAction } from "./request.actions"; -import { ResponseCacheEntry } from "../cache/response-cache.reducer"; +import { CoreState } from "../core.reducers"; +import { RemoteDataBuilder } from "../cache/models/remote-data-builder"; +import { RequestService } from "./request.service"; -export abstract class DataService { +export abstract class DataService { protected abstract objectCache: ObjectCacheService; protected abstract responseCache: ResponseCacheService; - protected abstract store: Store; + protected abstract requestService: RequestService; + protected abstract store: Store; protected abstract endpoint: string; - constructor(private resourceType: GenericConstructor) { + constructor(private normalizedResourceType: GenericConstructor) { } + protected abstract getListDataBuilder(href: string): RemoteDataBuilder; + protected abstract getSingleDataBuilder(href: string): RemoteDataBuilder; + protected getFindAllHref(scopeID?): string { let result = this.endpoint; if (hasValue(scopeID)) { @@ -32,28 +35,12 @@ export abstract class DataService { findAll(scopeID?: string): RemoteData> { const href = this.getFindAllHref(scopeID); - const request = new FindAllRequest(href, this.resourceType, scopeID); - this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(href)); - const requestObs = this.store.select('core', 'data', 'request', href); - const responseCacheObs = this.responseCache.get(href); - return new RemoteData( - requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(), - requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(), - responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(), - responseCacheObs - .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => ( entry.response).errorMessage) - .distinctUntilChanged(), - responseCacheObs - .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs) - .flatMap((resourceUUIDs: Array) => { - // use those IDs to fetch the actual objects from the ObjectCache - return this.objectCache.getList(resourceUUIDs, this.resourceType); - }).distinctUntilChanged() - ); + if (!this.responseCache.has(href) && !this.requestService.isPending(href)) { + const request = new FindAllRequest(href, this.normalizedResourceType, scopeID); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.getListDataBuilder(href).build(); } protected getFindByIDHref(resourceID): string { @@ -62,32 +49,21 @@ export abstract class DataService { findById(id: string): RemoteData { const href = this.getFindByIDHref(id); - const request = new FindByIDRequest(href, this.resourceType, id); - this.store.dispatch(new RequestConfigureAction(request)); - this.store.dispatch(new RequestExecuteAction(href)); - const requestObs = this.store.select('core', 'data', 'request', href); - const responseCacheObs = this.responseCache.get(href); - return new RemoteData( - requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(), - requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(), - responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(), - responseCacheObs - .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => ( entry.response).errorMessage) - .distinctUntilChanged(), - responseCacheObs - .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs) - .flatMap((resourceUUIDs: Array) => { - if (isNotEmpty(resourceUUIDs)) { - return this.objectCache.get(resourceUUIDs[0], this.resourceType); - } - else { - return Observable.of(undefined); - } - }).distinctUntilChanged() - ); + if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) { + const request = new FindByIDRequest(href, this.normalizedResourceType, id); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.getSingleDataBuilder(href).build(); + } + + findByHref(href: string): RemoteData { + if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) { + const request = new Request(href, this.normalizedResourceType); + this.store.dispatch(new RequestConfigureAction(request)); + this.store.dispatch(new RequestExecuteAction(href)); + } + return this.getSingleDataBuilder(href).build(); } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 6c64e5a8d9..777d75c53c 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -4,18 +4,41 @@ import { Item } from "../shared/item.model"; import { ObjectCacheService } from "../cache/object-cache.service"; import { ResponseCacheService } from "../cache/response-cache.service"; import { Store } from "@ngrx/store"; -import { RequestState } from "./request.reducer"; +import { CoreState } from "../core.reducers"; +import { NormalizedItem } from "../cache/models/normalized-item.model"; +import { RequestService } from "./request.service"; +import { ItemListRDBuilder, ItemRDBuilder } from "../cache/models/item-builder"; @Injectable() -export class ItemDataService extends DataService { +export class ItemDataService extends DataService { protected endpoint = '/items'; constructor( protected objectCache: ObjectCacheService, protected responseCache: ResponseCacheService, - protected store: Store + protected requestService: RequestService, + protected store: Store ) { - super(Item); + super(NormalizedItem); } + protected getListDataBuilder(href: string): ItemListRDBuilder { + return new ItemListRDBuilder( + this.objectCache, + this.responseCache, + this.requestService, + this.store, + href, + ); + } + + protected getSingleDataBuilder(href: string): ItemRDBuilder { + return new ItemRDBuilder( + this.objectCache, + this.responseCache, + this.requestService, + this.store, + href, + ); + } } diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index d1d2a7a3e2..7fa02bf25c 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -11,8 +11,8 @@ export enum RemoteDataState { * A class to represent the state of a remote resource */ export class RemoteData { - constructor( + public self: string, private requestPending: Observable, private responsePending: Observable, private isSuccessFul: Observable, diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts index f9663aaf7f..16ce1963bd 100644 --- a/src/app/core/data/request.actions.ts +++ b/src/app/core/data/request.actions.ts @@ -1,7 +1,5 @@ import { Action } from "@ngrx/store"; import { type } from "../../shared/ngrx/type"; -import { PaginationOptions } from "../shared/pagination-options.model"; -import { SortOptions } from "../shared/sort-options.model"; import { CacheableObject } from "../cache/object-cache.reducer"; import { Request } from "./request.models"; diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 037feedb95..e5d887626e 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -12,10 +12,11 @@ import { hasNoValue } from "../../shared/empty.util"; import { GlobalConfig, GLOBAL_CONFIG } from "../../../config"; import { RequestState, RequestEntry } from "./request.reducer"; import { - RequestActionTypes, RequestConfigureAction, RequestExecuteAction, + RequestActionTypes, RequestExecuteAction, RequestCompleteAction } from "./request.actions"; import { ResponseCacheService } from "../cache/response-cache.service"; +import { RequestService } from "./request.service"; @Injectable() export class RequestEffects { @@ -26,13 +27,14 @@ export class RequestEffects { private restApi: DSpaceRESTv2Service, private objectCache: ObjectCacheService, private responseCache: ResponseCacheService, + protected requestService: RequestService, private store: Store ) { } @Effect() execute = this.actions$ .ofType(RequestActionTypes.EXECUTE) .flatMap((action: RequestExecuteAction) => { - return this.store.select('core', 'data', 'request', action.payload) + return this.requestService.get(action.payload) .take(1); }) .flatMap((entry: RequestEntry) => { diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 6971323cc1..9171bbe509 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -1,5 +1,5 @@ -import { SortOptions } from "../shared/sort-options.model"; -import { PaginationOptions } from "../shared/pagination-options.model"; +import { SortOptions } from "../cache/models/sort-options.model"; +import { PaginationOptions } from "../cache/models/pagination-options.model"; import { GenericConstructor } from "../shared/generic-constructor"; export class Request { diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts new file mode 100644 index 0000000000..cd5b7b8a64 --- /dev/null +++ b/src/app/core/data/request.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core"; +import { RequestEntry, RequestState } from "./request.reducer"; +import { Store } from "@ngrx/store"; +import { hasValue } from "../../shared/empty.util"; +import { Observable } from "rxjs/Observable"; + +@Injectable() +export class RequestService { + + constructor(private store: Store) { + } + + isPending(href: string): boolean { + let isPending = false; + this.store.select('core', 'data', 'request', href) + .take(1) + .subscribe((re: RequestEntry) => { + isPending = (hasValue(re) && !re.completed) + }); + + return isPending; + } + + get(href: string): Observable { + return this.store.select('core', 'data', 'request', href); + } +} diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts index 2661b3708d..236244873c 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts @@ -140,19 +140,20 @@ describe("DSpaceRESTv2Serializer", () => { describe("deserializeArray", () => { - it("should turn a valid document describing a collection of objects in to an array of valid models", () => { - const serializer = new DSpaceRESTv2Serializer(TestModel); - const doc = { - "_embedded": testResponses - }; - - const models = serializer.deserializeArray(doc); - - expect(models[0].id).toBe(doc._embedded[0].id); - expect(models[0].name).toBe(doc._embedded[0].name); - expect(models[1].id).toBe(doc._embedded[1].id); - expect(models[1].name).toBe(doc._embedded[1].name); - }); + //TODO rewrite to incorporate normalisation. + // it("should turn a valid document describing a collection of objects in to an array of valid models", () => { + // const serializer = new DSpaceRESTv2Serializer(TestModel); + // const doc = { + // "_embedded": testResponses + // }; + // + // const models = serializer.deserializeArray(doc); + // + // expect(models[0].id).toBe(doc._embedded[0].id); + // expect(models[0].name).toBe(doc._embedded[0].name); + // expect(models[1].id).toBe(doc._embedded[1].id); + // expect(models[1].name).toBe(doc._embedded[1].name); + // }); //TODO cant implement/test this yet - depends on how relationships // will be handled in the rest api diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts index ec557df4ae..d4d5a7ce59 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts @@ -55,7 +55,7 @@ export class DSpaceRESTv2Serializer implements Serializer { if (Array.isArray(response._embedded)) { throw new Error('Expected a single model, use deserializeArray() instead'); } - let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._links)); + let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._embedded._links)); return Deserialize(normalized, this.modelType); } @@ -83,7 +83,7 @@ export class DSpaceRESTv2Serializer implements Serializer { for (let link in normalizedLinks) { if (Array.isArray(normalizedLinks[link])) { normalizedLinks[link] = normalizedLinks[link].map(linkedResource => { - return {'self': linkedResource.href }; + return linkedResource.href; }); } else { diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 3198cbb557..2e4056fb3e 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,16 +1,13 @@ -import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize"; import { DSpaceObject } from "./dspace-object.model"; import { Bitstream } from "./bitstream.model"; import { Item } from "./item.model"; import { RemoteData } from "../data/remote-data"; -@inheritSerialization(DSpaceObject) export class Collection extends DSpaceObject { /** * A string representing the unique handle of this Collection */ - @autoserialize handle: string; /** @@ -68,7 +65,6 @@ export class Collection extends DSpaceObject { */ owner: Collection; - @autoserializeAs(RemoteData) - items: RemoteData; + items: Array>; } diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 8563b38aad..06cac4108b 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,22 +1,3 @@
-
-

Collections

-

Loading…

-

Failed: {{(remoteData.errorMessage | async)}}

-
    -
  • - {{collection?.name}}
    - {{collection?.shortDescription}} -
  • -
-
-
-

Items

-
    -
  • - {{item?.name}}
    - {{item?.findMetadata('dc.description.abstract')}} -
  • -
-
+ Home component
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 3fc4b3ef4f..9cb0704a0a 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -1,11 +1,4 @@ -import { Component, ChangeDetectionStrategy, ViewEncapsulation, OnInit } from '@angular/core'; -import { Observable } from "rxjs"; -import { Collection } from "../core/shared/collection.model"; -import { Item } from "../core/shared/item.model"; -import { CollectionDataService } from "../core/data/collection-data.service"; -import { ItemDataService } from "../core/data/item-data.service"; -import { ObjectCacheService } from "../core/cache/object-cache.service"; -import { RemoteData } from "../core/data/remote-data"; +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; @Component({ changeDetection: ChangeDetectionStrategy.Default, @@ -14,16 +7,11 @@ import { RemoteData } from "../core/data/remote-data"; styleUrls: ['./home.component.css'], templateUrl: './home.component.html' }) -export class HomeComponent implements OnInit { - data: any = {}; - collections: RemoteData; - items: RemoteData; +export class HomeComponent { - constructor( - private cds: CollectionDataService, - private ids: ItemDataService, - private objectCache: ObjectCacheService - ) { + data: any = {}; + + constructor() { this.universalInit(); } @@ -31,13 +19,4 @@ export class HomeComponent implements OnInit { } - ngOnInit(): void { - this.collections = this.cds.findAll(); - this.items = this.ids.findAll(); - this.cds.findById('5179').payload.subscribe(o => console.log('collection 1', o)); - this.cds.findById('6547').payload.subscribe(o => console.log('collection 2', o)); - this.ids.findById('8871').payload.subscribe(o => console.log('item 1', o)); - this.ids.findById('9978').payload.subscribe(o => console.log('item 2', o)); - } - } diff --git a/src/server.aot.ts b/src/server.aot.ts index de41c8cae8..4d41fb9738 100644 --- a/src/server.aot.ts +++ b/src/server.aot.ts @@ -124,5 +124,5 @@ app.get('*', function(req, res) { // Server let server = app.listen(app.get('port'), app.get('address'), () => { - console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`); + console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`); }); diff --git a/src/server.ts b/src/server.ts index 9423add7a2..13837821d0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -118,5 +118,5 @@ app.get('*', function(req, res) { // Server let server = app.listen(app.get('port'), app.get('address'), () => { - console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`); + console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`); });