Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:Enabled state transitions without modification of models #209

Merged
merged 6 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Spring Boot version updated to 3.1.6 to fix CVE-2023-34053
- update Logback version to fix CVE-2023-6378
- Enabled state transitions without modification of models.

## 0.2.15
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ public ResponseEntity<SemanticModel> modifyModel( final SemanticModelType type,
return new ResponseEntity<>( resultingModel, HttpStatus.OK );
}

@Override
public ResponseEntity<SemanticModel> updateModel( String urn,
SemanticModelStatus status ) {
SemanticModel semanticModel = persistenceLayer.updateModel( urn , status );
return new ResponseEntity<>( semanticModel, HttpStatus.OK );

}

@Override
public ResponseEntity<Void> getModelOpenApi( final String modelId, final String baseUrl ) {
final String openApiJson = sdkHelper.getOpenApiDefinitionJson( modelId, baseUrl );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,6 @@ public interface PersistenceLayer {
boolean echo();

public SemanticModelList findModelListByUrns(List<AspectModelUrn> urns, int page, int pageSize);

SemanticModel updateModel(String urn, SemanticModelStatus status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,64 +104,94 @@ public SemanticModel getModel( final AspectModelUrn urn ) {
return findByUrn( urn );
}

@Override
public SemanticModel updateModel( String urn, SemanticModelStatus status ) {

SemanticModel semanticModel = Optional.ofNullable( findByUrn(
AspectModelUrn.fromUrn( urn ) ) ).orElseThrow( () -> new IllegalArgumentException(
String.format( "Invalid URN %s",
ModelPackageUrn.fromUrn( urn ).getUrn() ) ) );
ModelPackageStatus persistedModelStatus = ModelPackageStatus.valueOf(
semanticModel.getStatus().name() );

final Model model = findContainingModelByUrn( urn );

final AspectModelUrn modelUrn = sdsSdk.getAspectUrn( model );

validateStatus( status, model, modelUrn, persistedModelStatus );

updateModel( status, modelUrn, model );

return findByUrn( modelUrn );
}

@Override
public SemanticModel save( SemanticModelType type, String newModel, SemanticModelStatus status ) {
final Model rdfModel = sdsSdk.load( newModel.getBytes( StandardCharsets.UTF_8 ) );
final AspectModelUrn modelUrn = sdsSdk.getAspectUrn( rdfModel );
Optional<ModelPackage> existsByPackage = findByPackageByUrn( ModelPackageUrn.fromUrn( modelUrn ) );

if ( existsByPackage.isPresent() ) {
ModelPackageStatus persistedModelStatus = existsByPackage.get().getStatus();
final ModelPackageStatus desiredModelStatus = ModelPackageStatus.valueOf( status.name() );
switch ( persistedModelStatus ) {
case DRAFT:
if ( desiredModelStatus.equals( ModelPackageStatus.RELEASED ) && !hasReferenceToDraftPackage( modelUrn, rdfModel ) ) {
throw new InvalidStateTransitionException( "It is not allowed to release an aspect that has dependencies in DRAFT state." );
} else if ( desiredModelStatus.equals( ModelPackageStatus.STANDARDIZED ) ) {
throw new IllegalArgumentException(
String.format( "The package %s is in status %s. Only a transition to RELEASED or DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
break;
case RELEASED:
// released models can only be updated when the new state is deprecated or standardized
if ( desiredModelStatus.equals( ModelPackageStatus.DEPRECATED ) || desiredModelStatus.equals( ModelPackageStatus.STANDARDIZED ) ) {
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
} else {
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified. Only a transition to STANDARDIZED or DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
break;
case STANDARDIZED:
if ( desiredModelStatus.equals( ModelPackageStatus.DEPRECATED ) ) {
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
} else {
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified. Only a transition to DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
break;
case DEPRECATED:
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
validateStatus( status, rdfModel, modelUrn, existsByPackage.get().getStatus() );
}

sdsSdk.validate( rdfModel, this::findContainingModelByUrn, type );

Model rdfModelOriginal = sdsSdk.load( newModel.getBytes( StandardCharsets.UTF_8 ) );

updateModel( status, modelUrn, rdfModelOriginal );

return findByUrn( modelUrn );
}

private void updateModel( SemanticModelStatus status, AspectModelUrn modelUrn, Model rdfModelOriginal ) {
final Resource rootResource = ResourceFactory.createResource( modelUrn.getUrnPrefix() );
rdfModelOriginal.add( rootResource, SparqlQueries.STATUS_PROPERTY,
ModelPackageStatus.valueOf( status.name() ).toString() );

try ( final RDFConnection rdfConnection = rdfConnectionRemoteBuilder.build() ) {
rdfConnection.update( new UpdateBuilder().addInsert( rdfModelOriginal ).build() );
}
return findByUrn( modelUrn );
}


private void validateStatus( SemanticModelStatus status, Model rdfModel, AspectModelUrn modelUrn, ModelPackageStatus persistedModelStatus ) {
final ModelPackageStatus desiredModelStatus = ModelPackageStatus.valueOf( status.name() );
switch ( persistedModelStatus ) {
case DRAFT:
if ( desiredModelStatus.equals( ModelPackageStatus.RELEASED ) && !hasReferenceToDraftPackage( modelUrn, rdfModel ) ) {
throw new InvalidStateTransitionException( "It is not allowed to release an aspect that has dependencies in DRAFT state." );
} else if ( desiredModelStatus.equals( ModelPackageStatus.STANDARDIZED ) ) {
throw new IllegalArgumentException(
String.format( "The package %s is in status %s. Only a transition to RELEASED or DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
break;
case RELEASED:
// released models can only be updated when the new state is deprecated or standardized
if ( desiredModelStatus.equals( ModelPackageStatus.DEPRECATED ) || desiredModelStatus.equals( ModelPackageStatus.STANDARDIZED ) ) {
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
} else {
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified. Only a transition to STANDARDIZED or DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
break;
case STANDARDIZED:
if ( desiredModelStatus.equals( ModelPackageStatus.DEPRECATED ) ) {
deleteByUrn( ModelPackageUrn.fromUrn( modelUrn ) );
} else {
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified. Only a transition to DEPRECATED is possible.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
break;
case DEPRECATED:
throw new IllegalArgumentException(
String.format( "The package %s is already in status %s and cannot be modified.",
ModelPackageUrn.fromUrn( modelUrn ).getUrn(), persistedModelStatus.name() ) );
}
}

@Override
Expand Down
26 changes: 26 additions & 0 deletions backend/src/main/resources/static/semantic-hub-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,32 @@ paths:
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
put:
tags:
- SemanticHub
summary: Updates an existing Semantic Model status by the URN
operationId: updateModel
parameters:
- in: path
name: urn
schema:
type: string
required: true
- in: query
name: status
schema:
$ref: '#/components/schemas/SemanticModelStatus'
responses:
'200':
$ref: '#/components/responses/SemanticModel'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'
delete:
tags:
- SemanticHub
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ public MockHttpServletRequestBuilder post( String payload, String status ) {
.with(jwtTokenFactory.allRoles());
}

public MockHttpServletRequestBuilder update( String urn, String status ) {
return MockMvcRequestBuilders.put( "/api/v1/models/{urn}",urn)
.queryParam( "status", status)
.accept( MediaType.APPLICATION_JSON )
.with(jwtTokenFactory.allRoles());
}

public MockHttpServletRequestBuilder put( String payload, String status ) {
String type = "SAMM";
return MockMvcRequestBuilders.put( "/api/v1/models")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,59 @@ public void testDependentModelBAMM() throws Exception {
.andExpect( status().isOk() );
}

@Test
@DisplayName( "test model status update by URN" )
public void testModelStatusUpdateByURN() throws Exception {
String urnPrefix = "urn:bamm:io.catenax.shared.contact_information:2.0.0#ContactInformation";

//Given
mvc.perform( postBAMM( TestUtils.getTTLFile( "ContactInformation-2.0.0.ttl" ), "DRAFT" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() );

//When & Then
mvc.perform( update( urnPrefix, "DRAFT" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() )
.andExpect( jsonPath( "$.status", is( "DRAFT" ) ) );

mvc.perform( update( urnPrefix, "STANDARDIZED" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isBadRequest() );

mvc.perform( update( urnPrefix, "RELEASED" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() )
.andExpect( jsonPath( "$.status", is( "RELEASED" ) ) );

// transition from released to draft is not allowed
mvc.perform( update( urnPrefix, "DRAFT" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isBadRequest() )
.andExpect( jsonPath( "$.error.message", containsString(
"already in status RELEASED and cannot be modified. Only a transition to STANDARDIZED or DEPRECATED is possible." ) ) );

// transition from released to standardized is allowed
mvc.perform( update( urnPrefix, "STANDARDIZED" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() )
.andExpect( jsonPath( "$.status", is( "STANDARDIZED" ) ) );

// transition from standardized to draft is not allowed
mvc.perform( update( urnPrefix, "DRAFT" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( jsonPath( "$.error.message", is(
"The package urn:bamm:io.catenax.shared.contact_information:2.0.0# is already in status STANDARDIZED and cannot be modified. Only a transition to DEPRECATED is possible." ) ) )
.andExpect( status().isBadRequest() );

// transition from standardized to deprecated is allowed
mvc.perform( update( urnPrefix, "DEPRECATED" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() )
.andExpect( jsonPath( "$.status", is( "DEPRECATED" ) ) );

}

@Test
public void testInvalidDependentModelBAMMModel() throws Exception {

Expand Down Expand Up @@ -1006,4 +1059,15 @@ void testGeneratePngForSAMMModel() throws Exception {
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isOk() );
}

@Test
public void testUpdateModelStatusByInvalidURN() throws Exception {
String urnPrefix = "urn:bamm:io.catenax.shared.invalid:2.0.0#InvalidInformation";

mvc.perform( update( urnPrefix, "DRAFT" ) )
.andDo( MockMvcResultHandlers.print() )
.andExpect( status().isBadRequest() )
.andExpect( jsonPath( "$.error.message", containsString( "Invalid URN urn" ) ) );
}

}
Loading