Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package tools.vitruv.methodologist.setup.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import tools.vitruv.methodologist.setup.exception.ErrorResponseDTO;
import tools.vitruv.methodologist.setup.exception.GenmodelException;
import tools.vitruv.methodologist.setup.exception.MethodologistSetupException;
Expand All @@ -19,101 +20,95 @@
*/
@Slf4j
@RestControllerAdvice(basePackages = "tools.vitruv.methodologist.setup")
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private final ObjectMapper objectMapper = new ObjectMapper();

/**
* Handles MethodologistSetupException.
*
* @param ex the exception
* @param request the web request
* @return error response
*/
@ExceptionHandler(MethodologistSetupException.class)
public ResponseEntity<ErrorResponseDTO> handleMethodologistSetupException(
MethodologistSetupException ex, WebRequest request) {
public void handleMethodologistSetupException(
MethodologistSetupException ex, HttpServletRequest request, HttpServletResponse response)
throws IOException {
log.error("MethodologistSetupException occurred: {}", ex.getMessage(), ex);

ErrorResponseDTO errorResponse =
ErrorResponseDTO.builder()
.errorCode(ex.getErrorCode())
.message(ex.getMessage())
.status(HttpStatus.BAD_REQUEST.value())
.timestamp(System.currentTimeMillis())
.path(request.getDescription(false).replace("uri=", ""))
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
writeErrorResponse(
response, request, HttpStatus.BAD_REQUEST, ex.getErrorCode(), ex.getMessage());
}

/**
* Handles GenmodelException.
*
* @param ex the exception
* @param request the web request
* @return error response
*/
@ExceptionHandler(GenmodelException.class)
public ResponseEntity<ErrorResponseDTO> handleGenmodelException(
GenmodelException ex, WebRequest request) {
public void handleGenmodelException(
GenmodelException ex, HttpServletRequest request, HttpServletResponse response)
throws IOException {
log.error("GenmodelException occurred: {}", ex.getMessage(), ex);

ErrorResponseDTO errorResponse =
ErrorResponseDTO.builder()
.errorCode(ex.getErrorCode())
.message(ex.getMessage())
.status(HttpStatus.UNPROCESSABLE_ENTITY.value())
.timestamp(System.currentTimeMillis())
.path(request.getDescription(false).replace("uri=", ""))
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
writeErrorResponse(
response, request, HttpStatus.UNPROCESSABLE_ENTITY, ex.getErrorCode(), ex.getMessage());
}

/**
* Handles NoSuchFileException raised when an expected build artifact is missing.
*
* @param ex the exception
* @param request the web request
* @return error response with bad request status
*/
@ExceptionHandler(NoSuchFileException.class)
public ResponseEntity<ErrorResponseDTO> handleNoSuchFileException(
NoSuchFileException ex, WebRequest request) {
public void handleNoSuchFileException(
NoSuchFileException ex, HttpServletRequest request, HttpServletResponse response)
throws IOException {
log.error("NoSuchFileException occurred: {}", ex.getMessage(), ex);

ErrorResponseDTO errorResponse =
ErrorResponseDTO.builder()
.errorCode("VSUM_ARTIFACT_NOT_FOUND")
.message(ex.getMessage())
.status(HttpStatus.BAD_REQUEST.value())
.timestamp(System.currentTimeMillis())
.path(request.getDescription(false).replace("uri=", ""))
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
writeErrorResponse(
response, request, HttpStatus.BAD_REQUEST, "VSUM_ARTIFACT_NOT_FOUND", ex.getMessage());
}

/**
* Handles all other exceptions.
*
* @param ex the exception
* @param request the web request
* @return error response
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDTO> handleGlobalException(Exception ex, WebRequest request) {
public void handleGlobalException(
Exception ex, HttpServletRequest request, HttpServletResponse response) throws IOException {
log.error("Unexpected exception occurred: {}", ex.getMessage(), ex);

writeErrorResponse(
response,
request,
HttpStatus.INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
ErrorMessages.UNEXPECTED_ERROR);
}

private void writeErrorResponse(
HttpServletResponse response,
HttpServletRequest request,
HttpStatus status,
String errorCode,
String message)
throws IOException {
ErrorResponseDTO errorResponse =
ErrorResponseDTO.builder()
.errorCode("INTERNAL_ERROR")
.message(ErrorMessages.UNEXPECTED_ERROR)
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.errorCode(errorCode)
.message(message)
.status(status.value())
.timestamp(System.currentTimeMillis())
.path(request.getDescription(false).replace("uri=", ""))
.path(request.getRequestURI())
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
response.setStatus(status.value());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,14 @@ private byte[] buildArtifact(
Map<String, String> metamodelNamespaceMap = extractMetamodelNamespaceMap(modelPairs);
return generator.generate(modelPairs, copiedReactionFiles, metamodelNamespaceMap);
} catch (IOException | MissingModelException e) {
String reason = e.getMessage() != null ? e.getMessage() : e.toString();
throw new MethodologistSetupException(
VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive", e);
VSUM_BUILD_ERROR_CODE, "Failed to build VSUM project archive: " + reason, e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
String reason = e.getMessage() != null ? e.getMessage() : e.toString();
throw new MethodologistSetupException(
VSUM_BUILD_ERROR_CODE, "VSUM project build was interrupted", e);
VSUM_BUILD_ERROR_CODE, "VSUM project build was interrupted: " + reason, e);
} finally {
deleteRecursively(uploadWorkspace);
}
Expand Down Expand Up @@ -134,7 +136,10 @@ private void validateInputs(
if (metamodelFiles.size() != genmodelFiles.size()) {
throw new MethodologistSetupException(
VSUM_INPUT_ERROR_CODE,
"Metamodel and genmodel file counts must be identical to build file pairs");
String.format(
"Please upload the same number of metamodel and genmodel files "
+ "(received %d metamodel file(s) and %d genmodel file(s)).",
metamodelFiles.size(), genmodelFiles.size()));
}

for (MultipartFile file : metamodelFiles) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package tools.vitruv.methodologist.setup.vsum.controller;

import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import tools.vitruv.methodologist.setup.config.GlobalExceptionHandler;
import tools.vitruv.methodologist.setup.exception.MethodologistSetupException;
import tools.vitruv.methodologist.setup.vsum.service.VsumProjectBuildService;

@ExtendWith(MockitoExtension.class)
class VsumControllerErrorHandlingTest {

@Test
void buildProject_shouldReturnJsonErrorForZipAcceptHeader() throws Exception {
VsumProjectBuildService buildService = mock(VsumProjectBuildService.class);
VsumController controller = new VsumController(buildService);
MockMvc mockMvc =
MockMvcBuilders.standaloneSetup(controller)
.setControllerAdvice(new GlobalExceptionHandler())
.build();

when(buildService.buildProjectArchive(anyList(), anyList(), anyList()))
.thenThrow(
new MethodologistSetupException(
"VSUM_INPUT_ERROR",
"Please upload the same number of metamodel and genmodel files"));

MockMultipartFile metamodelFile =
new MockMultipartFile(
"metamodelFiles", "a.ecore", "application/xml", "<ecore/>".getBytes());
MockMultipartFile genmodelFile =
new MockMultipartFile(
"genmodelFiles", "a.genmodel", "application/xml", "<genmodel/>".getBytes());
MockMultipartFile reactionFile =
new MockMultipartFile(
"reactionFiles", "a.reactions", "text/plain", "import \"x\"".getBytes());

mockMvc
.perform(
multipart("/api/vsum/build")
.file(metamodelFile)
.file(genmodelFile)
.file(reactionFile)
.accept(MediaType.parseMediaType("application/zip")))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.errorCode").value("VSUM_INPUT_ERROR"))
.andExpect(
jsonPath("$.message")
.value("Please upload the same number of metamodel and genmodel files"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ void buildProjectArchive_shouldThrowWhenCountsDoNotMatch() {
List.of(mockMultipart("a.genmodel")),
List.of()))
.isInstanceOf(MethodologistSetupException.class)
.hasMessageContaining("counts must be identical");
.hasMessageContaining("Please upload the same number of metamodel and genmodel files")
.hasMessageContaining("received 2 metamodel file(s) and 1 genmodel file(s)");
}

@Test
Expand Down Expand Up @@ -202,7 +203,8 @@ void buildProjectJar_shouldThrowWhenCountsDoNotMatch() {
List.of(mockMultipart("a.genmodel")),
List.of()))
.isInstanceOf(MethodologistSetupException.class)
.hasMessageContaining("counts must be identical");
.hasMessageContaining("Please upload the same number of metamodel and genmodel files")
.hasMessageContaining("received 2 metamodel file(s) and 1 genmodel file(s)");
}

@Test
Expand Down
Loading