diff --git a/AMW_angular/io/src/app/resource/resource-type.ts b/AMW_angular/io/src/app/resource/resource-type.ts index daeaa8d0f..901cd415a 100644 --- a/AMW_angular/io/src/app/resource/resource-type.ts +++ b/AMW_angular/io/src/app/resource/resource-type.ts @@ -3,4 +3,5 @@ export interface ResourceType { name: string; hasChildren: boolean; children: ResourceType[]; + isApplication: boolean; } diff --git a/AMW_angular/io/src/app/resource/resource.service.ts b/AMW_angular/io/src/app/resource/resource.service.ts index fef88cef6..b4c3fcf2c 100644 --- a/AMW_angular/io/src/app/resource/resource.service.ts +++ b/AMW_angular/io/src/app/resource/resource.service.ts @@ -19,13 +19,13 @@ interface Named { export class ResourceService extends BaseService { private resourceType$: Subject = new Subject(); - private resourceGroupListForType: Observable = this.resourceType$.pipe( + private resourceGroupListForType$: Observable = this.resourceType$.pipe( switchMap((resourceType: ResourceType) => this.getGroupsForType(resourceType)), startWith(null), shareReplay(1), ); - resourceGroupListForTypeSignal = toSignal(this.resourceGroupListForType, { initialValue: [] as Resource[] }); + resourceGroupListForType = toSignal(this.resourceGroupListForType$, { initialValue: [] as Resource[] }); constructor(private http: HttpClient) { super(); @@ -46,6 +46,14 @@ export class ResourceService extends BaseService { ); } + createResourceForResourceType(resource: any) { + return this.http + .post(`${this.getBaseUrl()}/resources`, resource, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + getResourceName(resourceId: number): Observable { return this.http .get(`${this.getBaseUrl()}/resources/name/${resourceId}`, { @@ -64,6 +72,7 @@ export class ResourceService extends BaseService { catchError(this.handleError), ); } + get(resourceGroupName: string): Observable { return this.http .get(`${this.getBaseUrl()}/resources/${resourceGroupName}`, { diff --git a/AMW_angular/io/src/app/resources/resource-add/resource-add.component.html b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.html new file mode 100644 index 000000000..c35f8bb76 --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.html @@ -0,0 +1,32 @@ +
+ + + +
diff --git a/AMW_angular/io/src/app/resources/resource-add/resource-add.component.spec.ts b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.spec.ts new file mode 100644 index 000000000..87c117598 --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ResourceAddComponent } from './resource-add.component'; + +describe('ResourceAddComponent', () => { + let component: ResourceAddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ResourceAddComponent], + providers: [NgbActiveModal], + }).compileComponents(); + + fixture = TestBed.createComponent(ResourceAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/resources/resource-add/resource-add.component.ts b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.ts new file mode 100644 index 000000000..652e5e318 --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-add/resource-add.component.ts @@ -0,0 +1,57 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, inject, Input, Output } from '@angular/core'; +import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; +import { FormsModule } from '@angular/forms'; +import { ModalHeaderComponent } from '../../shared/modal-header/modal-header.component'; +import { ButtonComponent } from '../../shared/button/button.component'; +import { ResourceType } from '../../resource/resource-type'; +import { Release } from '../../settings/releases/release'; +import { NgSelectModule } from '@ng-select/ng-select'; + +@Component({ + selector: 'app-resource-add', + standalone: true, + imports: [FormsModule, ModalHeaderComponent, ButtonComponent, NgbTypeahead, NgSelectModule], + templateUrl: './resource-add.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ResourceAddComponent { + activeModal = inject(NgbActiveModal); + @Input() resourceType: ResourceType; + @Input() releases: Release[]; + @Input() selectedReleaseName: Release; + resourceName: string; + @Output() saveResource: EventEmitter = new EventEmitter(); + + getTitle() { + if (!this.resourceType) return; + return this.resourceType.name ? `Create new instance for ${this.resourceType.name}` : `Create new instance`; + } + + save() { + let forms: NodeListOf = document.querySelectorAll('.needs-validation'); + if (this.isValidForm()) { + const resourceToAdd = { + name: this.resourceName, + type: this.resourceType.name, + releaseName: this.selectedReleaseName, + }; + this.saveResource.emit(resourceToAdd); + this.activeModal.close(); + } else { + forms[0].classList.add('was-validated'); + } + } + + cancel() { + this.activeModal.close(); + } + + setSelectedRelease($event: any) { + this.selectedReleaseName = $event; + } + + isValidForm() { + const REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN = /^[a-zA-Z0-9_-]+$/; + return this.resourceName ? REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN.test(this.resourceName) : false; + } +} diff --git a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.html b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.html index 261b822fb..338e5963b 100644 --- a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.html +++ b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.html @@ -1,6 +1,15 @@
- @if (resourceType()){ -

{{resourceType().name}}

+ @if (resourceType() && permissions().canReadResources) { +

{{ resourceType().name }}

+ @if (permissions().canCreateResource && !resourceType().isApplication) { +
+ + + New Resource + +
+ } diff --git a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.spec.ts b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.spec.ts index f464cb38f..cc00cc317 100644 --- a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.spec.ts +++ b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.spec.ts @@ -3,6 +3,8 @@ import { ResourcesListComponent } from './resources-list.component'; import { ComponentRef } from '@angular/core'; import { Resource } from '../../resource/resource'; import { ResourceType } from '../../resource/resource-type'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; describe('ResourcesListComponent', () => { let component: ResourcesListComponent; @@ -13,6 +15,7 @@ describe('ResourcesListComponent', () => { name: 'type', hasChildren: false, children: [], + isApplication: false, }; const resourceGroupsOfResourceType: Resource[] = []; @@ -20,7 +23,7 @@ describe('ResourcesListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ResourcesListComponent], - providers: [], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()], }).compileComponents(); fixture = TestBed.createComponent(ResourcesListComponent); diff --git a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.ts b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.ts index 6ec143c54..9955ada89 100644 --- a/AMW_angular/io/src/app/resources/resources-list/resources-list.component.ts +++ b/AMW_angular/io/src/app/resources/resources-list/resources-list.component.ts @@ -1,8 +1,14 @@ -import { Component, input } from '@angular/core'; +import { Component, computed, inject, input, output } from '@angular/core'; import { Resource } from '../../resource/resource'; import { ResourceType } from '../../resource/resource-type'; import { ButtonComponent } from '../../shared/button/button.component'; import { IconComponent } from '../../shared/icon/icon.component'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { ResourceAddComponent } from '../resource-add/resource-add.component'; +import { Release } from '../../settings/releases/release'; +import { AuthService } from '../../auth/auth.service'; @Component({ selector: 'app-resources-list', @@ -11,6 +17,35 @@ import { IconComponent } from '../../shared/icon/icon.component'; imports: [ButtonComponent, IconComponent], }) export class ResourcesListComponent { + private modalService = inject(NgbModal); + private authService = inject(AuthService); + private destroy$ = new Subject(); resourceType = input.required(); - resourceGroupList = input(); + resourceGroupList = input.required(); + releases = input.required(); + resourceToAdd = output(); + + permissions = computed(() => { + if (this.authService.restrictions().length > 0) { + return { + canReadResources: this.authService.hasPermission('RESOURCE', 'READ'), + canCreateResource: this.authService.hasPermission('RESOURCE', 'CREATE'), + }; + } else { + return { + canReadResources: false, + canCreateResource: false, + }; + } + }); + + addResource() { + const modalRef: NgbModalRef = this.modalService.open(ResourceAddComponent); + modalRef.componentInstance.resourceType = this.resourceType(); + modalRef.componentInstance.releases = this.releases(); + modalRef.componentInstance.selectedReleaseName = this.releases()[0].name; + modalRef.componentInstance.saveResource + .pipe(takeUntil(this.destroy$)) + .subscribe((resource: any) => this.resourceToAdd.emit(resource)); + } } diff --git a/AMW_angular/io/src/app/resources/resources-page.component.html b/AMW_angular/io/src/app/resources/resources-page.component.html index 041e30427..edd770895 100644 --- a/AMW_angular/io/src/app/resources/resources-page.component.html +++ b/AMW_angular/io/src/app/resources/resources-page.component.html @@ -32,7 +32,10 @@ }
- +
diff --git a/AMW_angular/io/src/app/resources/resources-page.component.ts b/AMW_angular/io/src/app/resources/resources-page.component.ts index cb8c6d3c8..9b0498a8f 100644 --- a/AMW_angular/io/src/app/resources/resources-page.component.ts +++ b/AMW_angular/io/src/app/resources/resources-page.component.ts @@ -7,6 +7,11 @@ import { ResourceType } from '../resource/resource-type'; import { ResourcesListComponent } from './resources-list/resources-list.component'; import { ResourceService } from '../resource/resource.service'; import { Resource } from '../resource/resource'; +import { ReleasesService } from '../settings/releases/releases.service'; +import { Release } from '../settings/releases/release'; +import { ToastService } from '../shared/elements/toast/toast.service'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-resources-page', @@ -19,10 +24,15 @@ export class ResourcesPageComponent { private authService = inject(AuthService); private resourceTypesService = inject(ResourceTypesService); private resourceService = inject(ResourceService); + private releaseService = inject(ReleasesService); + private toastService = inject(ToastService); + private error$ = new BehaviorSubject(''); + private destroy$ = new Subject(); predefinedResourceTypes: Signal = this.resourceTypesService.predefinedResourceTypes; rootResourceTypes: Signal = this.resourceTypesService.rootResourceTypes; - resourceGroupListForTypeSignal: Signal = this.resourceService.resourceGroupListForTypeSignal; + resourceGroupListForType: Signal = this.resourceService.resourceGroupListForType; + releases: Signal = this.releaseService.allReleases; isLoading = signal(false); expandedResourceTypeId: number | null = null; selectedResourceType: WritableSignal = signal(null); @@ -51,4 +61,15 @@ export class ResourcesPageComponent { this.expandedResourceTypeId = this.expandedResourceTypeId === resourceType.id ? null : resourceType.id; this.selectedResourceType.set(resourceType); } + + addResource(resource: any) { + this.resourceService + .createResourceForResourceType(resource) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: () => this.toastService.success('Resource saved successfully.'), + error: (e) => this.error$.next(e), + complete: () => this.resourceService.setTypeForResourceGroupList(this.selectedResourceTypeOrDefault()), // refresh data of the selected resource type + }); + } } diff --git a/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts b/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts index e036bd33a..f7de09d59 100644 --- a/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/permission.component.spec.ts @@ -84,7 +84,9 @@ describe('PermissionComponent without any params (default: type Role)', () => { } as Environment, ]); expect(component.resourceGroups).toEqual([]); - expect(component.resourceTypes).toEqual([{ id: null, name: null, hasChildren: false, children: [] }]); + expect(component.resourceTypes).toEqual([ + { id: null, name: null, hasChildren: false, children: [], isApplication: false }, + ]); expect(component.restrictionType).toEqual('role'); }); diff --git a/AMW_angular/io/src/app/settings/permission/permission.component.ts b/AMW_angular/io/src/app/settings/permission/permission.component.ts index c598af52f..acc7e1874 100644 --- a/AMW_angular/io/src/app/settings/permission/permission.component.ts +++ b/AMW_angular/io/src/app/settings/permission/permission.component.ts @@ -63,7 +63,7 @@ export class PermissionComponent implements OnInit { Global: [], }; resourceGroups: Resource[] = []; - resourceTypes: ResourceType[] = [{ id: null, name: null, hasChildren: false, children: [] }]; + resourceTypes: ResourceType[] = [{ id: null, name: null, hasChildren: false, children: [], isApplication: false }]; defaultNavItem: string = 'Roles'; // role | user diff --git a/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts b/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts index 78ad6fe84..1bb5ee0c0 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-add.component.spec.ts @@ -223,18 +223,21 @@ describe('RestrictionAddComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; @@ -282,18 +285,21 @@ describe('RestrictionAddComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; @@ -341,18 +347,21 @@ describe('RestrictionAddComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.selectedPermissionNames = ['NEO']; diff --git a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts index cb84f79f2..8eac5d200 100644 --- a/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts +++ b/AMW_angular/io/src/app/settings/permission/restriction-edit.component.spec.ts @@ -115,12 +115,14 @@ describe('RestrictionEditComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'APPSERVER', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.restriction = { @@ -141,12 +143,14 @@ describe('RestrictionEditComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'APPSERVER', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.restriction = { @@ -453,18 +457,21 @@ describe('RestrictionEditComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.restriction = { @@ -512,18 +519,21 @@ describe('RestrictionEditComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.restriction = { @@ -570,18 +580,21 @@ describe('RestrictionEditComponent', () => { name: 'APP', hasChildren: false, children: [], + isApplication: false, }, { id: 2, name: 'AS', hasChildren: false, children: [], + isApplication: false, }, { id: 3, name: 'FOO', hasChildren: false, children: [], + isApplication: false, }, ]; restrictionComponent.restriction = { diff --git a/AMW_angular/io/src/app/settings/releases/releases.service.ts b/AMW_angular/io/src/app/settings/releases/releases.service.ts index 2f345ff39..6b5f2301d 100644 --- a/AMW_angular/io/src/app/settings/releases/releases.service.ts +++ b/AMW_angular/io/src/app/settings/releases/releases.service.ts @@ -5,9 +5,13 @@ import { map, catchError } from 'rxjs/operators'; import { Release } from './release'; import { Observable } from 'rxjs'; import { ResourceEntity } from './resource-entity'; +import { toSignal } from '@angular/core/rxjs-interop'; @Injectable({ providedIn: 'root' }) export class ReleasesService extends BaseService { + private allReleases$ = this.getAllReleases(); + allReleases = toSignal(this.allReleases$, { initialValue: [] as Release[] }); + constructor(private http: HttpClient) { super(); } @@ -30,6 +34,10 @@ export class ReleasesService extends BaseService { .pipe(catchError(this.handleError)); } + getAllReleases(): Observable { + return this.http.get(`${this.getBaseUrl()}/releases`).pipe(catchError(this.handleError)); + } + getReleaseResources(id: number): Observable> { return this.http.get(`${this.getBaseUrl()}/releases/${id}/resources`).pipe( map((jsonObject) => { diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceBoundary.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceBoundary.java index 4510417d2..bc564f649 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceBoundary.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceBoundary.java @@ -95,7 +95,7 @@ public Resource createNewResourceByName(ForeignableOwner creatingOwner, String n if (resourceTypeEntity == null) { String message = "ResourceType '" + resourceTypeName + "' doesn't exist"; log.info(message); - throw new ResourceNotFoundException(message); + throw new ResourceTypeNotFoundException(message); } ReleaseEntity release; try { diff --git a/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/util/NameChecker.java b/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/util/NameChecker.java index 1fedd2f50..b1b29f295 100644 --- a/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/util/NameChecker.java +++ b/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/util/NameChecker.java @@ -23,10 +23,11 @@ public class NameChecker { - private static final String ERR_MSG_SUFIX = "The name must not contain any other than alphanumerical and \"_\" caracters."; + private static final String ERR_MSG_SUFIX = "The name must not contain any other than alphanumerical and \"_\" characters."; + private static final String ERR_MSG_INVALID_NAME_CHARACTERS = "The name must not contain any other than alphanumerical and \"_\" / \"-\" characters."; - private static final String VALID_CHARACTER_REGEXP = "\\S*"; - + private static final String REGEXP_NON_EMPTY_STRING = "\\S*"; + private static final String REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN = "^[a-zA-Z0-9_-]+$"; /** * Ein Resourcename @parm name wird auf seine Gültigkeit geprüft. Der name @@ -41,12 +42,23 @@ public static boolean isNameValid(String name) { boolean isValid = true; if (name != null) { - isValid = name.matches(VALID_CHARACTER_REGEXP); + isValid = name.matches(REGEXP_NON_EMPTY_STRING); } return isValid; } - + + /** + * Ein Resourcename @parm name wird auf seine Gültigkeit geprüft. + * Der Name darf keine anderen als alphanumerische Zeichen und "_" und "-" enthalten. + * Null ist gültig! + * + * @param name + * @return + */ + public static boolean isValidAlphanumericWithUnderscoreHyphenName(String name) { + return name == null || name.matches(REGEXP_ALPHANUMERIC_WITH_UNDERSCORE_HYPHEN); + } /** * Gibt Fehlermeldung abhängig vom resourcetypen der gegebenen resource @@ -63,9 +75,17 @@ public static String getErrorText(String type, String name) { return "Invalid " + type + " name \"" + name + "\"! " + ERR_MSG_SUFIX; } + public static String getErrorTextAlphanumericWithUnderscoreHyphenName(String type, String name) { + return "Invalid " + type + " name \"" + name + "\"! " + ERR_MSG_INVALID_NAME_CHARACTERS; + } + public static String getErrorTextForResourceType(String resourceTypeName, String name) { return getErrorText(getResourceTypeScreenName(resourceTypeName), name); } + + public static String getErrorTextForInvalidResourceName(String resourceTypeName, String name) { + return getErrorTextAlphanumericWithUnderscoreHyphenName(getResourceTypeScreenName(resourceTypeName), name); + } public static String getErrorTextForResourceType(String resourceTypeName) { return getErrorText(getResourceTypeScreenName(resourceTypeName)); diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/dtos/ResourceTypeDTO.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/dtos/ResourceTypeDTO.java index 6fe317df4..d2d2a20bc 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/dtos/ResourceTypeDTO.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/dtos/ResourceTypeDTO.java @@ -40,6 +40,7 @@ public class ResourceTypeDTO { private String name; private boolean hasChildren; private List children; + private boolean resourceTypeIsApplication; public ResourceTypeDTO(ResourceTypeEntity resourceType){ this.id = resourceType.getId(); @@ -48,5 +49,6 @@ public ResourceTypeDTO(ResourceTypeEntity resourceType){ this.children = resourceType.getChildrenResourceTypes().stream() .map(ResourceTypeDTO::new) .collect(Collectors.toList()); + this.resourceTypeIsApplication = resourceType.isApplicationResourceType(); } } diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java index 3601dbff9..ed9676c8e 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java @@ -33,18 +33,15 @@ import ch.puzzle.itc.mobiliar.business.releasing.entity.ReleaseEntity; import ch.puzzle.itc.mobiliar.business.resourcegroup.boundary.*; import ch.puzzle.itc.mobiliar.business.resourcegroup.control.CopyResourceResult; -import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.Resource; import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceEntity; import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceGroupEntity; import ch.puzzle.itc.mobiliar.business.resourcerelation.control.ResourceRelationService; import ch.puzzle.itc.mobiliar.business.resourcerelation.entity.ConsumedResourceRelationEntity; import ch.puzzle.itc.mobiliar.business.resourcerelation.entity.ProvidedResourceRelationEntity; import ch.puzzle.itc.mobiliar.business.security.boundary.PermissionBoundary; -import ch.puzzle.itc.mobiliar.common.exception.AMWException; -import ch.puzzle.itc.mobiliar.common.exception.ElementAlreadyExistsException; +import ch.puzzle.itc.mobiliar.common.exception.*; import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; -import ch.puzzle.itc.mobiliar.common.exception.ResourceNotFoundException; -import ch.puzzle.itc.mobiliar.common.exception.ValidationException; +import ch.puzzle.itc.mobiliar.common.util.NameChecker; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -195,21 +192,26 @@ public ResourceDTO getExactOrClosestPastRelease(@PathParam("resourceGroupName") */ @POST @ApiOperation(value = "Add a Resource") - public Response addResource(@ApiParam("Add a Resource") ResourceReleaseDTO request) { - Resource resource; - if (StringUtils.isEmpty(request.getName())) { - return Response.status(BAD_REQUEST).entity(new ExceptionDto("Resource name must not be empty")).build(); - } - if (StringUtils.isEmpty(request.getReleaseName())) { - return Response.status(BAD_REQUEST).entity(new ExceptionDto("Release name must not be empty")).build(); - } + public Response addResource(@ApiParam("Add a Resource") ResourceReleaseDTO request) throws ValidationException, NotFoundException, ElementAlreadyExistsException { + if(StringUtils.isEmpty(request.getName()) || StringUtils.isEmpty(request.getName().trim())) + throw new ValidationException("Resource name must not be null or blank"); + + if(StringUtils.isEmpty(request.getReleaseName()) || StringUtils.isEmpty(request.getReleaseName().trim())) + throw new ValidationException("Release name must not be null or blank"); + + if(!NameChecker.isValidAlphanumericWithUnderscoreHyphenName(request.getName())) + throw new ValidationException(NameChecker.getErrorTextForInvalidResourceName( + (request.getType() != null) ? request.getType() : null, request.getName())); + try { - resource = resourceBoundary.createNewResourceByName(ForeignableOwner.getSystemOwner(), request.getName(), + resourceBoundary.createNewResourceByName(ForeignableOwner.getSystemOwner(), request.getName(), request.getType(), request.getReleaseName()); - } catch (AMWException e) { - return Response.status(BAD_REQUEST).entity(new ExceptionDto(e.getMessage())).build(); + } catch (ResourceTypeNotFoundException e) { + throw new NotFoundException("Resource type: " + request.getType() + " not found"); + } catch (ResourceNotFoundException e) { + throw new NotFoundException("Release : " + request.getReleaseName() + " not found"); } - return Response.status(CREATED).header("Location", "/resources/" + resource.getName()).build(); + return Response.status(Response.Status.OK).build(); } /** diff --git a/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java b/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java index 32fed1771..e114557ab 100644 --- a/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java +++ b/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java @@ -27,8 +27,7 @@ import static javax.ws.rs.core.Response.Status.OK; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -36,7 +35,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import javax.ws.rs.core.Response; @@ -73,8 +71,6 @@ import ch.puzzle.itc.mobiliar.common.exception.AMWException; import ch.puzzle.itc.mobiliar.common.exception.ElementAlreadyExistsException; import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; -import ch.puzzle.itc.mobiliar.common.exception.ResourceNotFoundException; -import ch.puzzle.itc.mobiliar.common.exception.ResourceTypeNotFoundException; import ch.puzzle.itc.mobiliar.common.exception.ValidationException; public class ResourceGroupsRestTest { @@ -205,15 +201,17 @@ public void shouldNotAllowCreationOfNewResourcesWithoutRelease() { resourceReleaseDTO.setName("Test"); // when - Response response = rest.addResource(resourceReleaseDTO); + Throwable exception = assertThrows(ValidationException.class, () -> { + rest.addResource(resourceReleaseDTO); + }); // then - assertEquals(BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals(exception.getMessage(), "Release name must not be null or blank"); } @Test - public void shouldReturnExpectedLocationHeaderOnSuccessfullResourceCreation() throws ResourceTypeNotFoundException, ResourceNotFoundException, ElementAlreadyExistsException { + public void shouldReturnExpectedLocationHeaderOnSuccessfullResourceCreation() throws ElementAlreadyExistsException, ValidationException, NotFoundException { // given ResourceTypeEntity resType = new ResourceTypeEntity(); resType.setName("APP"); @@ -239,8 +237,7 @@ public void shouldReturnExpectedLocationHeaderOnSuccessfullResourceCreation() th Response response = rest.addResource(resourceReleaseDTO); // then - assertEquals(CREATED.getStatusCode(), response.getStatus()); - assertTrue(response.getMetadata().get("Location").contains("/resources/" + resGroup.getName())); + assertEquals(OK.getStatusCode(), response.getStatus()); } @Test