Skip to content
Open
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
@@ -0,0 +1,70 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.migration.diagram.converter.webapp;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.apache.tomcat.util.http.fileupload.impl.FileCountLimitExceededException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MultipartException;

@ControllerAdvice
public class ConverterExceptionHandler {

private static final Logger LOG = LoggerFactory.getLogger(ConverterExceptionHandler.class);

@Value("${server.tomcat.max-part-count:50}")
private int maxPartCount;

@ExceptionHandler(MultipartException.class)
public void handleMultipartException(MultipartException ex, HttpServletResponse response)
throws IOException {
Throwable rootCause = getRootCause(ex);

if (rootCause instanceof FileCountLimitExceededException) {
LOG.warn("File count limit exceeded: {}", rootCause.getMessage());
writeJsonError(
response, HttpStatus.PAYLOAD_TOO_LARGE, "FILE_COUNT_LIMIT_EXCEEDED", maxPartCount);
return;
}

LOG.error("Multipart request processing failed", ex);
writeJsonError(response, HttpStatus.BAD_REQUEST, "MULTIPART_ERROR", -1);
}

private void writeJsonError(
HttpServletResponse response, HttpStatus status, String errorCode, int limit)
throws IOException {
response.setStatus(status.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

String json =
"{\"errorCode\":\""
+ errorCode
+ "\",\"status\":"
+ status.value()
+ ",\"limit\":"
+ limit
+ "}";
response.getWriter().write(json);
}

private Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null && cause.getCause() != cause) {
cause = cause.getCause();
}
return cause;
}
}
65 changes: 55 additions & 10 deletions diagram-converter/webapp/src/main/javascript/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ProgressIndicator,
ProgressStep,
Button,
Callout,
ActionableNotification,
DataTable,
Table,
TableHead,
Expand Down Expand Up @@ -46,6 +46,9 @@ function App() {
const [previewTableHeader, setPreviewTableHeader] = useState([]);
const [previewTableRows, setPreviewTableRows] = useState([]);

const [downloadError, setDownloadError] = useState(null);
const [downloadErrorTitle, setDownloadErrorTitle] = useState("");

const [showConfig, setShowConfig] = useState(false);
const [configOptions, setConfigOptions] = useState({
defaultJobType: "camunda-7-job",
Expand Down Expand Up @@ -199,41 +202,72 @@ function App() {
setValidFiles(validFiles);
}

function buildErrorMessage(errorBody) {
switch (errorBody.errorCode) {
case "FILE_COUNT_LIMIT_EXCEEDED":
return <>
Too many files uploaded. The online version supports up to {errorBody.limit} files per request.
{" "}To learn how to run the diagram converter locally with a custom file limit, consult the{" "}
<a href="https://docs.camunda.io/docs/guides/migrating-from-camunda-7/migration-tooling/diagram-converter/#local-web-application"
target="_blank" rel="noopener noreferrer">diagram converter guide</a>.
</>;
default:
return "Download failed. Please try again.";
}
}

async function handleDownloadResponse(filename, response, title) {
if (!response.ok) {
let errorMessage = "Download failed. Please try again.";
try {
const errorBody = await response.json();
errorMessage = buildErrorMessage(errorBody);
} catch {
// Response body is not JSON, use default message
}
setDownloadErrorTitle(title);
setDownloadError(errorMessage);
return;
}
setDownloadError(null);
await download1(filename, response);
}

async function downloadXLS() {
const formData = createFormData(validFiles);
//validFiles.forEach((file) => formData.append("file", file));
await download1("analysis.xlsx",
await handleDownloadResponse("analysis.xlsx",
await fetch(baseUrl + "/check", {
body: formData,
method: "POST",
headers: {
Accept:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
})
}),
"Downloading XLSX failed"
);
}
async function downloadCSV() {
const formData = createFormData(validFiles);
//validFiles.forEach((file) => formData.append("file", file));
await download1("analysis.csv",
await handleDownloadResponse("analysis.csv",
await fetch(baseUrl + "/check", {
body: formData,
method: "POST",
headers: {
Accept: "text/csv",
},
})
}),
"Downloading CSV failed"
);
}
async function downloadZIP() {
const formData = createFormData(validFiles);
//validFiles.forEach((file) => formData.append("file", file));
await download1("converted-models.zip",
await handleDownloadResponse("converted-models.zip",
await fetch(baseUrl + "/convertBatch", {
body: formData,
method: "POST",
})
}),
"Downloading ZIP failed"
);
}

Expand Down Expand Up @@ -522,6 +556,17 @@ function App() {
}
/>
))}
{downloadError && (
<ActionableNotification
kind="error"
title={downloadErrorTitle}
lowContrast
onClose={() => setDownloadError(null)}
className="download-error-notification"
>
{downloadError}
</ActionableNotification>
)}
<Button
kind="tertiary"
size="lg"
Expand Down
6 changes: 6 additions & 0 deletions diagram-converter/webapp/src/main/javascript/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ th, td {
stroke: #0dcaf0 !important;
}

.download-error-notification {
width: 100%;
max-inline-size: 100%;
margin-bottom: 1rem;
}

.configBox {
margin-top: 1rem;
padding: 1rem;
Expand Down
9 changes: 9 additions & 0 deletions diagram-converter/webapp/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
server:
tomcat:
max-part-count: 100

spring:
servlet:
multipart:
resolve-lazily: true

notification:
slack:
enabled: false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.migration.diagram.converter.webapp;

import static org.assertj.core.api.Assertions.*;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import java.io.File;
import java.net.URISyntaxException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.context.TestPropertySource;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@TestPropertySource(
properties = {"server.tomcat.max-part-count=2", "spring.servlet.multipart.resolve-lazily=true"})
public class ConverterExceptionHandlerTest {

@LocalServerPort int port;

@BeforeEach
void setup() {
RestAssured.port = port;
}

@Test
void convertBatchExceedingFileCountLimit() throws URISyntaxException {
Response response =
RestAssured.given()
.contentType(ContentType.MULTIPART)
.multiPart(
"file", new File(getClass().getClassLoader().getResource("example.bpmn").toURI()))
.multiPart(
"file", new File(getClass().getClassLoader().getResource("example2.bpmn").toURI()))
.formParam("appendDocumentation", true)
.accept("application/zip")
.post("/convertBatch");

assertThat(response.statusCode()).isEqualTo(413);
assertThat(response.jsonPath().getString("errorCode")).isEqualTo("FILE_COUNT_LIMIT_EXCEEDED");
assertThat(response.jsonPath().getInt("limit")).isEqualTo(2);
}
}