diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 5d1456c09b..ba8cd81245 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -160,6 +160,7 @@ import { EndUserAgreementService } from './end-user-agreement/end-user-agreement import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { UsageReport } from './statistics/models/usage-report.model'; +import { RootDataService } from './data/root-data.service'; import { Root } from './data/root.model'; /** @@ -279,6 +280,7 @@ const PROVIDERS = [ EndUserAgreementCurrentUserGuard, EndUserAgreementCookieGuard, EndUserAgreementService, + RootDataService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -353,6 +355,7 @@ export const models = ShortLivedToken, Registration, UsageReport, + Root, ]; @NgModule({ diff --git a/src/app/core/data/root-data.service.spec.ts b/src/app/core/data/root-data.service.spec.ts new file mode 100644 index 0000000000..80a180e463 --- /dev/null +++ b/src/app/core/data/root-data.service.spec.ts @@ -0,0 +1,38 @@ +import { RootDataService } from './root-data.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Observable } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { Root } from './root.model'; + +describe('RootDataService', () => { + let service: RootDataService; + let halService: HALEndpointService; + let rootEndpoint; + + beforeEach(() => { + rootEndpoint = 'root-endpoint'; + halService = jasmine.createSpyObj('halService', { + getRootHref: rootEndpoint + }); + service = new RootDataService(null, null, null, null, halService, null, null, null); + (service as any).dataService = jasmine.createSpyObj('dataService', { + findByHref: createSuccessfulRemoteDataObject$({}) + }); + }); + + describe('findRoot', () => { + let result$: Observable>; + + beforeEach(() => { + result$ = service.findRoot(); + }); + + it('should call findByHref using the root endpoint', (done) => { + result$.subscribe(() => { + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(rootEndpoint, true); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/root-data.service.ts b/src/app/core/data/root-data.service.ts new file mode 100644 index 0000000000..cd55aa7f7c --- /dev/null +++ b/src/app/core/data/root-data.service.ts @@ -0,0 +1,103 @@ +import { DataService } from './data.service'; +import { Root } from './root.model'; +import { Injectable } from '@angular/core'; +import { ROOT } from './root.resource-type'; +import { dataService } from '../cache/builders/build-decorators'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { Observable } from 'rxjs'; +import { RemoteData } from './remote-data'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { FindListOptions } from './request.models'; +import { PaginatedList } from './paginated-list.model'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = ''; + protected responseMsToLive = 6 * 60 * 60 * 1000; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } +} + +/** + * A service to retrieve the {@link Root} object from the REST API. + */ +@Injectable() +@dataService(ROOT) +export class RootDataService { + /** + * A private DataService instance to delegate specific methods to. + */ + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Find the {@link Root} object of the REST API + * @param reRequestOnStale Whether or not the request should automatically be re-requested after + * the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + findRoot(reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(this.halService.getRootHref(), reRequestOnStale, ...linksToFollow); + } + + /** + * Returns an observable of {@link RemoteData} of an object, based on an href, with a list of + * {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param reRequestOnStale Whether or not the request should automatically be re-requested after + * the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s + * should be automatically resolved + */ + findByHref(href: string, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { + return this.dataService.findByHref(href, reRequestOnStale, ...linksToFollow); + } + + /** + * Returns a list of observables of {@link RemoteData} of objects, based on an href, with a list + * of {@link FollowLinkConfig}, to automatically resolve {@link HALLink}s of the object + * @param href The url of object we want to retrieve + * @param findListOptions Find list options object + * @param reRequestOnStale Whether or not the request should automatically be re-requested after + * the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s + * should be automatically resolved + */ + findAllByHref(href: string, findListOptions: FindListOptions = {}, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.dataService.findAllByHref(href, findListOptions, reRequestOnStale, ...linksToFollow); + } +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/root.model.ts b/src/app/core/data/root.model.ts index b7411f53c8..4840051df5 100644 --- a/src/app/core/data/root.model.ts +++ b/src/app/core/data/root.model.ts @@ -38,6 +38,12 @@ export class Root implements CacheableObject { @autoserialize dspaceServer: string; + /** + * The current DSpace version + */ + @autoserialize + dspaceVersion: string; + /** * The {@link HALLink}s for the root object */ diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 59bf0fbd39..bc323decfd 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -53,6 +53,8 @@ import { environment } from '../../../environments/environment'; import { storeModuleConfig } from '../../app.reducer'; import { HardRedirectService } from '../services/hard-redirect.service'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { RootDataService } from '../data/root-data.service'; +import { Root } from '../data/root.model'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -91,6 +93,7 @@ describe('MetadataService', () => { let remoteDataBuildService: RemoteDataBuildService; let itemDataService: ItemDataService; let authService: AuthService; + let rootService: RootDataService; let location: Location; let router: Router; @@ -130,6 +133,11 @@ describe('MetadataService', () => { } } }; + rootService = jasmine.createSpyObj('rootService', { + findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), { + dspaceVersion: 'mock-dspace-version' + })) + }); TestBed.configureTestingModule({ imports: [ @@ -168,6 +176,7 @@ describe('MetadataService', () => { { provide: DefaultChangeAnalyzer, useValue: {} }, { provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService }, { provide: BitstreamDataService, useValue: mockBitstreamDataService }, + { provide: RootDataService, useValue: rootService }, Meta, Title, // tslint:disable-next-line:no-empty @@ -225,17 +234,18 @@ describe('MetadataService', () => { expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); })); - it('other navigation should title and description', fakeAsync(() => { + it('other navigation should add title, description and Generator', fakeAsync(() => { spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); expect(tagStore.size).toBeGreaterThan(0); router.navigate(['/other']); tick(); - expect(tagStore.size).toEqual(2); + expect(tagStore.size).toEqual(3); expect(title.getTitle()).toEqual('Dummy Title'); expect(tagStore.get('title')[0].content).toEqual('Dummy Title'); expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!'); + expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version'); })); describe('when the item has no bitstreams', () => { diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 90171bac10..414fb13a88 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -22,6 +22,7 @@ import { Item } from '../shared/item.model'; import { getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload } from '../shared/operators'; import { HardRedirectService } from '../services/hard-redirect.service'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { RootDataService } from '../data/root-data.service'; @Injectable() export class MetadataService { @@ -40,7 +41,8 @@ export class MetadataService { private dsoNameService: DSONameService, private bitstreamDataService: BitstreamDataService, private bitstreamFormatDataService: BitstreamFormatDataService, - private redirectService: HardRedirectService + private redirectService: HardRedirectService, + private rootService: RootDataService ) { // TODO: determine what open graph meta tags are needed and whether // the differ per route. potentially add image based on DSpaceObject @@ -91,6 +93,8 @@ export class MetadataService { this.addMetaTag('description', translatedDescription); }); } + + this.setGenerator(); } private initialize(dspaceObject: DSpaceObject): void { @@ -290,6 +294,15 @@ export class MetadataService { } } + /** + * Add to the containing the current DSpace version + */ + private setGenerator(): void { + this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => { + this.addMetaTag('Generator', root.dspaceVersion); + }); + } + private hasType(value: string): boolean { return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); } diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 943fd92758..c68dedab8d 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -20,7 +20,7 @@ export class HALEndpointService { ) { } - protected getRootHref(): string { + public getRootHref(): string { return new RESTURLCombiner().toString(); }