pipeline stuff

This commit is contained in:
Anthony Stirling 2023-07-12 00:17:44 +01:00
parent 94526de04b
commit 50bcca10e2
6 changed files with 1242 additions and 1180 deletions

View File

@ -1,73 +1,76 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication @SpringBootApplication
@EnableScheduling @EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired
private Environment env; private Environment env;
@PostConstruct @PostConstruct
public void init() { public void init() {
// Check if the BROWSER_OPEN environment variable is set to true // Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true"); boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
if (browserOpen) { if (browserOpen) {
try { try {
String port = env.getProperty("local.server.port"); String port = env.getProperty("local.server.port");
if(port == null || port.length() == 0) { if(port == null || port.length() == 0) {
port="8080"; port="8080";
} }
String url = "http://localhost:" + port; String url = "http://localhost:" + port;
String os = System.getProperty("os.name").toLowerCase(); String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
if (os.contains("win")) { if (os.contains("win")) {
// For Windows // For Windows
rt.exec("rundll32 url.dll,FileProtocolHandler " + url); rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
} }
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SPdfApplication.class, args); SpringApplication.run(SPdfApplication.class, args);
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/"); GeneralUtils.createDir("customFiles/templates/");
GeneralUtils.createDir("config"); GeneralUtils.createDir("config");
System.out.println("Stirling-PDF Started.");
String port = System.getProperty("local.server.port");
if(port == null || port.length() == 0) { System.out.println("Stirling-PDF Started.");
port="8080";
} String port = System.getProperty("local.server.port");
String url = "http://localhost:" + port; if(port == null || port.length() == 0) {
System.out.println("Navigate to " + url); port="8080";
} }
String url = "http://localhost:" + port;
System.out.println("Navigate to " + url);
}
} }

View File

@ -1,60 +1,60 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Locale; import java.util.Locale;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.i18n.SessionLocaleResolver;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(new CleanUrlInterceptor()); registry.addInterceptor(new CleanUrlInterceptor());
} }
@Bean @Bean
public LocaleChangeInterceptor localeChangeInterceptor() { public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang"); lci.setParamName("lang");
return lci; return lci;
} }
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = System.getProperty("APP_LOCALE"); String appLocaleEnv = System.getProperty("APP_LOCALE");
if (appLocaleEnv == null) if (appLocaleEnv == null)
appLocaleEnv = System.getenv("APP_LOCALE"); appLocaleEnv = System.getenv("APP_LOCALE");
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
} }

View File

@ -1,497 +1,497 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime; import java.time.LocalTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.OutputStream; import java.io.OutputStream;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@Tag(name = "Pipeline", description = "Pipeline APIs") @Tag(name = "Pipeline", description = "Pipeline APIs")
public class Controller { public class Controller {
private static final Logger logger = LoggerFactory.getLogger(Controller.class); private static final Logger logger = LoggerFactory.getLogger(Controller.class);
@Autowired @Autowired
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
final String jsonFileName = "pipelineConfig.json"; final String jsonFileName = "pipelineConfig.json";
final String watchedFoldersDir = "watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Scheduled(fixedRate = 25000) @Scheduled(fixedRate = 25000)
public void scanFolders() { public void scanFolders() {
logger.info("Scanning folders..."); logger.info("Scanning folders...");
Path watchedFolderPath = Paths.get(watchedFoldersDir); Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) { if (!Files.exists(watchedFolderPath)) {
try { try {
Files.createDirectories(watchedFolderPath); Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath); logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e); logger.error("Error creating directory: {}", watchedFolderPath, e);
return; return;
} }
} }
try (Stream<Path> paths = Files.walk(watchedFolderPath)) { try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> { paths.filter(Files::isDirectory).forEach(t -> {
try { try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t); handleDirectory(t);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Error handling directory: {}", t, e); logger.error("Error handling directory: {}", t, e);
} }
}); });
} catch (Exception e) { } catch (Exception e) {
logger.error("Error walking through directory: {}", watchedFolderPath, e); logger.error("Error walking through directory: {}", watchedFolderPath, e);
} }
} }
private void handleDirectory(Path dir) throws Exception { private void handleDirectory(Path dir) throws Exception {
logger.info("Handling directory: {}", dir); logger.info("Handling directory: {}", dir);
Path jsonFile = dir.resolve(jsonFileName); Path jsonFile = dir.resolve(jsonFileName);
Path processingDir = dir.resolve("processing"); // Directory to move files during processing Path processingDir = dir.resolve("processing"); // Directory to move files during processing
if (!Files.exists(processingDir)) { if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir); Files.createDirectory(processingDir);
logger.info("Created processing directory: {}", processingDir); logger.info("Created processing directory: {}", processingDir);
} }
if (Files.exists(jsonFile)) { if (Files.exists(jsonFile)) {
// Read JSON file // Read JSON file
String jsonString; String jsonString;
try { try {
jsonString = new String(Files.readAllBytes(jsonFile)); jsonString = new String(Files.readAllBytes(jsonFile));
logger.info("Read JSON file: {}", jsonFile); logger.info("Read JSON file: {}", jsonFile);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error reading JSON file: {}", jsonFile, e); logger.error("Error reading JSON file: {}", jsonFile, e);
return; return;
} }
// Decode JSON to PipelineConfig // Decode JSON to PipelineConfig
PipelineConfig config; PipelineConfig config;
try { try {
config = objectMapper.readValue(jsonString, PipelineConfig.class); config = objectMapper.readValue(jsonString, PipelineConfig.class);
// Assuming your PipelineConfig class has getters for all necessary fields, you // Assuming your PipelineConfig class has getters for all necessary fields, you
// can perform checks here // can perform checks here
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) { if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
throw new IOException("Invalid JSON format"); throw new IOException("Invalid JSON format");
} }
} catch (IOException e) { } catch (IOException e) {
logger.error("Error parsing PipelineConfig: {}", jsonString, e); logger.error("Error parsing PipelineConfig: {}", jsonString, e);
return; return;
} }
// For each operation in the pipeline // For each operation in the pipeline
for (PipelineOperation operation : config.getOperations()) { for (PipelineOperation operation : config.getOperations()) {
// Collect all files based on fileInput // Collect all files based on fileInput
File[] files; File[] files;
String fileInput = (String) operation.getParameters().get("fileInput"); String fileInput = (String) operation.getParameters().get("fileInput");
if ("automated".equals(fileInput)) { if ("automated".equals(fileInput)) {
// If fileInput is "automated", process all files in the directory // If fileInput is "automated", process all files in the directory
try (Stream<Path> paths = Files.list(dir)) { try (Stream<Path> paths = Files.list(dir)) {
files = paths files = paths
.filter(path -> !Files.isDirectory(path)) // exclude directories .filter(path -> !Files.isDirectory(path)) // exclude directories
.filter(path -> !path.equals(jsonFile)) // exclude jsonFile .filter(path -> !path.equals(jsonFile)) // exclude jsonFile
.map(Path::toFile) .map(Path::toFile)
.toArray(File[]::new); .toArray(File[]::new);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
return; return;
} }
} else { } else {
// If fileInput contains a path, process only this file // If fileInput contains a path, process only this file
files = new File[] { new File(fileInput) }; files = new File[] { new File(fileInput) };
} }
// Prepare the files for processing // Prepare the files for processing
List<File> filesToProcess = new ArrayList<>(); List<File> filesToProcess = new ArrayList<>();
for (File file : files) { for (File file : files) {
logger.info(file.getName()); logger.info(file.getName());
logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName())); logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
Files.move(file.toPath(), processingDir.resolve(file.getName())); Files.move(file.toPath(), processingDir.resolve(file.getName()));
filesToProcess.add(processingDir.resolve(file.getName()).toFile()); filesToProcess.add(processingDir.resolve(file.getName()).toFile());
} }
// Process the files // Process the files
try { try {
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString); List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
if(resources == null) { if(resources == null) {
return; return;
} }
// Move resultant files and rename them as per config in JSON file // Move resultant files and rename them as per config in JSON file
for (Resource resource : resources) { for (Resource resource : resources) {
String resourceName = resource.getFilename(); String resourceName = resource.getFilename();
String baseName = resourceName.substring(0, resourceName.lastIndexOf(".")); String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
String extension = resourceName.substring(resourceName.lastIndexOf(".")+1); String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
String outputFileName = config.getOutputPattern().replace("{filename}", baseName); String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
outputFileName = outputFileName.replace("{pipelineName}", config.getName()); outputFileName = outputFileName.replace("{pipelineName}", config.getName());
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter)); outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss"); DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter)); outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
outputFileName += "." + extension; outputFileName += "." + extension;
// {filename} {folder} {date} {tmime} {pipeline} // {filename} {folder} {date} {tmime} {pipeline}
String outputDir = config.getOutputDir(); String outputDir = config.getOutputDir();
// Check if the environment variable 'automatedOutputFolder' is set // Check if the environment variable 'automatedOutputFolder' is set
String outputFolder = System.getenv("automatedOutputFolder"); String outputFolder = System.getenv("automatedOutputFolder");
if (outputFolder == null || outputFolder.isEmpty()) { if (outputFolder == null || outputFolder.isEmpty()) {
// If the environment variable is not set, use the default value // If the environment variable is not set, use the default value
outputFolder = finishedFoldersDir; outputFolder = finishedFoldersDir;
} }
logger.info("outputDir 0={}", outputDir); logger.info("outputDir 0={}", outputDir);
// Replace the placeholders in the outputDir string // Replace the placeholders in the outputDir string
outputDir = outputDir.replace("{outputFolder}", outputFolder); outputDir = outputDir.replace("{outputFolder}", outputFolder);
outputDir = outputDir.replace("{folderName}", dir.toString()); outputDir = outputDir.replace("{folderName}", dir.toString());
logger.info("outputDir 1={}", outputDir); logger.info("outputDir 1={}", outputDir);
outputDir = outputDir.replace("\\watchedFolders", ""); outputDir = outputDir.replace("\\watchedFolders", "");
outputDir = outputDir.replace("//watchedFolders", ""); outputDir = outputDir.replace("//watchedFolders", "");
outputDir = outputDir.replace("\\\\watchedFolders", ""); outputDir = outputDir.replace("\\\\watchedFolders", "");
outputDir = outputDir.replace("/watchedFolders", ""); outputDir = outputDir.replace("/watchedFolders", "");
Path outputPath; Path outputPath;
logger.info("outputDir 2={}", outputDir); logger.info("outputDir 2={}", outputDir);
if (Paths.get(outputDir).isAbsolute()) { if (Paths.get(outputDir).isAbsolute()) {
// If it's an absolute path, use it directly // If it's an absolute path, use it directly
outputPath = Paths.get(outputDir); outputPath = Paths.get(outputDir);
} else { } else {
// If it's a relative path, make it relative to the current working directory // If it's a relative path, make it relative to the current working directory
outputPath = Paths.get(".", outputDir); outputPath = Paths.get(".", outputDir);
} }
logger.info("outputPath={}", outputPath); logger.info("outputPath={}", outputPath);
if (!Files.exists(outputPath)) { if (!Files.exists(outputPath)) {
try { try {
Files.createDirectories(outputPath); Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath); logger.info("Created directory: {}", outputPath);
} catch (IOException e) { } catch (IOException e) {
logger.error("Error creating directory: {}", outputPath, e); logger.error("Error creating directory: {}", outputPath, e);
return; return;
} }
} }
logger.info("outputPath {}", outputPath); logger.info("outputPath {}", outputPath);
logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString()); logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
File newFile = new File(outputPath.resolve(outputFileName).toString()); File newFile = new File(outputPath.resolve(outputFileName).toString());
OutputStream os = new FileOutputStream(newFile); OutputStream os = new FileOutputStream(newFile);
os.write(((ByteArrayResource)resource).getByteArray()); os.write(((ByteArrayResource)resource).getByteArray());
os.close(); os.close();
logger.info("made {}", outputPath.resolve(outputFileName)); logger.info("made {}", outputPath.resolve(outputFileName));
} }
// If successful, delete the original files // If successful, delete the original files
for (File file : filesToProcess) { for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName())); Files.deleteIfExists(processingDir.resolve(file.getName()));
} }
} catch (Exception e) { } catch (Exception e) {
// If an error occurs, move the original files back // If an error occurs, move the original files back
for (File file : filesToProcess) { for (File file : filesToProcess) {
Files.move(processingDir.resolve(file.getName()), file.toPath()); Files.move(processingDir.resolve(file.getName()), file.toPath());
} }
throw e; throw e;
} }
} }
} }
} }
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception { List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
logger.info("Processing files... " + outputFiles); logger.info("Processing files... " + outputFiles);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString); JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline"); JsonNode pipelineNode = jsonNode.get("pipeline");
ByteArrayOutputStream logStream = new ByteArrayOutputStream(); ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream); PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false; boolean hasErrors = false;
for (JsonNode operationNode : pipelineNode) { for (JsonNode operationNode : pipelineNode) {
String operation = operationNode.get("operation").asText(); String operation = operationNode.get("operation").asText();
logger.info("Running operation: {}", operation); logger.info("Running operation: {}", operation);
JsonNode parametersNode = operationNode.get("parameters"); JsonNode parametersNode = operationNode.get("parameters");
String inputFileExtension = ""; String inputFileExtension = "";
if (operationNode.has("inputFileType")) { if (operationNode.has("inputFileType")) {
inputFileExtension = operationNode.get("inputFileType").asText(); inputFileExtension = operationNode.get("inputFileType").asText();
} else { } else {
inputFileExtension = ".pdf"; inputFileExtension = ".pdf";
} }
List<Resource> newOutputFiles = new ArrayList<>(); List<Resource> newOutputFiles = new ArrayList<>();
boolean hasInputFileType = false; boolean hasInputFileType = false;
for (Resource file : outputFiles) { for (Resource file : outputFiles) {
if (file.getFilename().endsWith(inputFileExtension)) { if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true; hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file); body.add("fileInput", file);
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields(); Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
while (parameters.hasNext()) { while (parameters.hasNext()) {
Map.Entry<String, JsonNode> parameter = parameters.next(); Map.Entry<String, JsonNode> parameter = parameters.next();
body.add(parameter.getKey(), parameter.getValue().asText()); body.add(parameter.getKey(), parameter.getValue().asText());
} }
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers); HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/" + operation; String url = "http://localhost:8080/" + operation;
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
if (!response.getStatusCode().equals(HttpStatus.OK)) { if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody()); logPrintStream.println("Error: " + response.getBody());
hasErrors = true; hasErrors = true;
continue; continue;
} }
// Check if the response body is a zip file // Check if the response body is a zip file
if (isZip(response.getBody())) { if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files // Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody())); newOutputFiles.addAll(unzip(response.getBody()));
} else { } else {
Resource outputResource = new ByteArrayResource(response.getBody()) { Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getFilename(); // Preserving original filename return file.getFilename(); // Preserving original filename
} }
}; };
newOutputFiles.add(outputResource); newOutputFiles.add(outputResource);
} }
} }
if (!hasInputFileType) { if (!hasInputFileType) {
logPrintStream.println( logPrintStream.println(
"No files with extension " + inputFileExtension + " found for operation " + operation); "No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true; hasErrors = true;
} }
outputFiles = newOutputFiles; outputFiles = newOutputFiles;
} }
logPrintStream.close(); logPrintStream.close();
} }
if (hasErrors) { if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString()); logger.error("Errors occurred during processing. Log: {}", logStream.toString());
} }
return outputFiles; return outputFiles;
} }
List<Resource> handleFiles(File[] files, String jsonString) throws Exception { List<Resource> handleFiles(File[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) { if(files == null || files.length == 0) {
logger.info("No files"); logger.info("No files");
return null; return null;
} }
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString); JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline"); JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false; boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (File file : files) { for (File file : files) {
Path path = Paths.get(file.getAbsolutePath()); Path path = Paths.get(file.getAbsolutePath());
System.out.println("Reading file: " + path); // debug statement System.out.println("Reading file: " + path); // debug statement
if (Files.exists(path)) { if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getName(); return file.getName();
} }
}; };
outputFiles.add(fileResource); outputFiles.add(fileResource);
} else { } else {
System.out.println("File not found: " + path); // debug statement System.out.println("File not found: " + path); // debug statement
} }
} }
logger.info("Files successfully loaded. Starting processing..."); logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString); return processFiles(outputFiles, jsonString);
} }
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception { List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) { if(files == null || files.length == 0) {
logger.info("No files"); logger.info("No files");
return null; return null;
} }
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString); JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline"); JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false; boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) { for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) { Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getOriginalFilename(); return file.getOriginalFilename();
} }
}; };
outputFiles.add(fileResource); outputFiles.add(fileResource);
} }
logger.info("Files successfully loaded. Starting processing..."); logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString); return processFiles(outputFiles, jsonString);
} }
@PostMapping("/handleData") @PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files, public ResponseEntity<byte[]> handleData(@RequestPart("fileInput") MultipartFile[] files,
@RequestParam("json") String jsonString) { @RequestParam("json") String jsonString) {
logger.info("Received POST request to /handleData with {} files", files.length); logger.info("Received POST request to /handleData with {} files", files.length);
try { try {
List<Resource> outputFiles = handleFiles(files, jsonString); List<Resource> outputFiles = handleFiles(files, jsonString);
if (outputFiles != null && outputFiles.size() == 1) { if (outputFiles != null && outputFiles.size() == 1) {
// If there is only one file, return it directly // If there is only one file, return it directly
Resource singleFile = outputFiles.get(0); Resource singleFile = outputFiles.get(0);
InputStream is = singleFile.getInputStream(); InputStream is = singleFile.getInputStream();
byte[] bytes = new byte[(int) singleFile.contentLength()]; byte[] bytes = new byte[(int) singleFile.contentLength()];
is.read(bytes); is.read(bytes);
is.close(); is.close();
logger.info("Returning single file response..."); logger.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(),
MediaType.APPLICATION_OCTET_STREAM); MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) { } else if (outputFiles == null) {
return null; return null;
} }
// Create a ByteArrayOutputStream to hold the zip // Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos); ZipOutputStream zipOut = new ZipOutputStream(baos);
// Loop through each file and add it to the zip // Loop through each file and add it to the zip
for (Resource file : outputFiles) { for (Resource file : outputFiles) {
ZipEntry zipEntry = new ZipEntry(file.getFilename()); ZipEntry zipEntry = new ZipEntry(file.getFilename());
zipOut.putNextEntry(zipEntry); zipOut.putNextEntry(zipEntry);
// Read the file into a byte array // Read the file into a byte array
InputStream is = file.getInputStream(); InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()]; byte[] bytes = new byte[(int) file.contentLength()];
is.read(bytes); is.read(bytes);
// Write the bytes of the file to the zip // Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length); zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry(); zipOut.closeEntry();
is.close(); is.close();
} }
zipOut.close(); zipOut.close();
logger.info("Returning zipped file response..."); logger.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) { } catch (Exception e) {
logger.error("Error handling data: ", e); logger.error("Error handling data: ", e);
return null; return null;
} }
} }
private boolean isZip(byte[] data) { private boolean isZip(byte[] data) {
if (data == null || data.length < 4) { if (data == null || data.length < 4) {
return false; return false;
} }
// Check the first four bytes of the data against the standard zip magic number // Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
} }
private List<Resource> unzip(byte[] data) throws IOException { private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length); logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>(); List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data); try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) { ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry; ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) { while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int count; int count;
while ((count = zis.read(buffer)) != -1) { while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count); baos.write(buffer, 0, count);
} }
final String filename = entry.getName(); final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) { Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override @Override
public String getFilename() { public String getFilename() {
return filename; return filename;
} }
}; };
// If the unzipped file is a zip file, unzip it // If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) { if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename); logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray())); unzippedFiles.addAll(unzip(baos.toByteArray()));
} else { } else {
unzippedFiles.add(fileResource); unzippedFiles.add(fileResource);
} }
} }
} }
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles; return unzippedFiles;
} }
} }

View File

@ -1,28 +1,76 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@Controller @Controller
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class GeneralWebController { public class GeneralWebController {
@GetMapping("/pipeline")
@Hidden @GetMapping("/pipeline")
public String pipelineForm(Model model) { @Hidden
model.addAttribute("currentPage", "pipeline"); public String pipelineForm(Model model) {
return "pipeline"; model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList());
for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, Map.class);
String name = (String) jsonContent.get("name");
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
} catch (IOException e) {
e.printStackTrace();
} }
model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline";
}
@GetMapping("/merge-pdfs") @GetMapping("/merge-pdfs")
@Hidden @Hidden

View File

@ -1,432 +1,456 @@
document.getElementById('validateButton').addEventListener('click', function(event) { document.getElementById('validateButton').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
validatePipeline(); validatePipeline();
}); });
function validatePipeline() { function validatePipeline() {
let pipelineListItems = document.getElementById('pipelineList').children; let pipelineListItems = document.getElementById('pipelineList').children;
let isValid = true; let isValid = true;
let containsAddPassword = false; let containsAddPassword = false;
for (let i = 0; i < pipelineListItems.length - 1; i++) { for (let i = 0; i < pipelineListItems.length - 1; i++) {
let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent; let currentOperation = pipelineListItems[i].querySelector('.operationName').textContent;
let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent; let nextOperation = pipelineListItems[i + 1].querySelector('.operationName').textContent;
if (currentOperation === '/add-password') { if (currentOperation === '/add-password') {
containsAddPassword = true; containsAddPassword = true;
} }
console.log(currentOperation); console.log(currentOperation);
console.log(apiDocs[currentOperation]); console.log(apiDocs[currentOperation]);
let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; let currentOperationDescription = apiDocs[currentOperation]?.post?.description || "";
let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; let nextOperationDescription = apiDocs[nextOperation]?.post?.description || "";
console.log("currentOperationDescription", currentOperationDescription); console.log("currentOperationDescription", currentOperationDescription);
console.log("nextOperationDescription", nextOperationDescription); console.log("nextOperationDescription", nextOperationDescription);
let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || "";
let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || "";
console.log("Operation " + currentOperation + " Output: " + currentOperationOutput); console.log("Operation " + currentOperation + " Output: " + currentOperationOutput);
console.log("Operation " + nextOperation + " Input: " + nextOperationInput); console.log("Operation " + nextOperation + " Input: " + nextOperationInput);
// Splitting in case of multiple possible output/input // Splitting in case of multiple possible output/input
let currentOperationOutputArr = currentOperationOutput.split('/'); let currentOperationOutputArr = currentOperationOutput.split('/');
let nextOperationInputArr = nextOperationInput.split('/'); let nextOperationInputArr = nextOperationInput.split('/');
if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') { if (currentOperationOutput !== 'ANY' && nextOperationInput !== 'ANY') {
let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value)); let intersection = currentOperationOutputArr.filter(value => nextOperationInputArr.includes(value));
console.log(`Intersection: ${intersection}`); console.log(`Intersection: ${intersection}`);
if (intersection.length === 0) { if (intersection.length === 0) {
isValid = false; isValid = false;
console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
break; break;
} }
} }
} }
if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') {
alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.');
return false; return false;
} }
if (isValid) { if (isValid) {
console.log('Pipeline is valid'); console.log('Pipeline is valid');
// Continue with the pipeline operation // Continue with the pipeline operation
} else { } else {
console.error('Pipeline is not valid'); console.error('Pipeline is not valid');
// Stop operation, maybe display an error to the user // Stop operation, maybe display an error to the user
} }
return isValid; return isValid;
} }
document.getElementById('submitConfigBtn').addEventListener('click', function() { document.getElementById('submitConfigBtn').addEventListener('click', function() {
if (validatePipeline() === false) { if (validatePipeline() === false) {
return; return;
} }
let selectedOperation = document.getElementById('operationsDropdown').value; let selectedOperation = document.getElementById('operationsDropdown').value;
let parameters = operationSettings[selectedOperation] || {}; let parameters = operationSettings[selectedOperation] || {};
let pipelineConfig = { let pipelineConfig = {
"name": "uniquePipelineName", "name": "uniquePipelineName",
"pipeline": [{ "pipeline": [{
"operation": selectedOperation, "operation": selectedOperation,
"parameters": parameters "parameters": parameters
}], }],
"_examples": { "_examples": {
"outputDir" : "{outputFolder}/{folderName}", "outputDir" : "{outputFolder}/{folderName}",
"outputFileName" : "{filename}-{pipelineName}-{date}-{time}" "outputFileName" : "{filename}-{pipelineName}-{date}-{time}"
}, },
"outputDir" : "httpWebRequest", "outputDir" : "httpWebRequest",
"outputFileName" : "{filename}" "outputFileName" : "{filename}"
}; };
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2);
let formData = new FormData(); let formData = new FormData();
let fileInput = document.getElementById('fileInput'); let fileInput = document.getElementById('fileInput');
let files = fileInput.files; let files = fileInput.files;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
console.log("files[i]", files[i].name); console.log("files[i]", files[i].name);
formData.append('fileInput', files[i], files[i].name); formData.append('fileInput', files[i], files[i].name);
} }
console.log("pipelineConfigJson", pipelineConfigJson); console.log("pipelineConfigJson", pipelineConfigJson);
formData.append('json', pipelineConfigJson); formData.append('json', pipelineConfigJson);
console.log("formData", formData); console.log("formData", formData);
fetch('/handleData', { fetch('/handleData', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => response.blob()) .then(response => response.blob())
.then(blob => { .then(blob => {
let url = window.URL.createObjectURL(blob); let url = window.URL.createObjectURL(blob);
let a = document.createElement('a'); let a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'outputfile'; a.download = 'outputfile';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
}) })
.catch((error) => { .catch((error) => {
console.error('Error:', error); console.error('Error:', error);
}); });
}); });
let apiDocs = {}; let apiDocs = {};
let operationSettings = {}; let operationSettings = {};
fetch('v3/api-docs') fetch('v3/api-docs')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
apiDocs = data.paths; apiDocs = data.paths;
let operationsDropdown = document.getElementById('operationsDropdown'); let operationsDropdown = document.getElementById('operationsDropdown');
const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here
operationsDropdown.innerHTML = ''; operationsDropdown.innerHTML = '';
let operationsByTag = {}; let operationsByTag = {};
// Group operations by tags // Group operations by tags
Object.keys(data.paths).forEach(operationPath => { Object.keys(data.paths).forEach(operationPath => {
let operation = data.paths[operationPath].post; let operation = data.paths[operationPath].post;
if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) { if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) {
let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag
if (!operationsByTag[operationTag]) { if (!operationsByTag[operationTag]) {
operationsByTag[operationTag] = []; operationsByTag[operationTag] = [];
} }
operationsByTag[operationTag].push(operationPath); operationsByTag[operationTag].push(operationPath);
} }
}); });
// Specify the order of tags // Specify the order of tags
let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; let tagOrder = ["General", "Security", "Convert", "Other", "Filter"];
// Create dropdown options // Create dropdown options
tagOrder.forEach(tag => { tagOrder.forEach(tag => {
if (operationsByTag[tag]) { if (operationsByTag[tag]) {
let group = document.createElement('optgroup'); let group = document.createElement('optgroup');
group.label = tag; group.label = tag;
operationsByTag[tag].forEach(operationPath => { operationsByTag[tag].forEach(operationPath => {
let option = document.createElement('option'); let option = document.createElement('option');
let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes
option.textContent = operationWithoutSlash; option.textContent = operationWithoutSlash;
option.value = operationPath; // Keep the value with slashes for querying option.value = operationPath; // Keep the value with slashes for querying
group.appendChild(option); group.appendChild(option);
}); });
operationsDropdown.appendChild(group); operationsDropdown.appendChild(group);
} }
}); });
}); });
document.getElementById('addOperationBtn').addEventListener('click', function() { document.getElementById('addOperationBtn').addEventListener('click', function() {
let selectedOperation = document.getElementById('operationsDropdown').value; let selectedOperation = document.getElementById('operationsDropdown').value;
let pipelineList = document.getElementById('pipelineList'); let pipelineList = document.getElementById('pipelineList');
let listItem = document.createElement('li'); let listItem = document.createElement('li');
listItem.className = "list-group-item"; listItem.className = "list-group-item";
let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post &&
apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0); apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0);
listItem.innerHTML = ` listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100"> <div class="d-flex justify-content-between align-items-center w-100">
<div class="operationName">${selectedOperation}</div> <div class="operationName">${selectedOperation}</div>
<div class="arrows d-flex"> <div class="arrows d-flex">
<button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button> <button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button> <button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button>
<button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};"></span></button> <button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};"></span></button>
<button class="btn btn-danger remove"><span>X</span></button> <button class="btn btn-danger remove"><span>X</span></button>
</div> </div>
</div> </div>
`; `;
pipelineList.appendChild(listItem); pipelineList.appendChild(listItem);
listItem.querySelector('.move-up').addEventListener('click', function(event) { listItem.querySelector('.move-up').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
if (listItem.previousElementSibling) { if (listItem.previousElementSibling) {
pipelineList.insertBefore(listItem, listItem.previousElementSibling); pipelineList.insertBefore(listItem, listItem.previousElementSibling);
} }
}); });
listItem.querySelector('.move-down').addEventListener('click', function(event) { listItem.querySelector('.move-down').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
if (listItem.nextElementSibling) { if (listItem.nextElementSibling) {
pipelineList.insertBefore(listItem.nextElementSibling, listItem); pipelineList.insertBefore(listItem.nextElementSibling, listItem);
} }
}); });
listItem.querySelector('.remove').addEventListener('click', function(event) { listItem.querySelector('.remove').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
pipelineList.removeChild(listItem); pipelineList.removeChild(listItem);
}); });
listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
showpipelineSettingsModal(selectedOperation); showpipelineSettingsModal(selectedOperation);
}); });
function showpipelineSettingsModal(operation) { function showpipelineSettingsModal(operation) {
let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsModal = document.getElementById('pipelineSettingsModal');
let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent');
let operationData = apiDocs[operation].post.parameters || []; let operationData = apiDocs[operation].post.parameters || [];
pipelineSettingsContent.innerHTML = ''; pipelineSettingsContent.innerHTML = '';
operationData.forEach(parameter => { operationData.forEach(parameter => {
let parameterDiv = document.createElement('div'); let parameterDiv = document.createElement('div');
parameterDiv.className = "form-group"; parameterDiv.className = "form-group";
let parameterLabel = document.createElement('label'); let parameterLabel = document.createElement('label');
parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `;
parameterLabel.title = parameter.description; parameterLabel.title = parameter.description;
parameterDiv.appendChild(parameterLabel); parameterDiv.appendChild(parameterLabel);
let parameterInput; let parameterInput;
switch (parameter.schema.type) { switch (parameter.schema.type) {
case 'string': case 'string':
case 'number': case 'number':
case 'integer': case 'integer':
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = parameter.schema.type === 'string' ? 'text' : 'number'; parameterInput.type = parameter.schema.type === 'string' ? 'text' : 'number';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
break; break;
case 'boolean': case 'boolean':
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'checkbox'; parameterInput.type = 'checkbox';
break; break;
case 'array': case 'array':
case 'object': case 'object':
parameterInput = document.createElement('textarea'); parameterInput = document.createElement('textarea');
parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`; parameterInput.placeholder = `Enter a JSON formatted ${parameter.schema.type}`;
parameterInput.className = "form-control"; parameterInput.className = "form-control";
break; break;
case 'enum': case 'enum':
parameterInput = document.createElement('select'); parameterInput = document.createElement('select');
parameterInput.className = "form-control"; parameterInput.className = "form-control";
parameter.schema.enum.forEach(option => { parameter.schema.enum.forEach(option => {
let optionElement = document.createElement('option'); let optionElement = document.createElement('option');
optionElement.value = option; optionElement.value = option;
optionElement.text = option; optionElement.text = option;
parameterInput.appendChild(optionElement); parameterInput.appendChild(optionElement);
}); });
break; break;
default: default:
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'text'; parameterInput.type = 'text';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
} }
parameterInput.id = parameter.name; parameterInput.id = parameter.name;
if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) {
let savedValue = operationSettings[operation][parameter.name]; let savedValue = operationSettings[operation][parameter.name];
switch (parameter.schema.type) { switch (parameter.schema.type) {
case 'number': case 'number':
case 'integer': case 'integer':
parameterInput.value = savedValue.toString(); parameterInput.value = savedValue.toString();
break; break;
case 'boolean': case 'boolean':
parameterInput.checked = savedValue; parameterInput.checked = savedValue;
break; break;
case 'array': case 'array':
case 'object': case 'object':
parameterInput.value = JSON.stringify(savedValue); parameterInput.value = JSON.stringify(savedValue);
break; break;
default: default:
parameterInput.value = savedValue; parameterInput.value = savedValue;
} }
} }
parameterDiv.appendChild(parameterInput); parameterDiv.appendChild(parameterInput);
pipelineSettingsContent.appendChild(parameterDiv); pipelineSettingsContent.appendChild(parameterDiv);
}); });
let saveButton = document.createElement('button'); let saveButton = document.createElement('button');
saveButton.textContent = "Save Settings"; saveButton.textContent = "Save Settings";
saveButton.className = "btn btn-primary"; saveButton.className = "btn btn-primary";
saveButton.addEventListener('click', function(event) { saveButton.addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
let settings = {}; let settings = {};
operationData.forEach(parameter => { operationData.forEach(parameter => {
let value = document.getElementById(parameter.name).value; let value = document.getElementById(parameter.name).value;
switch (parameter.schema.type) { switch (parameter.schema.type) {
case 'number': case 'number':
case 'integer': case 'integer':
settings[parameter.name] = Number(value); settings[parameter.name] = Number(value);
break; break;
case 'boolean': case 'boolean':
settings[parameter.name] = document.getElementById(parameter.name).checked; settings[parameter.name] = document.getElementById(parameter.name).checked;
break; break;
case 'array': case 'array':
case 'object': case 'object':
try { try {
settings[parameter.name] = JSON.parse(value); settings[parameter.name] = JSON.parse(value);
} catch (err) { } catch (err) {
console.error(`Invalid JSON format for ${parameter.name}`); console.error(`Invalid JSON format for ${parameter.name}`);
} }
break; break;
default: default:
settings[parameter.name] = value; settings[parameter.name] = value;
} }
}); });
operationSettings[operation] = settings; operationSettings[operation] = settings;
console.log(settings); console.log(settings);
pipelineSettingsModal.style.display = "none"; pipelineSettingsModal.style.display = "none";
}); });
pipelineSettingsContent.appendChild(saveButton); pipelineSettingsContent.appendChild(saveButton);
pipelineSettingsModal.style.display = "block"; pipelineSettingsModal.style.display = "block";
pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() {
pipelineSettingsModal.style.display = "none"; pipelineSettingsModal.style.display = "none";
} }
window.onclick = function(event) { window.onclick = function(event) {
if (event.target == pipelineSettingsModal) { if (event.target == pipelineSettingsModal) {
pipelineSettingsModal.style.display = "none"; pipelineSettingsModal.style.display = "none";
} }
} }
} }
document.getElementById('savePipelineBtn').addEventListener('click', function() { document.getElementById('savePipelineBtn').addEventListener('click', function() {
if (validatePipeline() === false) { if (validatePipeline() === false) {
return; return;
} }
var pipelineName = document.getElementById('pipelineName').value; var pipelineName = document.getElementById('pipelineName').value;
let pipelineList = document.getElementById('pipelineList').children; let pipelineList = document.getElementById('pipelineList').children;
let pipelineConfig = { let pipelineConfig = {
"name": pipelineName, "name": pipelineName,
"pipeline": [], "pipeline": [],
"_examples": { "_examples": {
"outputDir" : "{outputFolder}/{folderName}", "outputDir" : "{outputFolder}/{folderName}",
"outputFileName" : "{filename}-{pipelineName}-{date}-{time}" "outputFileName" : "{filename}-{pipelineName}-{date}-{time}"
}, },
"outputDir" : "httpWebRequest", "outputDir" : "httpWebRequest",
"outputFileName" : "{filename}" "outputFileName" : "{filename}"
}; };
for (let i = 0; i < pipelineList.length; i++) { for (let i = 0; i < pipelineList.length; i++) {
let operationName = pipelineList[i].querySelector('.operationName').textContent; let operationName = pipelineList[i].querySelector('.operationName').textContent;
let parameters = operationSettings[operationName] || {}; let parameters = operationSettings[operationName] || {};
pipelineConfig.pipeline.push({ pipelineConfig.pipeline.push({
"operation": operationName, "operation": operationName,
"parameters": parameters "parameters": parameters
}); });
} }
let a = document.createElement('a'); let a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], {
type: 'application/json' type: 'application/json'
})); }));
a.download = 'pipelineConfig.json'; a.download = 'pipelineConfig.json';
a.style.display = 'none'; a.style.display = 'none';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
}); });
document.getElementById('uploadPipelineBtn').addEventListener('click', function() { async function processPipelineConfig(configString) {
document.getElementById('uploadPipelineInput').click(); let pipelineConfig = JSON.parse(configString);
}); let pipelineList = document.getElementById('pipelineList');
document.getElementById('uploadPipelineInput').addEventListener('change', function(e) { while (pipelineList.firstChild) {
let reader = new FileReader(); pipelineList.removeChild(pipelineList.firstChild);
reader.onload = function(event) { }
let pipelineConfig = JSON.parse(event.target.result); document.getElementById('pipelineName').value = pipelineConfig.name
let pipelineList = document.getElementById('pipelineList'); for (const operationConfig of pipelineConfig.pipeline) {
let operationsDropdown = document.getElementById('operationsDropdown');
while (pipelineList.firstChild) { operationsDropdown.value = operationConfig.operation;
pipelineList.removeChild(pipelineList.firstChild); operationSettings[operationConfig.operation] = operationConfig.parameters;
}
document.getElementById('pipelineName').value = pipelineConfig.name // assuming addOperation is async
pipelineConfig.pipeline.forEach(operationConfig => { await new Promise((resolve) => {
let operationsDropdown = document.getElementById('operationsDropdown'); document.getElementById('addOperationBtn').addEventListener('click', resolve, { once: true });
operationsDropdown.value = operationConfig.operation; document.getElementById('addOperationBtn').click();
operationSettings[operationConfig.operation] = operationConfig.parameters; });
document.getElementById('addOperationBtn').click();
let lastOperation = pipelineList.lastChild;
let lastOperation = pipelineList.lastChild;
Object.keys(operationConfig.parameters).forEach(parameterName => {
lastOperation.querySelector('.pipelineSettings').click(); let input = document.getElementById(parameterName);
if (input) {
Object.keys(operationConfig.parameters).forEach(parameterName => { switch (input.type) {
let input = document.getElementById(parameterName); case 'checkbox':
if (input) { input.checked = operationConfig.parameters[parameterName];
switch (input.type) { break;
case 'checkbox': case 'number':
input.checked = operationConfig.parameters[parameterName]; input.value = operationConfig.parameters[parameterName].toString();
break; break;
case 'number': case 'file':
input.value = operationConfig.parameters[parameterName].toString(); if (parameterName !== 'fileInput') {
break; // Create a new file input element
case 'text': let newInput = document.createElement('input');
case 'textarea': newInput.type = 'file';
default: newInput.id = parameterName;
input.value = JSON.stringify(operationConfig.parameters[parameterName]);
} // Add the new file input to the main page (change the selector according to your needs)
} document.querySelector('#main').appendChild(newInput);
}); }
break;
document.querySelector('#pipelineSettingsModal .btn-primary').click(); case 'text':
}); case 'textarea':
}; default:
reader.readAsText(e.target.files[0]); input.value = JSON.stringify(operationConfig.parameters[parameterName]);
}); }
}
});
}
}
document.getElementById('uploadPipelineBtn').addEventListener('click', function() {
document.getElementById('uploadPipelineInput').click();
});
document.getElementById('uploadPipelineInput').addEventListener('change', function(e) {
let reader = new FileReader();
reader.onload = function(event) {
processPipelineConfig(event.target.result);
};
reader.readAsText(e.target.files[0]);
});
document.getElementById('pipelineSelect').addEventListener('change', function(e) {
let selectedPipelineJson = e.target.value; // assuming the selected value is the JSON string of the pipeline config
processPipelineConfig(selectedPipelineJson);
});
}); });

View File

@ -1,115 +1,102 @@
<!DOCTYPE html> <!DOCTYPE html>
<html th:lang="${#locale.toString()}" <html th:lang="${#locale.toString()}"
th:lang-direction="#{language.direction}" th:lang-direction="#{language.direction}"
xmlns:th="http://www.thymeleaf.org"> xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{pipeline.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{pipeline.title})}"></th:block>
<body> <body>
<div id="page-container"> <div id="page-container">
<div id="content-wrap"> <div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div> <div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br> <br> <br>
<div class="container" id="dropContainer"> <div class="container" id="dropContainer">
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6">
<!-- Trigger/Open The Modal -->
<div class="mb-3"> <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#pipelineSettingsModal">
<button id="savePipelineBtn" class="btn btn-success">Download</button> Open Pipeline Settings
</button>
<button id="validateButton" class="btn btn-success">Validate</button>
<div class="btn-group">
<button id="uploadPipelineBtn" class="btn btn-primary">Upload</button> <button id="uploadPipelineBtn" class="btn btn-primary">Upload Custom Pipeline</button>
<input type="file" id="uploadPipelineInput" accept=".json" <select id="pipelineSelect">
style="display: none;"> <option value="">Select a pipeline</option>
</div> <th:block th:each="config : ${pipelineConfigsWithNames}">
</div> <option th:value="${config.json}" th:text="${config.name}"></option>
</th:block>
<div id="pipelineContainer" class="card">
</select>
<!-- Pipeline Configuration Card Header -->
<div class="card-header">
<h2 class="card-title">Pipeline Configuration</h2>
</div> <input type="file" id="fileInput" multiple>
<button class="btn btn-primary" id="submitConfigBtn">Submit</button>
<!-- Pipeline Configuration Body -->
<div class="card-body">
<div class="mb-3"> <!-- The Modal -->
<label for="pipelineName" class="form-label">Pipeline Name</label> <div class="modal" id="pipelineSettingsModal">
<input type="text" id="pipelineName" class="form-control" placeholder="Enter pipeline name here"> <div class="modal-dialog">
</div> <div class="modal-content">
<div class="mb-3">
<select id="operationsDropdown" class="form-select"> <!-- Modal Header -->
<!-- Options will be dynamically populated here --> <div class="modal-header">
</select> <h2 class="modal-title">Pipeline Configuration</h2>
</div> <button type="button" class="close" data-dismiss="modal">&times;</button>
<div class="mb-3"> </div>
<button id="addOperationBtn" class="btn btn-primary">Add operation</button>
</div> <!-- Modal body -->
<h3>Pipeline:</h3> <div class="modal-body">
<ol id="pipelineList" class="list-group"> <div class="mb-3">
<!-- Pipeline operations will be dynamically populated here --> <label for="pipelineName" class="form-label">Pipeline Name</label>
</ol> <input type="text" id="pipelineName" class="form-control" placeholder="Enter pipeline name here">
</div> </div>
<div class="mb-3">
<input type="file" id="fileInput" multiple> <select id="operationsDropdown" class="form-select">
<!-- Options will be dynamically populated here -->
<button class="btn btn-primary" id="submitConfigBtn">Submit</button> </select>
</div>
<div class="mb-3">
</div> <button id="addOperationBtn" class="btn btn-primary">Add operation</button>
</div>
<!-- pipelineSettings modal --> <h3>Pipeline:</h3>
<div id="pipelineSettingsModal" class="modal"> <ol id="pipelineList" class="list-group">
<div class="modal-content"> <!-- Pipeline operations will be dynamically populated here -->
<div class="modal-body"> </ol>
<span class="close">&times;</span> <div id="pipelineSettingsContent">
<h2>Operation Settings</h2> <!-- pipelineSettings will be dynamically populated here -->
<div id="pipelineSettingsContent"> </div>
<!-- pipelineSettings will be dynamically populated here --> </div>
</div>
</div> <!-- Modal footer -->
</div> <div class="modal-footer">
<script src="js/pipeline.js"></script> <button id="savePipelineBtn" class="btn btn-success">Download</button>
</div> <button id="validateButton" class="btn btn-success">Validate</button>
</div> <div class="btn-group">
</div> <input type="file" id="uploadPipelineInput" accept=".json" style="display: none;">
</div> </div>
</div> </div>
<style>
.modal { </div>
display: none; /* Hidden by default */ </div>
position: fixed; /* Stay in place */ </div>
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */ <script src="js/pipeline.js"></script>
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */ </div>
overflow: auto; /* Enable scroll if needed */ </div>
background-color: rgb(0, 0, 0); /* Fallback color */ </div>
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ <style>
}
/* Modal Content */ .btn-margin {
.modal-content { margin-right: 2px;
background-color: #fefefe; }
margin: auto;
padding: 20px; </style>
border: 1px solid #888; <div th:insert="~{fragments/footer.html :: footer}"></div>
width: 50%; </div>
}
</body>
.btn-margin {
margin-right: 2px;
}
.modal-body {
display: flex;
flex-direction: column;
}
</style>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html> </html>