diff --git a/README.md b/README.md index 78d7816f65..5d8a323461 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false] The same settings can also be overwritten by setting system environment variables instead, E.g.: ```bash -export DSPACE_HOST=https://dspace7.4science.cloud/server +export DSPACE_HOST=dspace7.4science.cloud ``` The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`** diff --git a/package.json b/package.json index 8d3c936212..21a89400bf 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "moment": "^2.22.1", "morgan": "^1.9.1", "ng-mocks": "^8.1.0", - "ng2-file-upload": "1.2.1", + "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.2", "ngx-bootstrap": "^5.3.2", "ngx-infinite-scroll": "6.0.1", diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts index 581d3341da..e3e8d08753 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.spec.ts @@ -13,6 +13,8 @@ import { AuthService } from '../../core/auth/auth.service'; import { of as observableOf } from 'rxjs'; import { By } from '@angular/platform-browser'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; describe('AdminSidebarComponent', () => { let comp: AdminSidebarComponent; @@ -21,13 +23,14 @@ describe('AdminSidebarComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NoopAnimationsModule], + imports: [TranslateModule.forRoot(), NoopAnimationsModule, RouterTestingModule], declarations: [AdminSidebarComponent], providers: [ { provide: Injector, useValue: {} }, { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: AuthService, useClass: AuthServiceStub }, + { provide: ActivatedRoute, useValue: {} }, { provide: NgbModal, useValue: { open: () => {/*comment*/} diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 5d885b918b..eedebae80b 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -93,7 +93,7 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { /** * Initialize all menu sections and items for this menu */ - private createMenu() { + createMenu() { const menuList = [ /* News */ { @@ -145,11 +145,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { this.modalService.open(CreateItemParentSelectorComponent); } } as OnClickMenuItemModel, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.new_item', - // link: '/submit' - // } as LinkMenuItemModel, + }, + { + id: 'new_process', + parentID: 'new', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.new_process', + link: '/processes/new' + } as LinkMenuItemModel, }, { id: 'new_item_version', @@ -439,6 +445,19 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { icon: 'cogs', index: 9 }, + /* Processes */ + { + id: 'processes', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: 'menu.section.processes', + link: '/processes' + } as LinkMenuItemModel, + icon: 'terminal', + index: 10 + }, /* Workflow */ { id: 'workflow', @@ -453,7 +472,9 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { index: 10 }, ]; - menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection)); + menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, { + shouldPersistOnRouteChange: true + }))); } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index b8ab9bdb41..7c1719eb82 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -21,9 +21,9 @@
- + {{"item.page.filesection.download" | translate}} - +
diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.html b/src/app/+item-page/simple/field-components/file-section/file-section.component.html index 6533322e03..17e4a795e7 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.html @@ -1,11 +1,11 @@ diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index aace169f6c..3bd2f64961 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -114,6 +114,7 @@ export function getDSOPath(dso: DSpaceObject): string { path: PROFILE_MODULE_PATH, loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] }, + { path: 'processes', loadChildren: './process-page/process-page.module#ProcessPageModule', canActivate: [AuthenticatedGuard] }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ], { diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index accd0f23a5..6868b8d546 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -462,7 +462,7 @@ describe('CommunityListService', () => { }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { - beforeAll((done) => { + beforeEach((done) => { service.transformCommunity(communityWithSubcoms, 0, null, null) .pipe(take(1)) .subscribe((value) => { diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 465fb69dd2..93f55389f9 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,12 +1,18 @@ import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; -import { Inject, Injectable } from '@angular/core'; +import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; import { isNotEmpty } from '../../shared/empty.util'; -import { AuthGetRequest, AuthPostRequest, GetRequest, PostRequest, RestRequest } from '../data/request.models'; -import { AuthStatusResponse, ErrorResponse } from '../cache/response.models'; +import { + AuthGetRequest, + AuthPostRequest, + GetRequest, + PostRequest, + RestRequest, + TokenPostRequest +} from '../data/request.models'; +import { AuthStatusResponse, ErrorResponse, TokenResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { getResponseFromEntry } from '../shared/operators'; import { HttpClient } from '@angular/common/http'; @@ -15,6 +21,7 @@ import { HttpClient } from '@angular/common/http'; export class AuthRequestService { protected linkName = 'authn'; protected browseEndpoint = ''; + protected shortlivedtokensEndpoint = 'shortlivedtokens'; constructor(protected halService: HALEndpointService, protected requestService: RequestService, @@ -67,4 +74,19 @@ export class AuthRequestService { mergeMap((request: GetRequest) => this.fetchRequest(request)), distinctUntilChanged()); } + + /** + * Send a POST request to retrieve a short-lived token which provides download access of restricted files + */ + public getShortlivedToken(): Observable { + return this.halService.getEndpoint(`${this.linkName}/${this.shortlivedtokensEndpoint}`).pipe( + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new TokenPostRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: PostRequest) => this.requestService.configure(request)), + switchMap((request: PostRequest) => this.requestService.getByUUID(request.uuid)), + getResponseFromEntry(), + map((response: TokenResponse) => response.token) + ); + } } diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 3b6fae4dd1..a15d604cc4 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -1,17 +1,14 @@ import { async, inject, TestBed } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; - import { Store, StoreModule } from '@ngrx/store'; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { of as observableOf } from 'rxjs'; - import { authReducer, AuthState } from './auth.reducer'; import { NativeWindowRef, NativeWindowService } from '../services/window.service'; import { AuthService, IMPERSONATING_COOKIE } from './auth.service'; import { RouterStub } from '../../shared/testing/router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; - import { CookieService } from '../services/cookie.service'; import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub'; import { AuthRequestService } from './auth-request.service'; @@ -49,6 +46,7 @@ describe('AuthService test', () => { let storage: CookieService; let token: AuthTokenInfo; let authenticatedState; + let unAuthenticatedState; let linkService; function init() { @@ -67,6 +65,13 @@ describe('AuthService test', () => { authToken: token, user: EPersonMock }; + unAuthenticatedState = { + authenticated: false, + loaded: true, + loading: false, + authToken: undefined, + user: undefined + }; authRequest = new AuthRequestServiceStub(); routeStub = new ActivatedRouteStub(); linkService = { @@ -214,6 +219,12 @@ describe('AuthService test', () => { }); }); + it('should return the shortlived token when user is logged in', () => { + authService.getShortlivedToken().subscribe((shortlivedToken: string) => { + expect(shortlivedToken).toEqual(authRequest.mockShortLivedToken); + }); + }); + it('should return token object when it is valid', () => { authService.hasValidAuthenticationToken().subscribe((tokenState: AuthTokenInfo) => { expect(tokenState).toBe(token); @@ -448,4 +459,44 @@ describe('AuthService test', () => { }); }); }); + + describe('when user is not logged in', () => { + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({ authReducer }, { + runtimeChecks: { + strictStateImmutability: false, + strictActionImmutability: false + } + }) + ], + providers: [ + { provide: AuthRequestService, useValue: authRequest }, + { provide: REQUEST, useValue: {} }, + { provide: Router, useValue: routerStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: RemoteDataBuildService, useValue: linkService }, + CookieService, + AuthService + ] + }).compileComponents(); + })); + + beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store, router: Router, routeService: RouteService) => { + store + .subscribe((state) => { + (state as any).core = Object.create({}); + (state as any).core.auth = unAuthenticatedState; + }); + authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store); + })); + + it('should return null for the shortlived token', () => { + authService.getShortlivedToken().subscribe((shortlivedToken: string) => { + expect(shortlivedToken).toBeNull(); + }); + }); + }); }); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index f9c1fc2cb9..fe9828bc73 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -534,4 +534,14 @@ export class AuthService { return this.getImpersonateID() === epersonId; } + /** + * Get a short-lived token for appending to download urls of restricted files + * Returns null if the user isn't authenticated + */ + getShortlivedToken(): Observable { + return this.isAuthenticated().pipe( + switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)) + ); + } + } diff --git a/src/app/core/auth/token-response-parsing.service.spec.ts b/src/app/core/auth/token-response-parsing.service.spec.ts new file mode 100644 index 0000000000..35927708f6 --- /dev/null +++ b/src/app/core/auth/token-response-parsing.service.spec.ts @@ -0,0 +1,45 @@ +import { TokenResponseParsingService } from './token-response-parsing.service'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { TokenResponse } from '../cache/response.models'; + +describe('TokenResponseParsingService', () => { + let service: TokenResponseParsingService; + + beforeEach(() => { + service = new TokenResponseParsingService(); + }); + + describe('parse', () => { + it('should return a TokenResponse containing the token', () => { + const data = { + payload: { + token: 'valid-token' + }, + statusCode: 200, + statusText: 'OK' + } as DSpaceRESTV2Response; + const expected = new TokenResponse(data.payload.token, true, 200, 'OK'); + expect(service.parse(undefined, data)).toEqual(expected); + }); + + it('should return an empty TokenResponse when payload doesn\'t contain a token', () => { + const data = { + payload: {}, + statusCode: 200, + statusText: 'OK' + } as DSpaceRESTV2Response; + const expected = new TokenResponse(null, false, 200, 'OK'); + expect(service.parse(undefined, data)).toEqual(expected); + }); + + it('should return an error TokenResponse when the response failed', () => { + const data = { + payload: {}, + statusCode: 400, + statusText: 'BAD REQUEST' + } as DSpaceRESTV2Response; + const expected = new TokenResponse(null, false, 400, 'BAD REQUEST'); + expect(service.parse(undefined, data)).toEqual(expected); + }); + }); +}); diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts new file mode 100644 index 0000000000..a1b1e23aa4 --- /dev/null +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -0,0 +1,23 @@ +import { ResponseParsingService } from '../data/parsing.service'; +import { RestRequest } from '../data/request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { RestResponse, TokenResponse } from '../cache/response.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { Injectable } from '@angular/core'; + +@Injectable() +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a token string + * wrapped in a TokenResponse + */ +export class TokenResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload.token) && (data.statusCode === 200)) { + return new TokenResponse(data.payload.token, true, data.statusCode, data.statusText); + } else { + return new TokenResponse(null, false, data.statusCode, data.statusText) + } + } + +} diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index a06abdc816..6b640a9db0 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -1,21 +1,38 @@ import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; +import { URLCombiner } from '../url-combiner/url-combiner'; describe('I18nBreadcrumbResolver', () => { describe('resolve', () => { let resolver: I18nBreadcrumbResolver; let i18nBreadcrumbService: any; let i18nKey: string; - let path: string; + let route: any; + let parentSegment; + let segment; + let expectedPath; beforeEach(() => { i18nKey = 'example.key'; - path = 'rest.com/path/to/breadcrumb'; + parentSegment = 'path'; + segment = 'breadcrumb'; + route = { + data: { breadcrumbKey: i18nKey }, + routeConfig: { + path: segment + }, + parent: { + routeConfig: { + path: parentSegment + } + } as any + }; + expectedPath = new URLCombiner(parentSegment, segment).toString(); i18nBreadcrumbService = {}; resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path], pathFromRoot: [{ url: [path] }] } as any, {} as any); - const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path }; + const resolvedConfig = resolver.resolve(route, {} as any); + const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index a6298628c7..cce36f590a 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; import { hasNoValue } from '../../shared/empty.util'; +import { currentPathFromSnapshot } from '../../shared/utils/route.utils'; /** * The class that resolves a BreadcrumbConfig object with an i18n key string for a route @@ -25,17 +26,7 @@ export class I18nBreadcrumbResolver implements Resolve> if (hasNoValue(key)) { throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data') } - const fullPath = this.getResolvedUrl(route); + const fullPath = currentPathFromSnapshot(route); return { provider: this.breadcrumbService, key: key, url: fullPath }; } - - /** - * Resolve the full URL of an ActivatedRouteSnapshot - * @param route - */ - getResolvedUrl(route: ActivatedRouteSnapshot): string { - return route.pathFromRoot - .map((v) => v.url.map((segment) => segment.toString()).join('/')) - .join('/'); - } } diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index dc7af65c8d..5f19185d1c 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -167,6 +167,20 @@ export class AuthStatusResponse extends RestResponse { } } +/** + * A REST Response containing a token + */ +export class TokenResponse extends RestResponse { + constructor( + public token: string, + public isSuccessful: boolean, + public statusCode: number, + public statusText: string + ) { + super(isSuccessful, statusCode, statusText); + } +} + export class IntegrationSuccessResponse extends RestResponse { constructor( public dataDefinition: PaginatedList, diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index 9f4242cc9a..cd14a527e8 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; import { RouteEffects } from './services/route.effects'; import { RouterEffects } from './router/router.effects'; +import { MenuEffects } from '../shared/menu/menu.effects'; export const coreEffects = [ RequestEffects, @@ -17,5 +18,6 @@ export const coreEffects = [ ServerSyncBufferEffects, ObjectUpdatesEffects, RouteEffects, - RouterEffects + RouterEffects, + MenuEffects ]; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 300d33fda8..07ff1f9572 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; - import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { EffectsModule } from '@ngrx/effects'; @@ -138,11 +137,21 @@ import { VersionDataService } from './data/version-data.service'; import { VersionHistoryDataService } from './data/version-history-data.service'; import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; +import { Script } from '../process-page/scripts/script.model'; +import { Process } from '../process-page/processes/process.model'; +import { ProcessDataService } from './data/processes/process-data.service'; +import { ScriptDataService } from './data/processes/script-data.service'; +import { ProcessFilesResponseParsingService } from './data/process-files-response-parsing.service'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { Registration } from './shared/registration.model'; import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; import { MetadataFieldDataService } from './data/metadata-field-data.service'; +import { TokenResponseParsingService } from './auth/token-response-parsing.service'; +import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; +import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; +import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; +import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -209,6 +218,8 @@ const PROVIDERS = [ BrowseItemsResponseParsingService, BrowseService, ConfigResponseParsingService, + SubmissionCcLicenseDataService, + SubmissionCcLicenseUrlDataService, SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionRestService, @@ -257,8 +268,12 @@ const PROVIDERS = [ LicenseDataService, ItemTypeDataService, WorkflowActionDataService, + ProcessDataService, + ScriptDataService, + ProcessFilesResponseParsingService, MetadataSchemaDataService, MetadataFieldDataService, + TokenResponseParsingService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -291,6 +306,8 @@ export const models = License, WorkflowItem, WorkspaceItem, + SubmissionCcLicence, + SubmissionCcLicenceUrl, SubmissionDefinitionsModel, SubmissionFormsModel, SubmissionSectionModel, @@ -307,6 +324,8 @@ export const models = ItemType, ExternalSource, ExternalSourceEntry, + Script, + Process, Version, VersionHistory, WorkflowAction, diff --git a/src/app/core/data/process-files-response-parsing.service.ts b/src/app/core/data/process-files-response-parsing.service.ts new file mode 100644 index 0000000000..0fa7c66869 --- /dev/null +++ b/src/app/core/data/process-files-response-parsing.service.ts @@ -0,0 +1,41 @@ +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { Injectable } from '@angular/core'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { Bitstream } from '../shared/bitstream.model'; + +@Injectable() +/** + * A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse + * containing a PaginatedList of a process's output files + */ +export class ProcessFilesResponseParsingService implements ResponseParsingService { + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const payload = data.payload; + + let page; + if (isNotEmpty(payload._embedded) && isNotEmpty(Object.keys(payload._embedded))) { + const bitstreams = new DSpaceSerializer(Bitstream).deserializeArray(payload._embedded[Object.keys(payload._embedded)[0]]); + + if (isNotEmpty(bitstreams)) { + page = new PaginatedList(Object.assign(new PageInfo(), { + elementsPerPage: bitstreams.length, + totalElements: bitstreams.length, + totalPages: 1, + currentPage: 1 + }), bitstreams); + } + } + + if (isEmpty(page)) { + page = new PaginatedList(new PageInfo(), []); + } + + return new GenericSuccessResponse(page, data.statusCode, data.statusText); + } +} diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts new file mode 100644 index 0000000000..48c1d502cc --- /dev/null +++ b/src/app/core/data/processes/process-data.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@angular/core'; +import { DataService } from '../data.service'; +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 { Process } from '../../../process-page/processes/process.model'; +import { dataService } from '../../cache/builders/build-decorators'; +import { PROCESS } from '../../../process-page/processes/process.resource-type'; +import { Observable } from 'rxjs/internal/Observable'; +import { map, switchMap } from 'rxjs/operators'; +import { ProcessFilesRequest, RestRequest } from '../request.models'; +import { configureRequest, filterSuccessfulResponses } from '../../shared/operators'; +import { GenericSuccessResponse } from '../../cache/response.models'; +import { PaginatedList } from '../paginated-list'; +import { Bitstream } from '../../shared/bitstream.model'; +import { RemoteData } from '../remote-data'; + +@Injectable() +@dataService(PROCESS) +export class ProcessDataService extends DataService { + protected linkPath = 'processes'; + + 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(); + } + + /** + * Get the endpoint for a process his files + * @param processId The ID of the process + */ + getFilesEndpoint(processId: string): Observable { + return this.getBrowseEndpoint().pipe( + switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`)) + ); + } + + /** + * Get a process his output files + * @param processId The ID of the process + */ + getFiles(processId: string): Observable>> { + const request$ = this.getFilesEndpoint(processId).pipe( + map((href) => new ProcessFilesRequest(this.requestService.generateRequestId(), href)), + configureRequest(this.requestService) + ); + const requestEntry$ = request$.pipe( + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + const payload$ = requestEntry$.pipe( + filterSuccessfulResponses(), + map((response: GenericSuccessResponse>) => response.payload) + ); + + return this.rdbService.toRemoteDataObservable(requestEntry$, payload$); + } +} diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts new file mode 100644 index 0000000000..6600444ea0 --- /dev/null +++ b/src/app/core/data/processes/script-data.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { DataService } from '../data.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 { Script } from '../../../process-page/scripts/script.model'; +import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; +import { find, map, switchMap } from 'rxjs/operators'; +import { URLCombiner } from '../../url-combiner/url-combiner'; +import { MultipartPostRequest, RestRequest } from '../request.models'; +import { RequestService } from '../request.service'; +import { Observable } from 'rxjs'; +import { RequestEntry } from '../request.reducer'; +import { dataService } from '../../cache/builders/build-decorators'; +import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; + +@Injectable() +@dataService(SCRIPT) +export class ScriptDataService extends DataService