switch to live rest-backend, still issue with bitstreams on item page

This commit is contained in:
Art Lowel
2017-06-07 18:25:52 +02:00
parent a8781de044
commit 0677bc38ee
26 changed files with 247 additions and 167 deletions

View File

@@ -2,10 +2,10 @@ module.exports = {
// The REST API server settings.
"rest": {
"ssl": false,
"address": "localhost",
"port": 3000,
"address": "dspace7.4science.it",
"port": 80,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
"nameSpace": "/api"
"nameSpace": "/dspace-spring-rest/api"
},
// Angular2 UI server settings.
"ui": {

View File

@@ -45,6 +45,11 @@ export class RemoteDataBuildService {
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged();
const statusCode = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry))
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
.distinctUntilChanged();
const payload =
Observable.race(
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType),
@@ -70,6 +75,7 @@ export class RemoteDataBuildService {
responsePending,
isSuccessFul,
errorMessage,
statusCode,
payload
);
}
@@ -93,6 +99,11 @@ export class RemoteDataBuildService {
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
.distinctUntilChanged();
const statusCode = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry))
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
.distinctUntilChanged();
const payload = responseCacheObs
.filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
@@ -112,6 +123,7 @@ export class RemoteDataBuildService {
responsePending,
isSuccessFul,
errorMessage,
statusCode,
payload
);
}
@@ -186,6 +198,18 @@ export class RemoteDataBuildService {
.join(", ")
);
const statusCode = Observable.combineLatest(
...input.map(rd => rd.statusCode),
).map((...statusCodes) => statusCodes
.map((code, idx) => {
if (hasValue(code)) {
return `[${idx}]: ${code}`;
}
})
.filter(c => hasValue(c))
.join(", ")
);
const payload = <Observable<T[]>> Observable.combineLatest(
...input.map(rd => rd.payload)
);
@@ -199,6 +223,7 @@ export class RemoteDataBuildService {
responsePending,
isSuccessFul,
errorMessage,
statusCode,
payload
);
}

View File

@@ -1,7 +1,8 @@
import { inheritSerialization, autoserialize } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
import { Bitstream } from "../../shared/bitstream.model";
import { mapsTo } from "../builders/build-decorators";
import { mapsTo, relationship } from "../builders/build-decorators";
import { ResourceType } from "../../shared/resource-type";
@mapsTo(Bitstream)
@inheritSerialization(NormalizedDSpaceObject)
@@ -16,6 +17,7 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
/**
* The relative path to this Bitstream's file
*/
@autoserialize
url: string;
/**
@@ -27,23 +29,32 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
/**
* The format of this Bitstream
*/
@autoserialize
format: string;
/**
* The description of this Bitstream
*/
@autoserialize
description: string;
/**
* An array of Bundles that are direct parents of this Bitstream
*/
@autoserialize
@relationship(ResourceType.Item)
parents: Array<string>;
/**
* The Bundle that owns this Bitstream
*/
@autoserialize
@relationship(ResourceType.Item)
owner: string;
/**
* The name of the Bundle this Bitstream is part of
*/
@autoserialize
retrieve: string;
bundleName: string;
}

View File

@@ -20,13 +20,17 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
logo: string;
/**
* An array of Collections that are direct parents of this Collection
* An array of Communities that are direct parents of this Collection
*/
@autoserialize
@relationship(ResourceType.Community)
parents: Array<string>;
/**
* The Collection that owns this Collection
* The Community that owns this Collection
*/
@autoserialize
@relationship(ResourceType.Community)
owner: string;
@autoserialize

View File

@@ -22,11 +22,15 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
/**
* An array of Communities that are direct parents of this Community
*/
@autoserialize
@relationship(ResourceType.Community)
parents: Array<string>;
/**
* The Community that owns this Community
*/
@autoserialize
@relationship(ResourceType.Community)
owner: string;
@autoserialize

View File

@@ -13,8 +13,11 @@ export abstract class NormalizedDSpaceObject implements CacheableObject {
/**
* The human-readable identifier of this DSpaceObject
*
* Currently mapped to uuid but left in to leave room
* for a shorter, more user friendly type of id
*/
@autoserialize
@autoserializeAs(String, 'uuid')
id: string;
/**

View File

@@ -1,4 +1,4 @@
import { inheritSerialization, autoserialize } from "cerialize";
import { inheritSerialization, autoserialize, autoserializeAs } from "cerialize";
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
import { Item } from "../../shared/item.model";
import { mapsTo, relationship } from "../builders/build-decorators";
@@ -17,16 +17,25 @@ export class NormalizedItem extends NormalizedDSpaceObject {
/**
* The Date of the last modification of this Item
*/
@autoserialize
lastModified: Date;
/**
* A boolean representing if this Item is currently archived or not
*/
@autoserializeAs(Boolean, 'inArchive')
isArchived: boolean;
/**
* A boolean representing if this Item is currently discoverable or not
*/
@autoserializeAs(Boolean, 'discoverable')
isDiscoverable: boolean;
/**
* A boolean representing if this Item is currently withdrawn or not
*/
@autoserializeAs(Boolean, 'withdrawn')
isWithdrawn: boolean;
/**
@@ -39,9 +48,11 @@ export class NormalizedItem extends NormalizedDSpaceObject {
/**
* The Collection that owns this Item
*/
@autoserializeAs(String, 'owningCollection')
@relationship(ResourceType.Collection)
owner: string;
@autoserialize
@relationship(ResourceType.Bundle)
bundles: Array<string>;
@relationship(ResourceType.Bitstream)
bitstreams: Array<string>;
}

View File

@@ -1,18 +1,25 @@
import { RequestError } from "../data/request.models";
export class Response {
constructor(public isSuccessful: boolean) {}
constructor(
public isSuccessful: boolean,
public statusCode: string
) {}
}
export class SuccessResponse extends Response {
constructor(public resourceUUIDs: Array<String>) {
super(true);
constructor(
public resourceUUIDs: Array<String>,
public statusCode: string
) {
super(true, statusCode);
}
}
export class ErrorResponse extends Response {
errorMessage: string;
constructor(error: Error) {
super(false);
constructor(error: RequestError) {
super(false, error.statusText);
console.error(error);
this.errorMessage = error.message;
}

View File

@@ -11,7 +11,7 @@ import { RemoteDataBuildService } from "../cache/builders/remote-data-build.serv
@Injectable()
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
protected endpoint = '/collections';
protected endpoint = '/core/collections';
constructor(
protected objectCache: ObjectCacheService,

View File

@@ -11,7 +11,7 @@ import { RemoteDataBuildService } from "../cache/builders/remote-data-build.serv
@Injectable()
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
protected endpoint = '/communities';
protected endpoint = '/core/communities';
constructor(
protected objectCache: ObjectCacheService,

View File

@@ -11,7 +11,7 @@ import { RemoteDataBuildService } from "../cache/builders/remote-data-build.serv
@Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> {
protected endpoint = '/items';
protected endpoint = '/core/items';
constructor(
protected objectCache: ObjectCacheService,

View File

@@ -17,6 +17,7 @@ export class RemoteData<T> {
private responsePending: Observable<boolean>,
private isSuccessFul: Observable<boolean>,
public errorMessage: Observable<string>,
public statusCode: Observable<string>,
public payload: Observable<T>
) {
}

View File

@@ -1,6 +1,5 @@
import { Injectable, Inject } from "@angular/core";
import { Actions, Effect } from "@ngrx/effects";
import { Store } from "@ngrx/store";
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
import { ObjectCacheService } from "../cache/object-cache.service";
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
@@ -10,7 +9,7 @@ import { Observable } from "rxjs";
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from "../../shared/empty.util";
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
import { RequestState, RequestEntry } from "./request.reducer";
import { RequestEntry } from "./request.reducer";
import {
RequestActionTypes, RequestExecuteAction,
RequestCompleteAction
@@ -19,11 +18,16 @@ import { ResponseCacheService } from "../cache/response-cache.service";
import { RequestService } from "./request.service";
import { NormalizedObjectFactory } from "../cache/models/normalized-object-factory";
import { ResourceType } from "../shared/resource-type";
import { RequestError } from "./request.models";
function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
}
function isPaginatedResponse(halObj: any) {
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
}
@Injectable()
export class RequestEffects {
@@ -33,8 +37,7 @@ export class RequestEffects {
private restApi: DSpaceRESTv2Service,
private objectCache: ObjectCacheService,
private responseCache: ResponseCacheService,
protected requestService: RequestService,
private store: Store<RequestState>
protected requestService: RequestService
) { }
@Effect() execute = this.actions$
@@ -45,27 +48,31 @@ export class RequestEffects {
})
.flatMap((entry: RequestEntry) => {
return this.restApi.get(entry.request.href)
.map((data: DSpaceRESTV2Response) => this.processEmbedded(data._embedded))
.map((ids: Array<string>) => new SuccessResponse(ids))
.map((data: DSpaceRESTV2Response) => new SuccessResponse(this.process(data.payload), data.statusCode))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href))
.catch((error: Error) => Observable.of(new ErrorResponse(error))
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
});
protected processEmbedded(_embedded: any): Array<string> {
protected process(data: any): Array<string> {
if (isNotEmpty(_embedded)) {
if (isObjectLevel(_embedded)) {
return this.deserializeAndCache(_embedded);
if (isNotEmpty(data)) {
if (isPaginatedResponse(data)) {
//TODO parse page object
return this.process(data._embedded);
}
else if (isObjectLevel(data)) {
return this.deserializeAndCache(data);
}
else {
let uuids = [];
Object.keys(_embedded)
.filter(property => _embedded.hasOwnProperty(property))
Object.keys(data)
.filter(property => data.hasOwnProperty(property))
.filter(property => hasValue(data[property]))
.forEach(property => {
uuids = [...uuids, ...this.deserializeAndCache(_embedded[property])];
uuids = [...uuids, ...this.deserializeAndCache(data[property])];
});
return uuids;
}
@@ -96,7 +103,7 @@ export class RequestEffects {
if (isArray) {
obj.forEach(o => {
if (isNotEmpty(o._embedded)) {
this.processEmbedded(o._embedded);
this.process(o._embedded);
}
});
const normalizedObjArr = serializer.deserializeArray(obj);
@@ -105,7 +112,7 @@ export class RequestEffects {
}
else {
if (isNotEmpty(obj._embedded)) {
this.processEmbedded(obj._embedded);
this.process(obj._embedded);
}
const normalizedObj = serializer.deserialize(obj);
this.addToObjectCache(normalizedObj);

View File

@@ -27,3 +27,7 @@ export class FindAllRequest<T> extends Request<T> {
super(href);
}
}
export class RequestError extends Error {
statusText: string;
}

View File

@@ -1,4 +1,7 @@
export interface DSpaceRESTV2Response {
payload: {
_embedded?: any;
_links?: any;
},
statusCode: string
}

View File

@@ -26,10 +26,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
* @param model The model to serialize
* @returns An object to send to the backend
*/
serialize(model: T): DSpaceRESTV2Response {
return {
"_embedded": Serialize(model, this.modelType)
};
serialize(model: T): any {
return Serialize(model, this.modelType);
}
/**
@@ -38,10 +36,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
* @param models The array of models to serialize
* @returns An object to send to the backend
*/
serializeArray(models: Array<T>): DSpaceRESTV2Response {
return {
"_embedded": Serialize(models, this.modelType)
};
serializeArray(models: Array<T>): any {
return Serialize(models, this.modelType);
}
/**

View File

@@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable';
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
/**
* Service to access DSpace's REST API
@@ -24,9 +25,9 @@ export class DSpaceRESTv2Service {
* @return {Observable<string>}
* An Observablse<string> containing the response from the server
*/
get(relativeURL: string, options?: RequestOptionsArgs): Observable<string> {
get(relativeURL: string, options?: RequestOptionsArgs): Observable<DSpaceRESTV2Response> {
return this.http.get(new RESTURLCombiner(this.EnvConfig, relativeURL).toString(), options)
.map(res => res.json())
.map(res => ({ payload: res.json(), statusCode: res.statusText }))
.catch(err => {
console.log('Error: ', err);
return Observable.throw(err);

View File

@@ -1,6 +1,6 @@
import { DSpaceObject } from "./dspace-object.model";
import { Bundle } from "./bundle.model";
import { RemoteData } from "../data/remote-data";
import { Item } from "./item.model";
export class Bitstream extends DSpaceObject {
@@ -25,14 +25,19 @@ export class Bitstream extends DSpaceObject {
description: string;
/**
* An array of Bundles that are direct parents of this Bitstream
* The name of the Bundle this Bitstream is part of
*/
parents: RemoteData<Bundle[]>;
bundleName: string;
/**
* An array of Items that are direct parents of this Bitstream
*/
parents: RemoteData<Item[]>;
/**
* The Bundle that owns this Bitstream
*/
owner: Bundle;
owner: RemoteData<Item>;
/**
* The Bundle that owns this Bitstream

View File

@@ -17,7 +17,7 @@ export class Bundle extends DSpaceObject {
/**
* The Item that owns this Bundle
*/
owner: Item;
owner: RemoteData<Item>;
bitstreams: RemoteData<Bitstream[]>

View File

@@ -63,7 +63,7 @@ export class Collection extends DSpaceObject {
/**
* The Collection that owns this Collection
*/
owner: Collection;
owner: RemoteData<Collection>;
items: RemoteData<Item[]>;

View File

@@ -55,7 +55,7 @@ export class Community extends DSpaceObject {
/**
* The Community that owns this Community
*/
owner: Community;
owner: RemoteData<Community>;
collections: RemoteData<Collection[]>;

View File

@@ -44,7 +44,7 @@ export abstract class DSpaceObject implements CacheableObject {
/**
* The DSpaceObject that owns this DSpaceObject
*/
owner: DSpaceObject;
owner: RemoteData<DSpaceObject>;
/**
* Find a metadata field by key and language

View File

@@ -1,10 +1,9 @@
import { DSpaceObject } from "./dspace-object.model";
import { Collection } from "./collection.model";
import { RemoteData } from "../data/remote-data";
import { Bundle } from "./bundle.model";
import { Bitstream } from "./bitstream.model";
import { Observable } from "rxjs";
import { hasValue } from "../../shared/empty.util";
import { isNotEmpty } from "../../shared/empty.util";
export class Item extends DSpaceObject {
@@ -23,6 +22,11 @@ export class Item extends DSpaceObject {
*/
isArchived: boolean;
/**
* A boolean representing if this Item is currently discoverable or not
*/
isDiscoverable: boolean;
/**
* A boolean representing if this Item is currently withdrawn or not
*/
@@ -36,9 +40,9 @@ export class Item extends DSpaceObject {
/**
* The Collection that owns this Item
*/
owner: Collection;
owner: RemoteData<Collection>;
bundles: RemoteData<Bundle[]>;
bitstreams: RemoteData<Bitstream[]>;
/**
@@ -46,11 +50,12 @@ export class Item extends DSpaceObject {
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
*/
getThumbnail(): Observable<Bitstream> {
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
return bundle
.filter(bundle => hasValue(bundle))
.flatMap(bundle => bundle.primaryBitstream.payload)
.startWith(undefined);
//TODO currently this just picks the first thumbnail
//should be adjusted when we have a way to determine
//the primary thumbnail from rest
return this.getBitstreamsByBundleName("THUMBNAIL")
.filter(thumbnails => isNotEmpty(thumbnails))
.map(thumbnails => thumbnails[0])
}
/**
@@ -58,45 +63,31 @@ export class Item extends DSpaceObject {
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
*/
getThumbnailForOriginal(original: Bitstream): Observable<Bitstream> {
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
return bundle
.filter(bundle => hasValue(bundle))
.flatMap(bundle => bundle
.bitstreams.payload.map(files => files
return this.getBitstreamsByBundleName("THUMBNAIL").map(files => files
.find(thumbnail => thumbnail
.name.startsWith(original.name)
)
)
)
.startWith(undefined);;
).startWith(undefined);
}
/**
* Retrieves all files that should be displayed on the item page of this item
* @returns {Observable<Array<Observable<Bitstream>>>} an array of all Bitstreams in the "ORIGINAL" bundle
*/
getFiles(name: String = "ORIGINAL"): Observable<Bitstream[]> {
const bundle: Observable <Bundle> = this.getBundle(name);
return bundle
.filter(bundle => hasValue(bundle))
.flatMap(bundle => bundle.bitstreams.payload)
.startWith([]);
getFiles(): Observable<Bitstream[]> {
return this.getBitstreamsByBundleName("ORIGINAL");
}
/**
* Retrieves the bundle of this item by its name
* @param name The name of the Bundle that should be returned
* @returns {Observable<Bundle>} the Bundle that belongs to this item with the given name
* Retrieves bitstreams by bundle name
* @param bundleName The name of the Bundle that should be returned
* @returns {Observable<Bitstream[]>} the bitstreams with the given bundleName
*/
getBundle(name: String): Observable<Bundle> {
return this.bundles.payload
.filter(bundles => hasValue(bundles))
.map(bundles => {
return bundles.find((bundle: Bundle) => {
return bundle.name === name
});
})
.startWith(undefined);
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
return this.bitstreams.payload.startWith([])
.map(bitstreams => bitstreams
.filter(bitstream => bitstream.bundleName === bundleName)
);
}
}

View File

@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
import { Collection } from "../../../core/shared/collection.model";
import { Observable } from "rxjs";
import { Item } from "../../../core/shared/item.model";
import { RemoteDataBuildService } from "../../../core/cache/builders/remote-data-build.service";
/**
* This component renders the parent collections section of the item
@@ -22,7 +23,9 @@ export class CollectionsComponent implements OnInit {
collections: Observable<Collection[]>;
constructor() {
constructor(
private rdbs: RemoteDataBuildService
) {
this.universalInit();
}
@@ -31,7 +34,11 @@ export class CollectionsComponent implements OnInit {
}
ngOnInit(): void {
this.collections = this.item.parents.payload;
// this.collections = this.item.parents.payload;
//TODO this should use parents, but the collections
// for an Item aren't returned by the REST API yet,
// only the owning collection
this.collections = this.rdbs.aggregate([this.item.owner]).payload
}

View File

@@ -35,8 +35,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
}
initialize(): void {
const originals = this.item.getFiles("ORIGINAL");
const licenses = this.item.getFiles("LICENSE");
const originals = this.item.getFiles();
const licenses = this.item.getBitstreamsByBundleName("LICENSE");
this.files = Observable.combineLatest(originals, licenses, (originals, licenses) => [...originals, ...licenses]);
this.files.subscribe(
files =>

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper [label]="label | translate">
<div class="file-section">
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.retrieve">
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.url">
<span>{{file?.name}}</span>
<span>({{(file?.size) | dsFileSize }})</span>
<span *ngIf="!last" innerHTML="{{separator}}"></span>