diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 139758bd..40c8666d 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -22,15 +22,15 @@ jobs: - name: Print diffs run: git --no-pager diff --exit-code - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'temurin' cache: maven - name: Build with Maven - run: cd ./API && mvn -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn package --file pom.xml + run: cd ./backend && mvn -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn package --file pom.xml Build_Node_js: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index f4feda32..6e766739 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ upload-dir/ .vscode/ +.quarkus/ diff --git a/API/.gitignore b/API/.gitignore deleted file mode 100644 index 31b82829..00000000 --- a/API/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -../start.ps1 -HELP.md -target/ -upload-dir/* -!.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ - diff --git a/API/.mvn/wrapper/maven-wrapper.jar b/API/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index c1dd12f1..00000000 Binary files a/API/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/API/.mvn/wrapper/maven-wrapper.properties b/API/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index b74bf7fc..00000000 --- a/API/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/API/pom.xml b/API/pom.xml deleted file mode 100644 index bdd24493..00000000 --- a/API/pom.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.0.1 - - - backend - API - 0.0.1-SNAPSHOT - API - Backend for the Baja dataviewer, for file transfer though - - 17 - - - - org.apache.commons - commons-math3 - 3.6.1 - - - com.opencsv - opencsv - 5.8 - - - commons-io - commons-io - 2.8.0 - - - org.springframework.boot - spring-boot-starter-thymeleaf - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - test - - - com.fazecast - jSerialComm - [2.0.0,3.0.0) - - - com.drewnoakes - metadata-extractor - 2.19.0 - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - \ No newline at end of file diff --git a/API/src/main/java/backend/API/FileUploadController.java b/API/src/main/java/backend/API/FileUploadController.java deleted file mode 100644 index 10ba207c..00000000 --- a/API/src/main/java/backend/API/FileUploadController.java +++ /dev/null @@ -1,550 +0,0 @@ -package backend.API; - -/* - * This is the main controller for the entire backend. - * It handles all the requests that come in from the front end - * and then sends the appropriate response back. - * This is the only class that the front end should be interacting with. - * There are quite a few moving parts, but each request should have - * 1. A unique mapping to the URL that the request is coming in on. - * This may include a variale such as {filename} - * 2. A method signature that includes the appropriate parameters - * (important ones are return type and input variables) for the request. - * - The return type should generally be a ResponseEntity, - * which may contain any object that can be converted to JSON - * - The input variables may be Path variables (such as ex/{filename}), - * Request Parameters (such as ?filename=), or Request Body (such as the file itself) - * 3. A method body that handles the request and returns the appropriate response. - * - This may include calling other methods, but it should be self-contained. - * That's all I got for explanation, the internet and - * specifically the spring boot guides through their website daeldung.com are your friend. - */ - -import backend.API.analyzer.Analyzer; -import backend.API.binary_csv.BinaryTOCSV; -import backend.API.live.Serial; -import backend.API.model.fileInformation; -import backend.API.model.fileTimespan; -import backend.API.storage.StorageFileNotFoundException; -import backend.API.storage.StorageService; -import jakarta.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -/** Controller class for handling file uploads. */ -@Controller -public class FileUploadController { - - private final StorageService storageService; - - @Autowired - public FileUploadController(StorageService storageService) { - this.storageService = storageService; - } - - // This is the method that shows the upload form page, used for debugging - - /** - * Handles GET requests to the root ("/") URL. Lists all uploaded files. - * - * @param model the Model object to pass attributes to the view - * @return the name of the view that will be used to render the response - * @throws IOException if an I/O error occurs when opening the file - */ - @GetMapping("/") - public String listUploadedFiles(Model model) throws IOException { - - model.addAttribute( - "files", - storageService - .loadAll() - .map( - path -> - MvcUriComponentsBuilder.fromMethodName( - FileUploadController.class, "serveFile", path.getFileName().toString()) - .build() - .toUri() - .toString()) - .collect(Collectors.toList())); - - return "uploadForm"; - } - - /** - * Handles GET requests to the "/files" URL. Returns a list of information about all uploaded - * files. - * - * @return a ResponseEntity with a list of fileInformation objects as the body and CORS headers - * @throws IOException if an I/O error occurs when opening the file - */ - @GetMapping("/files") - @ResponseBody - public ResponseEntity> listUploadedFiles() throws IOException { - - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - ArrayList files = new ArrayList(); - - // Get name, headers and size of each file - storageService - .loadAll() - .forEach( - path -> { - try { - // Get the path and filename of each file and print it - long size = storageService.loadAsResource(path.toString()).contentLength(); - String[] headers = storageService.readHeaders(path.toString()).split(","); - files.add(new fileInformation(path.toString().replace("\\", "/"), headers, size)); - } catch (IOException e) { - e.printStackTrace(); - } - }); - - return ResponseEntity.ok().headers(responseHeaders).body(files); - } - - /** - * Handles GET requests to the "/files/**" URL. Serves the requested file as a resource. - * - * @param request the HttpServletRequest object that contains the request made by the client - * @return a ResponseEntity with the requested file as the body and appropriate headers - */ - @GetMapping("/files/**") - @ResponseBody - public ResponseEntity serveFile(HttpServletRequest request) { - // Catch the exception if the file is not found - String path = - (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - path = path.substring("/files/".length()); - path = URLDecoder.decode(path, StandardCharsets.UTF_8); - - System.out.println("Serving file " + path); - - Resource file = storageService.loadAsResource(path); - - HttpHeaders responseHeaders = new HttpHeaders(); - - responseHeaders.add( - HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\""); - // Set these headers so that you can access from LocalHost - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - return ResponseEntity.ok().headers(responseHeaders).body(file); - } - - /** - * Handles GET requests to the "/files/folder/{foldername:.+}" URL. Returns a list of information - * about all files in the specified folder. - * - * @param foldername the name of the folder to list files from - * @return a ResponseEntity with a list of fileInformation objects as the body and CORS headers - * @throws IOException if an I/O error occurs when opening the file - */ - @GetMapping("/files/folder/{foldername:.+}") - @ResponseBody - public ResponseEntity> listFolderFiles(@PathVariable String foldername) - throws IOException { - - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - ArrayList files = new ArrayList(); - - // Get name, headers and size of each file - storageService - .loadFolder(foldername) - .forEach( - path -> { - try { - // Get the path and filename of each file and print it - long size = storageService.loadAsResource(path.toString()).contentLength(); - String[] headers = storageService.readHeaders(path.toString()).split(","); - files.add(new fileInformation(path.toString().replace("\\", "/"), headers, size)); - } catch (IOException e) { - e.printStackTrace(); - } - }); - - return ResponseEntity.ok().headers(responseHeaders).body(files); - } - - /** - * Handles GET requests to the "/timespan/folder/{foldername:.+}" URL. Returns a list of time - * spans for all files in the specified folder. - * - * @param foldername the name of the folder to list files from - * @return a ResponseEntity with a list of fileTimespan objects as the body and CORS headers - * @throws IOException if an I/O error occurs when opening the file - */ - @GetMapping("/timespan/folder/{foldername:.+}") - @ResponseBody - public ResponseEntity> listFolderTimespans( - @PathVariable String foldername) throws IOException { - - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - ArrayList timespans = new ArrayList(); - - Stream paths = storageService.loadFolder(foldername); - - switch (foldername) { - case "csv": - // Holds the parent folder and the zero time - Object[] container = {null, null}; - paths.forEach( - path -> { - Path parent = path.getParent(); - if (parent != null) { - if (storageService.canComputeTimespan(parent.toString())) { - // Updates the parent folder and zero time if the parent folder changes to avoid - // recalculating the zero time - if (container[0] != parent) { - container[0] = parent; - container[1] = storageService.getZeroTime((Path) container[0]); - } - // Get the path and filename of each file and print it - LocalDateTime[] timespan = - storageService.getTimespan(path.toString(), (LocalDateTime) container[1]); - timespans.add( - new fileTimespan( - path.toString().replace("\\", "/"), timespan[0], timespan[1])); - } - } - }); - break; - case "mp4": - paths.forEach( - path -> { - // Get the path and filename of each file and print it - LocalDateTime[] timespan = storageService.getTimespan(path.toString()); - timespans.add( - new fileTimespan(path.toString().replace("\\", "/"), timespan[0], timespan[1])); - }); - break; - default: - throw new IllegalArgumentException("Invalid folder name"); - } - - return ResponseEntity.ok().headers(responseHeaders).body(timespans); - } - - /** - * Handles GET requests to the "/analyze" URL. Performs analysis on the specified input files and - * returns the result as a resource. - * - * @param inputFiles the input files to analyze - * @param inputColumns the input columns to analyze - * @param outputFiles the output files to write the results to - * @param analyzer the analyzer to use - * @param liveOptions options for live analysis - * @return a ResponseEntity with the result file as the body and appropriate headers - * @throws InterruptedException if the analysis is interrupted - */ - @GetMapping("/analyze") - @ResponseBody - public ResponseEntity handleFileRequest( - @RequestParam(value = "inputFiles", required = true) String[] inputFiles, - @RequestParam(value = "inputColumns", required = true) String[] inputColumns, - @RequestParam(value = "outputFiles", required = false) String[] outputFiles, - @RequestParam(value = "analyzer", required = false) String[] analyzer, - @RequestParam(value = "liveOptions", required = false) String[] liveOptions) - throws InterruptedException { - - // Catch exceptions first - if (inputFiles == null || inputFiles.length == 0) { - throw new IllegalArgumentException("No input files selected"); - } - - // If no output files are selected, give it a single - if (outputFiles == null || outputFiles.length == 0) { - // Set output files to empty string - outputFiles = new String[10]; - } - - // For all of the input and output files, add the root location to the front - for (int i = 0; i < inputFiles.length; i++) { - inputFiles[i] = - storageService.getRootLocation().toString() - + "/" - + storageService.getTypeFolder(inputFiles[i]) - + "/" - + inputFiles[i]; - } - for (int i = 0; i < outputFiles.length; i++) { - outputFiles[i] = - storageService.getRootLocation().toString() - + "/" - + storageService.getTypeFolder(outputFiles[i]) - + "/" - + outputFiles[i]; - } - - // Then run the selected analyzer - if (analyzer != null && analyzer.length != 0 && analyzer[0] != null) { - try { - Analyzer.createAnalyzer( - analyzer[0], - inputFiles, - inputColumns, - outputFiles, - (Object[]) Arrays.copyOfRange(analyzer, 1, analyzer.length)) - .analyze(); - } catch (Exception e) { - System.out.println(e); - } - } else { - // If no analyzer is selected, only one file is selected, copy it - // storageService.copyFile(inputFiles[0], outputFiles[outputFiles.length - 1]); - outputFiles[outputFiles.length - 1] = inputFiles[0]; - } - - // TODO: THIS SHOULD HAPPEN BEFORE RUNNING THE ANALYZER IN THE COMMON CASE - // Then check if live is true, and set the options + files accordingly - String fileOutputString = outputFiles[outputFiles.length - 1].substring(13); - - // print live options - System.out.println("Live options: " + liveOptions[0]); - - if (liveOptions[0].equals("true")) { - outputFiles = new String[10]; - // When live is true, we only want a certain amount of time from its timestamp - // Get the last timestamp, then subtract a certain amount of time, and use split analyzer - // between the two - int lastPoint = Integer.valueOf(storageService.getLast(fileOutputString)); - int firstPoint = Math.max(0, lastPoint - 3000); - - // print the two values - System.out.println("First point: " + firstPoint); - System.out.println("Last point: " + lastPoint); - - Object[] extraValues = new Object[] {String.valueOf(firstPoint), String.valueOf(lastPoint)}; - String[] lastFile = new String[] {fileOutputString}; - - try { - Analyzer.createAnalyzer("split", lastFile, inputColumns, outputFiles, extraValues) - .analyze(); - } catch (Exception e) { - System.out.println(e); - } - } - - // Then return the final file, removing the prefix for upload dir - String filePath = outputFiles[outputFiles.length - 1]; - Path path = Paths.get(filePath); - Path newPath = path.subpath(2, path.getNameCount()); - - // Set these headers so that you can access from LocalHost and download the file - HttpHeaders responseHeaders = new HttpHeaders(); - Path absoluteFilePath = storageService.load(newPath.toString()); - String relativePath = storageService.getRootLocation().relativize(absoluteFilePath).toString(); - responseHeaders.add( - HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + relativePath + "\""); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Disposition"); - - Resource file = storageService.loadAsResource(newPath.toString()); - - return ResponseEntity.ok().headers(responseHeaders).body(file); - } - - /** - * Handles GET requests to the "/deleteAll" URL. Deletes all files from the storage and then - * re-initializes it. - * - * @return a ResponseEntity with a confirmation message as the body and CORS headers - */ - @GetMapping("/deleteAll") - @ResponseBody - public ResponseEntity deleteAll() { - storageService.deleteAll(); - storageService.init(); - - HttpHeaders responseHeaders = new HttpHeaders(); - // Set these headers so that you can access from LocalHost - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - return ResponseEntity.ok().headers(responseHeaders).body("All files deleted"); - } - - /** - * Handles GET requests to the "/files/maxmin/**" URL. Returns the maximum and minimum values of a - * specified header in the requested file. - * - * @param request the HttpServletRequest object that contains the request made by the client - * @param headerName the name of the header to get the maximum and minimum values of - * @return ResponseEntity with the maximum and minimum values as the body and appropriate headers - * @throws IOException if an I/O error occurs when opening the file - */ - @GetMapping("/files/maxmin/**") - @ResponseBody - public ResponseEntity getMaxMin( - HttpServletRequest request, - @RequestParam(value = "headerName", required = true) String headerName) - throws IOException { - - String filename = - (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - filename = filename.substring("/files/maxmin/csv/".length()); - // Decode to add spaces back in and special characters - filename = URLDecoder.decode(filename, StandardCharsets.UTF_8); - - System.out.println("Getting max and min for " + headerName + " in " + filename); - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - // Get size, headers, datetime, etc. - String maxmin = storageService.getMaxMin(filename, headerName); - - return ResponseEntity.ok().headers(responseHeaders).body(maxmin); - } - - /** - * Handles POST requests to the "/" URL. Uploads a file to the server and, if the file is a binary - * file, converts it to CSV format. This can probably be deleted, only for the 8080 endpoint - * rather than frontend. - * - * @param file the file to upload - * @param redirectAttributes the attributes to add to the redirect - * @return a redirect instruction to the "/" URL - * @throws IllegalArgumentException if no file was uploaded - */ - @PostMapping("/") - public String handleFileUpload( - @RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { - - String filename = file.getOriginalFilename(); - if (filename == null) { - throw new IllegalArgumentException("No file selected"); - } - if (filename.substring(filename.lastIndexOf(".") + 1).equals("bin")) { - storageService.store(file); - String csvFilename = storageService.load(filename).toAbsolutePath().toString(); - String csvOutputDir = storageService.load("").toAbsolutePath() + "\\"; - BinaryTOCSV.toCSV(csvFilename, csvOutputDir, false); - storageService.delete(filename); - } else { - storageService.store(file); - } - - redirectAttributes.addFlashAttribute( - "message", "You successfully uploaded " + file.getOriginalFilename() + "!"); - - return "redirect:/"; - } - - /** - * Handles POST requests to the "/upload" URL. Uploads a file to the server, and if the file is a - * binary file, converts it to CSV format. If the file is a MOV file, copies it to an MP4 file. - * - * @param file the file to upload - * @return a ResponseEntity with a success message as the body and appropriate headers - * @throws IllegalArgumentException if no file was uploaded - */ - @PostMapping("/upload") - public ResponseEntity handleFileUploadApi(@RequestParam("file") MultipartFile file) { - - // Check type of file, either CSV or bin - String filename = file.getOriginalFilename(); - if (filename == null) { - throw new IllegalArgumentException("No file selected"); - } - if (filename.substring(filename.lastIndexOf(".") + 1).equals("bin")) { - storageService.store(file); - BinaryTOCSV.toCSV( - storageService.load(filename).toAbsolutePath().toString(), - storageService.load("").toAbsolutePath() + "\\", - true); - storageService.delete(filename); - } else if (filename.substring(filename.lastIndexOf(".") + 1).equalsIgnoreCase("mov")) { - storageService.store(file); - storageService.copyFile(filename, filename.substring(0, filename.lastIndexOf(".")) + ".mp4"); - storageService.delete(filename); - } else { - storageService.store(file); - } - - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - return ResponseEntity.ok() - .headers(responseHeaders) - .body(String.format("%s uploaded", file.getOriginalFilename())); - } - - /** - * Handles POST requests to the "/live" URL. Starts the live data collection from a specified - * port. - * - * @param port the port to start the live data collection from - * @return a ResponseEntity with a message indicating that the live data collection has started - */ - @PostMapping("/live") - public ResponseEntity handleLive( - @RequestParam(name = "port", required = false) String port) { - // Start the live data collection - - // call the readLive function in Serial.java - if (!Serial.exit) { - Serial.exit = true; - } else { - new Thread( - () -> { - Serial.readLive(); - }) - .start(); - } - - // Set these headers so that you can access from LocalHost - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - - return ResponseEntity.ok() - .headers(responseHeaders) - .body(String.format("Live data collection started on port %s", port)); - } - - @ExceptionHandler(StorageFileNotFoundException.class) - public ResponseEntity handleStorageFileNotFound(StorageFileNotFoundException exc) { - return ResponseEntity.notFound().build(); - } -} diff --git a/API/src/main/java/backend/API/MasterExceptionHandler.java b/API/src/main/java/backend/API/MasterExceptionHandler.java deleted file mode 100644 index 29db715e..00000000 --- a/API/src/main/java/backend/API/MasterExceptionHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -package backend.API; - -import org.apache.commons.math3.exception.NotStrictlyPositiveException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -/** - * This class extends ResponseEntityExceptionHandler to provide custom handling for specific - * exceptions. It is annotated with @ControllerAdvice to make it applicable to all controllers in - * the application. - */ -@ControllerAdvice -public class MasterExceptionHandler extends ResponseEntityExceptionHandler { - - /** - * Handles ArrayIndexOutOfBoundsExceptions, which are frequently caused by faulty CSV files. - * - * @param ex the exception that was thrown - * @param request the current web request - * @return a ResponseEntity with an error message as the body and a CONFLICT status code - */ - @ExceptionHandler(value = {ArrayIndexOutOfBoundsException.class}) - protected ResponseEntity handleBadCsv(RuntimeException ex, WebRequest request) { - String bodyOfResponse = - "Error: " - + ex.getClass().getCanonicalName() - + " → " - + ex.getMessage() - + ".\nMessage: This is most likely caused by a faulty CSV file. " - + "Make sure to check for any empty cells, trailing whitespaces or \\n characters."; - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - return handleExceptionInternal( - ex, bodyOfResponse, responseHeaders, HttpStatus.CONFLICT, request); - } - - /** - * Handles NumberFormatExceptions, NotStrictlyPositiveExceptions, and IndexOutOfBoundsExceptions, - * which are often caused by issues with analyzer options. - * - * @param ex the exception that was thrown - * @param request the current web request - * @return a ResponseEntity with an error message and a CONFLICT status code - */ - @ExceptionHandler( - value = { - NumberFormatException.class, - NotStrictlyPositiveException.class, - IndexOutOfBoundsException.class - }) - protected ResponseEntity handleBadAnalyzerOption( - RuntimeException ex, WebRequest request) { - String bodyOfResponse = - "Error: " - + ex.getClass().getCanonicalName() - + " → " - + ex.getMessage() - + ".\nMessage: Make sure to check your analyzer " - + "options before submitting your request."; - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - responseHeaders.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - return handleExceptionInternal( - ex, bodyOfResponse, responseHeaders, HttpStatus.CONFLICT, request); - } -} diff --git a/API/src/main/java/backend/API/UploadingFilesApplication.java b/API/src/main/java/backend/API/UploadingFilesApplication.java deleted file mode 100644 index 5622f5b5..00000000 --- a/API/src/main/java/backend/API/UploadingFilesApplication.java +++ /dev/null @@ -1,43 +0,0 @@ -package backend.API; - -import backend.API.storage.StorageProperties; -import backend.API.storage.StorageService; -import org.springframework.boot.CommandLineRunner; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; - -/** - * This is the main class for the UploadingFiles application. Its annotated - * with @SpringBootApplication to indicate that it's a Spring Boot application. Its annotated - * with @EnableConfigurationProperties to enable support for configuration properties. - */ -@SpringBootApplication -@EnableConfigurationProperties(StorageProperties.class) -public class UploadingFilesApplication { - - /** - * The main method of the application. It starts the Spring Boot application. - * - * @param args command line arguments - */ - public static void main(String[] args) { - SpringApplication.run(UploadingFilesApplication.class, args); - } - - /** - * Annotated with @Bean to indicate that it should be managed by the Spring framework. It returns - * a CommandLineRunner that deletes all files from the storage and then initializes it. - * - * @param storageService the storage service to use - * @return a CommandLineRunner that deletes all files from the storage and then initializes it - */ - @Bean - CommandLineRunner init(StorageService storageService) { - return (args) -> { - storageService.deleteAll(); - storageService.init(); - }; - } -} diff --git a/API/src/main/java/backend/API/analyzer/Analyzer.java b/API/src/main/java/backend/API/analyzer/Analyzer.java deleted file mode 100644 index 2a77c9d4..00000000 --- a/API/src/main/java/backend/API/analyzer/Analyzer.java +++ /dev/null @@ -1,250 +0,0 @@ -package backend.API.analyzer; - -import com.opencsv.CSVReader; -import com.opencsv.CSVReaderBuilder; -import com.opencsv.CSVWriter; -import com.opencsv.CSVWriterBuilder; -import com.opencsv.ICSVWriter; -import com.opencsv.exceptions.CsvValidationException; -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.List; - -public abstract class Analyzer { - - // Input and output files are arrays because some analyzers may need multiple input files - protected String[] inputFiles; - protected String[] inputColumns; - protected String[] outputFiles; - - public Analyzer(String[] inputFiles, String[] inputColumns, String[] outputFiles) { - this.inputFiles = inputFiles; - // inputColumns is the names of the columns we are analyzing. index 0 is the independent - // variable (usually timestamp), 1+ are dependent variable(s) - this.inputColumns = inputColumns; - this.outputFiles = outputFiles; - } - - // Some analyzers work on entire rows and don't need to select columns (e.g. compression), they - // should use this constructor - public Analyzer(String[] inputFiles, String[] outputFiles) { - this.inputFiles = inputFiles; - this.inputColumns = new String[1]; - this.outputFiles = outputFiles; - } - - // Abstract method to be implemented by subclasses - public abstract void analyze() throws IOException, CsvValidationException; - - // I/O methods - // Streams as they avoid loading the entire file into memory at once - public CSVReader getReader(String filePath) throws IOException { - FileReader fileReader = new FileReader(filePath); - BufferedReader bufferedReader = new BufferedReader(fileReader); - return new CSVReaderBuilder(bufferedReader) - .withSkipLines(0) // Skip header if needed - .build(); - } - - public ICSVWriter getWriter(String filePath) throws IOException { - FileWriter fileWriter = new FileWriter(filePath); - BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); - return new CSVWriterBuilder(bufferedWriter) - .withSeparator(CSVWriter.DEFAULT_SEPARATOR) - .withQuoteChar(CSVWriter.NO_QUOTE_CHARACTER) - .withEscapeChar(CSVWriter.DEFAULT_ESCAPE_CHARACTER) - .withLineEnd(CSVWriter.DEFAULT_LINE_END) - .build(); - } - - // Factory method allows creation of different types of analyzers without having to change the - // code that calls it - // When a new analyzer is created, add it to this factory method - public static Analyzer createAnalyzer( - String type, - String[] inputFiles, - String[] inputColumns, - String[] outputFiles, - Object... params) { - // Before every input and output file location, add the storage directory before it - switch (type) { - case "accelCurve": - if (outputFiles.length == 10) { - // Concept here is when no output files are provided to format automatically, the last one - // is always used as the final output - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_roll.csv"; - outputFiles[1] = inputFiles[1].substring(0, inputFiles[1].length() - 4) + "_roll.csv"; - outputFiles[2] = - inputFiles[0].substring(0, inputFiles[0].length() - 4) - + "_inter_" - + inputFiles[1].substring(13, inputFiles[1].length() - 4).replace("/", "") - + ".csv"; - outputFiles[9] = outputFiles[2]; - } - return new AccelCurveAnalyzer(inputFiles, inputColumns, outputFiles); - case "rollAvg": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_roll.csv"; - outputFiles[9] = outputFiles[0]; - } - // Check if passed a window size - if (params.length == 0) { - return new RollingAvgAnalyzer(inputFiles, inputColumns, outputFiles); - } - int windowSize = Integer.parseInt((String) params[0]); - return new RollingAvgAnalyzer(inputFiles, inputColumns, outputFiles, windowSize); - case "sGolay": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_sgolay.csv"; - outputFiles[9] = outputFiles[0]; - } - // Check if passed a window size - if (params.length == 0) { - return new SGolayFilter(inputFiles, inputColumns, outputFiles); - } - windowSize = Integer.parseInt((String) params[0]); - int polynomialDegree = Integer.parseInt((String) params[1]); - return new SGolayFilter( - inputFiles, inputColumns, outputFiles, windowSize, polynomialDegree); - case "linearInterpolate": - if (outputFiles.length == 10) { - outputFiles[0] = - inputFiles[0].substring(0, inputFiles[0].length() - 4) - + "_inter_" - + inputFiles[1].substring(13, inputFiles[1].length() - 4).replace("/", "") - + ".csv"; - outputFiles[9] = outputFiles[0]; - } - // We use timestamp and inputColumns[1] as the inputColumns because we don't - // actually care which column in the first file we're interpolating with, we'll just - // add every column from the first file to the new file - // Print all input columns - return new LinearInterpolaterAnalyzer( - inputFiles, new String[] {"Timestamp (ms)", inputColumns[1]}, outputFiles); - case "RDPCompression": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_rdp.csv"; - outputFiles[9] = outputFiles[0]; - } - if (params.length == 0) { - return new RDPCompressionAnalyzer(inputFiles, outputFiles, 15); - } - double epsilon = Double.parseDouble((String) params[0]); - return new RDPCompressionAnalyzer(inputFiles, outputFiles, epsilon); - - case "split": - System.out.println("SplitAnalyzer"); - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_split.csv"; - outputFiles[9] = outputFiles[0]; - } - if (params[1] == "" || params[0] == "") { - return null; - } - int start = Integer.parseInt((String) params[0]); - int end = Integer.parseInt((String) params[1]); - return new SplitAnalyzer(inputFiles, inputColumns, outputFiles, start, end); - case "linearMultiply": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_mult.csv"; - outputFiles[9] = outputFiles[0]; - } - if (params[1] == "" || params[0] == "") { - return null; - } - double m = Double.parseDouble((String) params[0]); - double b = Double.parseDouble((String) params[1]); - return new LinearMultiplyAnalyzer(inputFiles, inputColumns, outputFiles, m, b); - - case "average": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_avg.csv"; - outputFiles[9] = outputFiles[0]; - } - int[] range = new int[2]; - range[0] = Integer.parseInt((String) params[0]); - range[1] = Integer.parseInt((String) params[1]); - return new AverageAnalyzer(inputFiles, outputFiles, range); - - case "interpolaterPro": - if (outputFiles.length == 10) { - // For each file, add to the output string - StringBuilder outputString = - new StringBuilder(inputFiles[0].substring(0, inputFiles[0].lastIndexOf("/") + 1)); - for (String inputFile : inputFiles) { - outputString - .append(inputFile, inputFile.lastIndexOf("/") + 1, inputFile.length() - 4) - .append("_"); - } - outputFiles[0] = outputString + "inter.csv"; - outputFiles[9] = outputFiles[0]; - } - return new InterpolaterProAnalyzer(inputFiles, inputColumns, outputFiles); - case "cubic": - if (outputFiles.length == 10) { - outputFiles[0] = inputFiles[0].substring(0, inputFiles[0].length() - 4) + "_cubic.csv"; - outputFiles[9] = outputFiles[0]; - } - double a = Double.parseDouble((String) params[0]); - double b1 = Double.parseDouble((String) params[1]); - double c = Double.parseDouble((String) params[2]); - double d = Double.parseDouble((String) params[3]); - return new CubicAnalyzer(inputFiles, inputColumns, outputFiles, a, b1, c, d); - default: - return null; - } - } - - // From this list of headers, which one are we actually doing analysis on - // fileIndex is basically the axis, 0=X, 1=Y, I made it a int to future-proof adding new columns - public int getAnalysisColumnIndex(int fileIndex, List fileHeaders) - throws RuntimeException { - for (int i = 0; i < fileHeaders.size(); i++) { - if (fileHeaders.get(i).trim().equals(this.inputColumns[fileIndex])) { - return i; - } - } - // The inputColum is wrong somehow, should never happen with working frontend - throw new RuntimeException("No column in file exists with analysis column name"); - } - - public int getColumnIndex(String columnName, String[] fileHeaders) throws RuntimeException { - for (int i = 0; i < fileHeaders.length; i++) { - if (fileHeaders[i].trim().equals(columnName)) { - return i; - } - } - throw new RuntimeException("No column in file exists with analysis column name"); - } - - public static void main(String[] args) { - // Test the factory method - // System.out.println("Hello"); - // String[] inputFiles = - // {"X:\\Code\\Projects\\Baja\\Better-Data-Viewer\\data\\live_F_RPM_PRIM.csv"}; - // String[] outputFiles = {"output.csv"}; - // Analyzer.createAnalyzer("rollingAvg", inputFiles, outputFiles, 30).analyze(); - - // // Now test AccelCurveAnalyzer - // String[] inputFiles2 = - // {"X:\\Code\\Projects\\Baja\\Better-Data-Viewer\\data\\live_F_RPM_PRIM.csv", - // "X:\\Code\\Projects\\Baja\\Better-Data-Viewer\\data\\live_F_RPM_SEC.csv"}; - // String[] outputFiles2 = {"prim_average.csv", "sec_average.csv", "interpolate.csv", - // "accel_curve.csv"}; - // Analyzer.createAnalyzer("accelCurve", inputFiles2, outputFiles2).analyze(); - // System.out.println("Hello"); - - // String[] inputFiles3 = {"X:/Code/Projects/Baja/Better-Data-Viewer/data/temp.csv"}; - // String[] outputFiles3 = {"X:/Code/Projects/Baja/Better-Data-Viewer/data/temp2.csv"}; - // System.out.println("Hello"); - - // String[] range = new String[2]; - // range[0] = "0"; - // range[1] = "100"; - - // Analyzer.createAnalyzer("average", inputFiles3, outputFiles3, range).analyze(); - } -} diff --git a/API/src/main/java/backend/API/binary_csv/BinaryTOCSV.java b/API/src/main/java/backend/API/binary_csv/BinaryTOCSV.java deleted file mode 100644 index 447178d4..00000000 --- a/API/src/main/java/backend/API/binary_csv/BinaryTOCSV.java +++ /dev/null @@ -1,26 +0,0 @@ -package backend.API.binary_csv; - -import java.nio.file.Paths; - -public class BinaryTOCSV { - - public static native void toCSV(String filename, String outputDir, boolean folder); - - private static final String relativePath = "/src/main/java/backend/API/binary_csv/"; - - static { - String path = System.getProperty("user.dir"); - path += relativePath + "/binary_to_csv_lib.dll"; - System.out.println("PATH " + path); - System.load(path); - } - - public static void main(String[] args) { - System.out.println(Paths.get("upload-dir").toAbsolutePath() + "\\"); - toCSV( - Paths.get(relativePath + "/151408.bin").toAbsolutePath().toString(), - Paths.get("API/upload-dir").toAbsolutePath() + "\\", - false); - System.out.println("Done"); - } -} diff --git a/API/src/main/java/backend/API/binary_csv/backend_API_binary_csv_BinaryTOCSV.h b/API/src/main/java/backend/API/binary_csv/backend_API_binary_csv_BinaryTOCSV.h deleted file mode 100644 index 48e737f0..00000000 --- a/API/src/main/java/backend/API/binary_csv/backend_API_binary_csv_BinaryTOCSV.h +++ /dev/null @@ -1,21 +0,0 @@ -/* DO NOT EDIT THIS FILE - it is machine generated */ -#include -/* Header for class backend_API_binary_csv_BinaryTOCSV */ - -#ifndef _Included_backend_API_binary_csv_BinaryTOCSV -#define _Included_backend_API_binary_csv_BinaryTOCSV -#ifdef __cplusplus -extern "C" { -#endif -/* - * Class: backend_API_binary_csv_BinaryTOCSV - * Method: toCSV - * Signature: (Ljava/lang/String;Ljava/lang/String;Z)V - */ -JNIEXPORT void JNICALL Java_backend_API_binary_1csv_BinaryTOCSV_toCSV - (JNIEnv *, jclass, jstring, jstring, jboolean); - -#ifdef __cplusplus -} -#endif -#endif diff --git a/API/src/main/java/backend/API/binary_csv/binary_to_csv_lib.dll b/API/src/main/java/backend/API/binary_csv/binary_to_csv_lib.dll deleted file mode 100644 index a2cc6993..00000000 Binary files a/API/src/main/java/backend/API/binary_csv/binary_to_csv_lib.dll and /dev/null differ diff --git a/API/src/main/java/backend/API/model/fileInformation.java b/API/src/main/java/backend/API/model/fileInformation.java deleted file mode 100644 index c12f8fde..00000000 --- a/API/src/main/java/backend/API/model/fileInformation.java +++ /dev/null @@ -1,25 +0,0 @@ -package backend.API.model; - -// This class represents the data structure that is used to send information about the file through -// to the front end -// In order to send information, there must be either public getters or public variables -public class fileInformation { - - public String key; - public String[] fileHeaders; - public long size; - - public fileInformation(String key, String[] fileHeaders, long size) { - this.key = key; - this.fileHeaders = fileHeaders; - this.size = size; - } - - public String toString() { - StringBuilder headers = new StringBuilder(); - for (String fileHeader : fileHeaders) { - headers.append(fileHeader).append(", "); - } - return "File Name: " + key + "\nFile Headers: " + headers + "\nFile Size: " + size; - } -} diff --git a/API/src/main/java/backend/API/model/fileTimespan.java b/API/src/main/java/backend/API/model/fileTimespan.java deleted file mode 100644 index 2e90b5d8..00000000 --- a/API/src/main/java/backend/API/model/fileTimespan.java +++ /dev/null @@ -1,25 +0,0 @@ -package backend.API.model; - -import java.time.LocalDateTime; - -public class fileTimespan { - - public String key; - public LocalDateTime start; - public LocalDateTime end; - - public fileTimespan(String key, LocalDateTime start, LocalDateTime end) { - this.key = key; - this.start = start; - this.end = end; - } - - public String toString() { - return "File Name: " - + key - + "\nStart Date: " - + start.toString() - + "\nEnd Date: " - + end.toString(); - } -} diff --git a/API/src/main/java/backend/API/storage/FileSystemStorageService.java b/API/src/main/java/backend/API/storage/FileSystemStorageService.java deleted file mode 100644 index 78b71494..00000000 --- a/API/src/main/java/backend/API/storage/FileSystemStorageService.java +++ /dev/null @@ -1,402 +0,0 @@ -// Author: Kai Arseneau -// This code is a file storage service implemented in Java using the Spring framework. -// The service implements the StorageService interface and uses the file system to store and -// retrieve files. -// The root location of the files is specified in a StorageProperties class that is passed to the -// constructor. -// The service provides methods to store, load, and delete files, as well as to read the headers of -// a file. -// Exceptions are thrown in the case of errors, such as IOException or StorageException, specified -// in the StorageException class. - -package backend.API.storage; - -import com.drew.imaging.mp4.Mp4MetadataReader; -import com.drew.metadata.Tag; -import com.drew.metadata.mp4.Mp4Directory; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.DoubleSummaryStatistics; -import java.util.Locale; -import java.util.stream.Stream; -import org.apache.commons.io.input.ReversedLinesFileReader; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; -import org.springframework.stereotype.Service; -import org.springframework.util.FileSystemUtils; -import org.springframework.web.multipart.MultipartFile; - -@Service -public class FileSystemStorageService implements StorageService { - - private final Path rootLocation; - - public Path getRootLocation() { - return this.rootLocation; - } - - @Autowired - public FileSystemStorageService(StorageProperties properties) { - - if (properties.getLocation().trim().isEmpty()) { - throw new StorageException("File upload location can not be Empty."); - } - - this.rootLocation = Paths.get(properties.getLocation()); - } - - @Override - public void store(MultipartFile file) { - try { - if (file.isEmpty()) { - throw new StorageException("Failed to store empty file."); - } - Path destinationFile = - this.rootLocation - .resolve( - Paths.get(getTypeFolder(file.getOriginalFilename()), file.getOriginalFilename())) - .normalize() - .toAbsolutePath(); - if (!destinationFile.startsWith(this.rootLocation.toAbsolutePath())) { - // This is a security check - throw new StorageException("Cannot store file outside current directory."); - } - try (InputStream inputStream = file.getInputStream()) { - Files.copy(inputStream, destinationFile, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw new StorageException("Failed to store file.", e); - } - } - - @Override - public Stream loadAll() { - try { - return Files.walk(this.rootLocation, 3) // optional depth parameter - .filter(path -> !Files.isDirectory(path)) - .map(this.rootLocation::relativize); - - } catch (IOException e) { - throw new StorageException("Failed to read stored files", e); - } - } - - @Override - public Stream loadFolder(String foldername) { - try { - Path folder = this.rootLocation.resolve(foldername); - return Files.walk(folder, 2) // optional depth parameter - .filter(path -> !Files.isDirectory(path)) - .map(folder::relativize); - - } catch (IOException e) { - throw new StorageException("Failed to read stored files", e); - } - } - - @Override - public Path load(String filename) { - return rootLocation.resolve(getTypeFolder(filename) + "/" + filename); - } - - @Override - public Resource loadAsResource(String filename) { - try { - Path file = load(filename); - Resource resource = new UrlResource(file.toUri()); - if (resource.exists() || resource.isReadable()) { - return resource; - } else { - throw new StorageFileNotFoundException("Could not read file: " + filename); - } - } catch (MalformedURLException e) { - throw new StorageFileNotFoundException("Could not read file: " + filename, e); - } - } - - @Override - public void deleteAll() { - FileSystemUtils.deleteRecursively(rootLocation.toFile()); - } - - @Override - public void init() { - try { - Files.createDirectories(rootLocation); - // Create sub folders for csv and mp4 files - Files.createDirectories(rootLocation.resolve("csv")); - Files.createDirectories(rootLocation.resolve("mp4")); - } catch (IOException e) { - throw new StorageException("Could not initialize storage", e); - } - } - - @Override - public String readHeaders(String filename) { - // Basically read the first line of the file and return it - try { - Path file = load(filename); - String headers; - if (getTypeFolder(filename).equals("mp4")) { - headers = extractMetadata(file); - } else { - headers = Files.lines(file).findFirst().get(); - } - return headers; - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - @Override - public void delete(String filename) { - try { - Path file = load(filename); - Files.delete(file); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void copyFile(String filename, String newFilename) { - try { - Path file = load(filename); - Path newFile = load(newFilename); - Files.copy(file, newFile, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public String getMaxMin(String filename, String headerName) { - // Find the maximum and minimum values in the file for a given column - try { - // First find the index of the column - String[] headerArray = readHeaders(filename).split(","); - int index = -1; - - for (int i = 0; i < headerArray.length; i++) { - if (headerArray[i].equals(headerName)) { - index = i; - break; - } - } - - if (index == -1) { - return null; - } - - // Now find the max and min values - - Path file = load(filename); - final int finalIndex = index; - try (Stream lines = Files.lines(file)) { - DoubleSummaryStatistics stats = - lines - .skip(1) // Skip the header line - .map(line -> line.split(",")[finalIndex]) - .mapToDouble(Double::parseDouble) - .summaryStatistics(); - - double min = stats.getMin(); - double max = stats.getMax(); - - return min + "," + max; - } - - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - @Override - public String getLast(String filename) { - - String timestamp; - - try { - ReversedLinesFileReader reverseReader = - new ReversedLinesFileReader(load(filename), StandardCharsets.UTF_8); - timestamp = reverseReader.readLine().split(",")[0]; - reverseReader.close(); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - - return timestamp; - } - - @Override - public Boolean canComputeTimespan(String foldername) { - // Checks if the folder has the necessary files to compute the timespan - try { - Path folder = rootLocation.resolve("csv/" + foldername); - return Files.exists(folder.resolve("GPS SECOND MINUTE HOUR.csv")) - && Files.exists(folder.resolve("GPS DAY MONTH YEAR.csv")); - } catch (Exception e) { - e.printStackTrace(); - return false; - } - } - - // Get timspan for mp4 file from metadata - @Override - public LocalDateTime[] getTimespan(String filename) { - // Gets the metadata of the file to find the creation time and duration - String metadata = extractMetadata(load(filename)); - - // Parses with timezeone, converts to GMT, and then to LocalDateTime - assert metadata != null; - LocalDateTime creationTime = - ZonedDateTime.parse( - getTagValue(metadata, "Creation Time"), - DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ENGLISH)) - .withZoneSameInstant(ZoneId.of("GMT")) - .toLocalDateTime(); - - // Below calculation gives a better estimate than Duration in Seconds tag - // Each is converted to nanoseconds and then divided to preserve precision - long duration = - (Long.parseLong(getTagValue(metadata, "Duration")) * 1_000_000_000) - / (Long.parseLong(getTagValue(metadata, "Media Time Scale")) * 1_000_000_000); - - // Returns the start and end times as strings in GMT with milliseconds - return new LocalDateTime[] {creationTime, creationTime.plusSeconds(duration)}; - } - - // Get time span for csv file from a parsed bin file - @Override - public LocalDateTime[] getTimespan(String filename, LocalDateTime zeroTime) { - String timestamp1 = null; - String timestamp2 = null; - try { - BufferedReader reader = new BufferedReader(Files.newBufferedReader(load(filename))); - timestamp1 = reader.lines().skip(1).findFirst().orElseThrow().split(",")[0]; - reader.close(); - ReversedLinesFileReader reverseReader = - new ReversedLinesFileReader(load(filename), StandardCharsets.UTF_8); - timestamp2 = reverseReader.readLine().split(",")[0]; - reverseReader.close(); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - - LocalDateTime startTime = zeroTime.plusNanos((long) Double.parseDouble(timestamp1) * 1_000_000); - LocalDateTime endTime = zeroTime.plusNanos((long) Double.parseDouble(timestamp2) * 1_000_000); - - return new LocalDateTime[] {startTime, endTime}; - } - - // Returns the DateTime of the folder at 0 timestamp - // Only works for converted bins from the DAQ Box - public LocalDateTime getZeroTime(Path folder) { - try { - // Get the values of the first line from the gps files ingoring the header - String[] smhArray = - Files.lines( - rootLocation.resolve("csv/" + folder.toString() + "/GPS SECOND MINUTE HOUR.csv")) - .skip(1) - .findFirst() - .orElseThrow() - .split(","); - String[] dmyArray = - Files.lines(rootLocation.resolve("csv/" + folder + "/GPS DAY MONTH YEAR.csv")) - .skip(1) - .findFirst() - .orElseThrow() - .split(","); - - // Convert the values to a LocalDateTime and subtract the timestamp - long timestamp = Long.parseLong(smhArray[0]); - LocalDateTime zeroTime = - LocalDateTime.of( - 2000 + Integer.parseInt(dmyArray[3]), - Integer.parseInt(dmyArray[2]), - Integer.parseInt(dmyArray[1]), - Integer.parseInt(smhArray[3]), - Integer.parseInt(smhArray[2]), - Integer.parseInt(smhArray[1])) - .minusNanos(timestamp * 1_000_000); - - return zeroTime; - } catch (IOException e) { - e.printStackTrace(); - return null; - } - } - - // Returns the extension of the file for folder organization - @Override - public String getTypeFolder(String filename) { - if (filename == null) { - return ""; // No file - } - int dotIndex = filename.lastIndexOf("."); - if (dotIndex == -1) { - return ""; // No extension - } - String extension = filename.substring(dotIndex + 1).toLowerCase(); - // Returns csv for bin and mp4 for mov for file conversion - switch (extension) { - case "bin": - return "csv"; - case "mov": - return "mp4"; - default: - return extension; - } - } - - // Returns all the metadata in the file as string with commas between each value - // Each value will be in the format "key - value" - private String extractMetadata(Path file) { - try { - // Gets all the metadata from the file in the form of a directory - Mp4Directory metadata = - Mp4MetadataReader.readMetadata(file.toFile()).getFirstDirectoryOfType(Mp4Directory.class); - - // Extracts all the key value pairs - String metadataString = ""; - for (Tag tag : metadata.getTags()) { - metadataString += tag.toString() + ","; - } - return metadataString; - - } catch (IOException e) { - e.printStackTrace(); - } - - return null; - } - - // Gets the value of a tag from the metadata of a file - private String getTagValue(String metadata, String tag) { - // Finds the tag in the metadata - String[] metadataArray = metadata.split(","); - for (String tagString : metadataArray) { - if (tagString.contains(tag)) { - return tagString.split(" - ")[1]; - } - } - - return null; - } -} diff --git a/API/src/main/java/backend/API/storage/StorageFileNotFoundException.java b/API/src/main/java/backend/API/storage/StorageFileNotFoundException.java deleted file mode 100644 index f0fb7297..00000000 --- a/API/src/main/java/backend/API/storage/StorageFileNotFoundException.java +++ /dev/null @@ -1,12 +0,0 @@ -package backend.API.storage; - -public class StorageFileNotFoundException extends StorageException { - - public StorageFileNotFoundException(String message) { - super(message); - } - - public StorageFileNotFoundException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/API/src/main/java/backend/API/storage/StorageProperties.java b/API/src/main/java/backend/API/storage/StorageProperties.java deleted file mode 100644 index 366b4115..00000000 --- a/API/src/main/java/backend/API/storage/StorageProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -package backend.API.storage; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties("storage") -public class StorageProperties { - - /** - * Folder location for storing files Now you would think changing this would change the location, - * but I couldn't figure out how to use this class in other places, so its hard coded in a lot of - * other places... sorry! - */ - private String location = "upload-dir"; - - public String getLocation() { - return location; - } - - public void setLocation(String location) { - this.location = location; - } -} diff --git a/API/src/main/java/backend/API/storage/StorageService.java b/API/src/main/java/backend/API/storage/StorageService.java deleted file mode 100644 index bf1b4103..00000000 --- a/API/src/main/java/backend/API/storage/StorageService.java +++ /dev/null @@ -1,46 +0,0 @@ -package backend.API.storage; - -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.util.stream.Stream; -import org.springframework.core.io.Resource; -import org.springframework.web.multipart.MultipartFile; - -public interface StorageService { - - void init(); - - Path getRootLocation(); - - void store(MultipartFile file); - - Stream loadAll(); - - Stream loadFolder(String foldername); - - Path load(String filename); - - Resource loadAsResource(String filename); - - void deleteAll(); - - void delete(String filename); - - void copyFile(String filename, String newFilename); - - String readHeaders(String filename); - - String getMaxMin(String filename, String headerName); - - String getLast(String filename); - - Boolean canComputeTimespan(String foldername); - - LocalDateTime[] getTimespan(String filename); - - LocalDateTime[] getTimespan(String filename, LocalDateTime zeroTime); - - LocalDateTime getZeroTime(Path folder); - - String getTypeFolder(String filename); -} diff --git a/API/src/main/resources/application.properties b/API/src/main/resources/application.properties deleted file mode 100644 index 77af0cea..00000000 --- a/API/src/main/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -spring.servlet.multipart.max-file-size=1024MB -spring.servlet.multipart.max-request-size=1024MB \ No newline at end of file diff --git a/API/src/main/resources/templates/uploadForm.html b/API/src/main/resources/templates/uploadForm.html deleted file mode 100644 index 66f8dfaa..00000000 --- a/API/src/main/resources/templates/uploadForm.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - -
-

-

- -
-
- - - - - - - - - -
File to upload:
-
-
- - - - - - \ No newline at end of file diff --git a/API/src/test/java/backend/API/FileUploadControllerTests.java b/API/src/test/java/backend/API/FileUploadControllerTests.java deleted file mode 100644 index 7b84c11f..00000000 --- a/API/src/test/java/backend/API/FileUploadControllerTests.java +++ /dev/null @@ -1,18 +0,0 @@ -package backend.API; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -public class FileUploadControllerTests { - - @Autowired private FileUploadController controller; - - @Test - void contextLoads() { - assertThat(controller).isNotNull(); - } -} diff --git a/API/src/test/java/backend/API/FileUploadTests.java b/API/src/test/java/backend/API/FileUploadTests.java deleted file mode 100644 index 3a16560e..00000000 --- a/API/src/test/java/backend/API/FileUploadTests.java +++ /dev/null @@ -1,59 +0,0 @@ -package backend.API; - -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import backend.API.storage.StorageFileNotFoundException; -import backend.API.storage.StorageService; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.web.servlet.MockMvc; - -@AutoConfigureMockMvc -@SpringBootTest -public class FileUploadTests { - - @Autowired private MockMvc mvc; - - @MockBean private StorageService storageService; - - // @Test - // public void shouldListAllFiles() throws Exception { - // given(this.storageService.loadAll()) - // .willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt"))); - - // this.mvc.perform(get("/")).andExpect(status().isOk()) - // .andExpect(model().attribute("files", - // Matchers.contains("http://localhost/files/first.txt", - // "http://localhost/files/second.txt"))); - // } - - @Test - public void shouldSaveUploadedFile() throws Exception { - MockMultipartFile multipartFile = - new MockMultipartFile("file", "test.txt", "text/plain", "Spring Framework".getBytes()); - this.mvc - .perform(multipart("/").file(multipartFile)) - .andExpect(status().isFound()) - .andExpect(header().string("Location", "/")); - - then(this.storageService).should().store(multipartFile); - } - - @SuppressWarnings("unchecked") - @Test - public void should404WhenMissingFile() throws Exception { - given(this.storageService.loadAsResource("test.txt")) - .willThrow(StorageFileNotFoundException.class); - - this.mvc.perform(get("/files/test.txt")).andExpect(status().isNotFound()); - } -} diff --git a/API/src/test/java/backend/API/storage/FileSystemStorageServiceTests.java b/API/src/test/java/backend/API/storage/FileSystemStorageServiceTests.java deleted file mode 100644 index 8d8d8e0d..00000000 --- a/API/src/test/java/backend/API/storage/FileSystemStorageServiceTests.java +++ /dev/null @@ -1,92 +0,0 @@ -package backend.API.storage; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Random; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; - -public class FileSystemStorageServiceTests { - - private StorageProperties properties = new StorageProperties(); - private FileSystemStorageService service; - - @BeforeEach - public void init() { - properties.setLocation("target/files/" + Math.abs(new Random().nextLong())); - service = new FileSystemStorageService(properties); - service.init(); - } - - @Test - public void emptyUploadLocation() { - service = null; - properties.setLocation(""); - assertThrows( - StorageException.class, - () -> { - service = new FileSystemStorageService(properties); - }); - } - - @Test - public void loadNonExistent() { - assertThat(service.load("foo.txt")).doesNotExist(); - } - - // @Test - // public void saveAndLoad() { - // service.store( - // new MockMultipartFile( - // "foo", "foo.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World".getBytes())); - // assertThat(service.load("foo.txt")).exists(); - // } - - // @Test - // public void saveRelativePathNotPermitted() { - // assertThrows( - // StorageException.class, - // () -> { - // service.store( - // new MockMultipartFile( - // "foo", "../foo.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World".getBytes())); - // }); - // } - - @Test - public void saveAbsolutePathNotPermitted() { - assertThrows( - StorageException.class, - () -> { - service.store( - new MockMultipartFile( - "foo", "/etc/passwd", MediaType.TEXT_PLAIN_VALUE, "Hello, World".getBytes())); - }); - } - - @Test - @EnabledOnOs({OS.LINUX}) - public void saveAbsolutePathInFilenamePermitted() { - // Unix file systems (e.g. ext4) allows backslash '\' in file names. - String fileName = "\\etc\\passwd"; - service.store( - new MockMultipartFile( - fileName, fileName, MediaType.TEXT_PLAIN_VALUE, "Hello, World".getBytes())); - assertTrue(Files.exists(Paths.get(properties.getLocation()).resolve(Paths.get(fileName)))); - } - - // @Test - // public void savePermitted() { - // service.store( - // new MockMultipartFile( - // "foo", "bar/../foo.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World".getBytes())); - // } -} diff --git a/API/src/test/resources/backend/API/testUpload.txt b/API/src/test/resources/backend/API/testUpload.txt deleted file mode 100644 index ca8c1403..00000000 --- a/API/src/test/resources/backend/API/testUpload.txt +++ /dev/null @@ -1 +0,0 @@ -Spring ~Boot~ \ No newline at end of file diff --git a/README.md b/README.md index e873cbe3..9108f094 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ For an in-depth user guide, consult the McMaster Baja Wiki. ### Required tools: NodeJS and NPM -JDK +JDK 21+ ### To setup: 1. Clone repository. 2. Inside a terminal, navigate to the folder Better-Data-Viewer\front-end, then run the command `npm i --force` -3. Inside a terminal, navigate to the folder Better-Data-Viewer\API, then run the command `./mvnw spring-boot:run` +3. Inside a terminal, navigate to the folder Better-Data-Viewer\backend, then run the command `./mvnw quarkus:dev` 4. Inside start.ps1. add your JDK path to the list of `javaHomeLocations`. Setup complete! @@ -32,10 +32,26 @@ Setup complete! 2. Run the command `./start.ps1`, and leave all terminals open. 3. Go to `localhost:3000` to begin. +### Individual startup + +For Quarkus, you can use the Quarkus CLI or Maven commands. For more information, see the readme in the `backend/` folder. +1. Ensure that java 21 is being used. You can check with the `java --version` command. +2. In the `backend/` folder, run either: `quarkus dev` or `./mvnw quarkus:dev` +3. Head to `localhost:8080` to test it out! + +To run the front end, simply use the react scripts +1. Ensure you have NodeJS and npm installed. +2. In the `front-end/` folder, run the `npm start` command. +3. Head to `localhost:3000` to test it out! + ## Known errors - Powershell script unsigned, means script won't run unless `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` is run in powershell first Optionally, run `Set-ExecutionPolicy unrestricted` in an administrator terminal to set it permanently +## Linting +- To run ESLint on the front end, simply download the ESLint extension (The ) in VSCode. You should then be able to run the `Auto-format file` command via the command palette (Ctrl + Shift + P). +- To run Java Google Format, go to the repository (https://github.com/google/google-java-format) and download the latest all-deps `.jar` file. Additionally you can find it locally in the backend folder. You can then run it via the command `java -jar /path/to/google-java-format-${GJF_VERSION?}-all-deps.jar [files...]`. A common option to add is`--replace` which will automatically format it. + - To run it easier, here's a cool command (run in `backend/` directory using bash) `find ./ -name "*.java" -type f -print | xargs java -jar google-java-format-1.22.0-all-deps.jar --replace` Just make sure to check over what it changes ## Rust Library Build/Setup (Only necessary if making library changes) - first have rustup installed https://www.rust-lang.org/tools/install diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..94810d00 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..9dff6de3 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,46 @@ +# Project +uploads/ + +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ diff --git a/backend/.mvn/wrapper/.gitignore b/backend/.mvn/wrapper/.gitignore new file mode 100644 index 00000000..e72f5e8b --- /dev/null +++ b/backend/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/backend/.mvn/wrapper/MavenWrapperDownloader.java b/backend/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..f8bd0915 --- /dev/null +++ b/backend/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public final class MavenWrapperDownloader { + private static final String WRAPPER_VERSION = "3.2.0"; + + private static final boolean VERBOSE = Boolean.parseBoolean(System.getenv("MVNW_VERBOSE")); + + public static void main(String[] args) { + log("Apache Maven Wrapper Downloader " + WRAPPER_VERSION); + + if (args.length != 2) { + System.err.println(" - ERROR wrapperUrl or wrapperJarPath parameter missing"); + System.exit(1); + } + + try { + log(" - Downloader started"); + final URL wrapperUrl = new URL(args[0]); + final String jarPath = args[1].replace("..", ""); // Sanitize path + final Path wrapperJarPath = Paths.get(jarPath).toAbsolutePath().normalize(); + downloadFileFromURL(wrapperUrl, wrapperJarPath); + log("Done"); + } catch (IOException e) { + System.err.println("- Error downloading: " + e.getMessage()); + if (VERBOSE) { + e.printStackTrace(); + } + System.exit(1); + } + } + + private static void downloadFileFromURL(URL wrapperUrl, Path wrapperJarPath) throws IOException { + log(" - Downloading to: " + wrapperJarPath); + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + final String username = System.getenv("MVNW_USERNAME"); + final char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault( + new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + try (InputStream inStream = wrapperUrl.openStream()) { + Files.copy(inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING); + } + log(" - Downloader complete"); + } + + private static void log(String msg) { + if (VERBOSE) { + System.out.println(msg); + } + } +} diff --git a/backend/.mvn/wrapper/maven-wrapper.properties b/backend/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..346d645f --- /dev/null +++ b/backend/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..6308f13d --- /dev/null +++ b/backend/README.md @@ -0,0 +1,60 @@ +# backend + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./mvnw package +``` +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./mvnw package -Dnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./mvnw package -Dnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/backend-1.2.0-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. + +## Related Guides + +- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### REST + +Easily start your REST Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/backend/google-java-format-1.22.0-all-deps.jar b/backend/google-java-format-1.22.0-all-deps.jar new file mode 100644 index 00000000..2e8b83d0 Binary files /dev/null and b/backend/google-java-format-1.22.0-all-deps.jar differ diff --git a/API/mvnw b/backend/mvnw old mode 100644 new mode 100755 similarity index 50% rename from API/mvnw rename to backend/mvnw index 8a8fb228..8d937f4c --- a/API/mvnw +++ b/backend/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,7 +19,7 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven Start Up Batch script +# Apache Maven Wrapper startup batch script, version 3.2.0 # # Required ENV vars: # ------------------ @@ -27,7 +27,6 @@ # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @@ -54,7 +53,7 @@ fi cygwin=false; darwin=false; mingw=false -case "`uname`" in +case "$(uname)" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true @@ -62,9 +61,9 @@ case "`uname`" in # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME else - export JAVA_HOME="/Library/Java/Home" + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME fi fi ;; @@ -72,68 +71,38 @@ esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + JAVA_HOME=$(java-config --jre-home) fi fi -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" - - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi - done - - saveddir=`pwd` - - M2_HOME=`dirname "$PRG"`/.. - - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` - - cd "$saveddir" - # echo Using m2 at $M2_HOME -fi - # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" fi if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" else - javaExecutable="`readlink -f \"$javaExecutable\"`" + javaExecutable="$(readlink -f "\"$javaExecutable\"")" fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') JAVA_HOME="$javaHome" export JAVA_HOME fi @@ -149,7 +118,7 @@ if [ -z "$JAVACMD" ] ; then JAVACMD="$JAVA_HOME/bin/java" fi else - JAVACMD="`\\unset -f command; \\command -v java`" + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" fi fi @@ -163,12 +132,9 @@ if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { - if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" @@ -184,96 +150,99 @@ find_maven_basedir() { fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` + wdir=$(cd "$wdir/.." || exit 1; pwd) fi # end of workaround done - echo "${basedir}" + printf '%s' "$(cd "$basedir" || exit 1; pwd)" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" fi } -BASE_DIR=`find_maven_basedir "$(pwd)"` +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") if [ -z "$BASE_DIR" ]; then exit 1; fi +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## -if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found .mvn/wrapper/maven-wrapper.jar" - fi +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." - fi + log "Couldn't find $wrapperJarPath, downloading it ..." + if [ -n "$MVNW_REPOURL" ]; then - jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" else - jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" fi - while IFS="=" read key value; do - case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; esac - done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" - if [ "$MVNW_VERBOSE" = true ]; then - echo "Downloading from: $jarUrl" - fi - wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + if $cygwin; then - wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") fi if command -v wget > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found wget ... using wget" - fi + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" fi elif command -v curl > /dev/null; then - if [ "$MVNW_VERBOSE" = true ]; then - echo "Found curl ... using curl" - fi + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl -o "$wrapperJarPath" "$jarUrl" -f + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" else - curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" fi - else - if [ "$MVNW_VERBOSE" = true ]; then - echo "Falling back to using Java to download" - fi - javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" # For Cygwin, switch paths to Windows format before running javac if $cygwin; then - javaClass=`cygpath --path --windows "$javaClass"` + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") fi - if [ -e "$javaClass" ]; then - if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Compiling MavenWrapperDownloader.java ..." - fi - # Compiling the Java class - ("$JAVA_HOME/bin/javac" "$javaClass") + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") fi - if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then - # Running the downloader - if [ "$MVNW_VERBOSE" = true ]; then - echo " - Running MavenWrapperDownloader.java ..." - fi - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" fi fi fi @@ -282,35 +251,58 @@ fi # End of extension ########################################################################################## -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi fi + MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") fi # Provide a "standardized" way to retrieve the CLI args that will # work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" export MAVEN_CMD_LINE_ARGS WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +# shellcheck disable=SC2086 # safe args exec "$JAVACMD" \ $MAVEN_OPTS \ $MAVEN_DEBUG_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" \ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/API/mvnw.cmd b/backend/mvnw.cmd similarity index 81% rename from API/mvnw.cmd rename to backend/mvnw.cmd index 1d8ab018..c4586b56 100644 --- a/API/mvnw.cmd +++ b/backend/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,13 +18,12 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Maven Start Up Batch script +@REM Apache Maven Wrapper startup batch script, version 3.2.0 @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @@ -120,10 +119,10 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain -set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @@ -134,11 +133,11 @@ if exist %WRAPPER_JAR% ( ) ) else ( if not "%MVNW_REPOURL%" == "" ( - SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" ) if "%MVNW_VERBOSE%" == "true" ( echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %DOWNLOAD_URL% + echo Downloading from: %WRAPPER_URL% ) powershell -Command "&{"^ @@ -146,7 +145,7 @@ if exist %WRAPPER_JAR% ( "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ "}" if "%MVNW_VERBOSE%" == "true" ( echo Finished downloading %WRAPPER_JAR% @@ -154,6 +153,24 @@ if exist %WRAPPER_JAR% ( ) @REM End of extension +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + @REM Provide a "standardized" way to retrieve the CLI args that will @REM work with both Windows and non-Windows executions. set MAVEN_CMD_LINE_ARGS=%* diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 00000000..66a17bd5 --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,165 @@ + + + 4.0.0 + com.mcmasterbaja + backend + 1.2.0 + + + 3.12.1 + 21 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.9.3 + true + 3.2.5 + true + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-rest-jackson + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-swagger-ui + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + org.projectlombok + lombok + 1.18.30 + + + com.opencsv + opencsv + 5.8 + + + org.apache.commons + commons-math3 + 3.6.1 + + + commons-io + commons-io + 2.16.1 + + + com.fazecast + jSerialComm + [2.0.0,3.0.0) + + + com.drewnoakes + metadata-extractor + 2.19.0 + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/backend/src/main/docker/Dockerfile.jvm b/backend/src/main/docker/Dockerfile.jvm new file mode 100644 index 00000000..f837a7d7 --- /dev/null +++ b/backend/src/main/docker/Dockerfile.jvm @@ -0,0 +1,97 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/backend-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backend-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/backend-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-21:1.18 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] + diff --git a/backend/src/main/docker/Dockerfile.legacy-jar b/backend/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 00000000..2cb56e86 --- /dev/null +++ b/backend/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,93 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/backend-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backend-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/backend-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-21:1.18 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/backend/src/main/docker/Dockerfile.native b/backend/src/main/docker/Dockerfile.native new file mode 100644 index 00000000..2f279874 --- /dev/null +++ b/backend/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/backend . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backend +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.9 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/backend/src/main/docker/Dockerfile.native-micro b/backend/src/main/docker/Dockerfile.native-micro new file mode 100644 index 00000000..b041ac93 --- /dev/null +++ b/backend/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Dnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/backend . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backend +# +### +FROM quay.io/quarkus/quarkus-micro-image:2.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/backend/src/main/java/com/mcmasterbaja/ExceptionMappers.java b/backend/src/main/java/com/mcmasterbaja/ExceptionMappers.java new file mode 100644 index 00000000..820e3cca --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/ExceptionMappers.java @@ -0,0 +1,106 @@ +package com.mcmasterbaja; + +import com.mcmasterbaja.exceptions.InvalidArgumentException; +import com.mcmasterbaja.exceptions.MalformedCsvException; +import com.mcmasterbaja.exceptions.StorageException; +import com.mcmasterbaja.model.ErrorResponse; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.util.UUID; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; + +// Exceptions are mapped in priority of most specific first +public class ExceptionMappers { + + @Inject Logger logger; + + // Handles invalid arguments + @ServerExceptionMapper(value = {InvalidArgumentException.class, IllegalArgumentException.class}) + public Response invalidArgument(RuntimeException e) { + String errorId = UUID.randomUUID().toString(); + logger.error("errorId[{}]", errorId, e); + + ErrorResponse errorResponse = + new ErrorResponse( + errorId, + e.getStackTrace()[0].getClassName() + "." + e.getStackTrace()[0].getMethodName(), + "An invalid argument was passed.", + "INVALID_ARGUMENT", + e.getMessage()); + + return Response.status(Response.Status.BAD_REQUEST) + .entity(errorResponse) + .type(MediaType.APPLICATION_JSON) + .build(); + } + + // Handles poor CSVs + @ServerExceptionMapper + public Response mapNotFoundException(MalformedCsvException e) { + String errorId = UUID.randomUUID().toString(); + logger.error("errorId[{}]", errorId, e); + + ErrorResponse errorResponse = + new ErrorResponse( + errorId, + e.getStackTrace()[14].getClassName() + "." + e.getStackTrace()[14].getMethodName(), + "The CSV file `" + e.getFile() + "` is invalid. Please check or re-upload.", + "MALFORMED_CSV", + e.getMessage()); + + return Response.status(422).entity(errorResponse).type(MediaType.APPLICATION_JSON).build(); + } + + // Handles general storage exceptions + @ServerExceptionMapper + public Response mapStorageException(StorageException e) { + String errorId = UUID.randomUUID().toString(); + logger.error("errorId[{}]", errorId, e); + + ErrorResponse errorResponse = + new ErrorResponse( + errorId, + e.getStackTrace()[0].getClassName() + "." + e.getStackTrace()[0].getMethodName(), + "Something went wrong. It's probably your fault.", + "STORAGE_EXCEPTION", + e.getMessage()); + + return Response.status(500).entity(errorResponse).type(MediaType.APPLICATION_JSON).build(); + } + + // TODO: Put these in the upload resource file, since they are specific to that + @ServerExceptionMapper + public Response mapUnsatisfiedLink(UnsatisfiedLinkError e) { + String errorId = UUID.randomUUID().toString(); + logger.error("errorId[{}]", errorId, e); + + ErrorResponse errorResponse = + new ErrorResponse( + errorId, + e.getStackTrace()[6].getClassName() + "." + e.getStackTrace()[6].getMethodName(), + "Failed to link to parser library. Might need to restart the backend.", + "UNSATISFIED_LINK_ERROR", + e.getMessage()); + + return Response.status(500).entity(errorResponse).type(MediaType.APPLICATION_JSON).build(); + } + + @ServerExceptionMapper + public Response mapUnsatisfiedLink(NoClassDefFoundError e) { + String errorId = UUID.randomUUID().toString(); + logger.error("errorId[{}]", errorId, e); + + ErrorResponse errorResponse = + new ErrorResponse( + errorId, + e.getStackTrace()[0].getClassName() + "." + e.getStackTrace()[0].getMethodName(), + "Failed to link to parser library. Probably need to restart your backend, live reload" + + " for the parser does not work.", + "UNSATISFIED_LINK_ERROR", + e.getMessage()); + + return Response.status(500).entity(errorResponse).type(MediaType.APPLICATION_JSON).build(); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java b/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java new file mode 100644 index 00000000..f7851cf7 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/FileAnalyzeResource.java @@ -0,0 +1,98 @@ +package com.mcmasterbaja; + +import com.mcmasterbaja.analyzer.Analyzer; +import com.mcmasterbaja.analyzer.AnalyzerFactory; +import com.mcmasterbaja.exceptions.InvalidArgumentException; +import com.mcmasterbaja.live.Serial; +import com.mcmasterbaja.model.AnalyzerParams; +import com.mcmasterbaja.services.FileMetadataService; +import com.mcmasterbaja.services.StorageService; +import jakarta.inject.Inject; +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PATCH; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.RestResponse.ResponseBuilder; + +@jakarta.ws.rs.Path("/") +public class FileAnalyzeResource { + + @Inject Logger logger; + @Inject StorageService storageService; + @Inject FileMetadataService fileMetadataService; + + // TODO: Convert to using POST body rather than path variables + @POST + @jakarta.ws.rs.Path("analyze") + public RestResponse runAnalyzer(@BeanParam AnalyzerParams params) { + logger.info("Running analyzer with params: " + params.toString()); + + if (!params.getErrors().isEmpty()) { + throw new InvalidArgumentException(params.getErrors()); + } + + // Update input files with rootLocation/csv and generate output file names + params.updateInputFiles(storageService.getRootLocation()); + params.generateOutputFileNames(); + + // TODO: Can't pass in null to createAnalyzer, this if statement feels redundant + if (params.getType() != null) { + Analyzer analyzer = AnalyzerFactory.createAnalyzer(params); + if (analyzer != null) { + try { + analyzer.analyze(); + } catch (Exception e) { + logger.error("Error running analyzer", e); + throw new RuntimeException("Error running analyzer"); + } + } + } + + Path targetPath = Paths.get(params.getOutputFiles()[0]); + File file = storageService.load(targetPath).toFile(); + Path relativePath = storageService.load(Paths.get("csv")).relativize(targetPath); + + return ResponseBuilder.ok(file, "application/octet-stream") + .header("Content-Disposition", "attachment; filename=\"" + relativePath.toString() + "\"") + .header("Access-Control-Expose-Headers", "Content-Disposition") + .build(); + } + + @GET + @jakarta.ws.rs.Path("minMax/{filekey}") + public Double[] getMinMax( + @PathParam("filekey") String filekey, @QueryParam("column") String column) { + logger.info("Getting min and max for file: " + filekey); + + String typeFolder = fileMetadataService.getTypeFolder(Paths.get(filekey)); + Path targetPath = storageService.load(Paths.get(typeFolder)).resolve(filekey); + Double[] minMax = fileMetadataService.getMinMax(targetPath, column); + + return minMax; + } + + @PATCH + @jakarta.ws.rs.Path("togglelive") + public String toggleLive() { + logger.info("Toggling live data to: " + Serial.exit); + + if (!Serial.exit) { + Serial.exit = true; + } else { + new Thread( + () -> { + Serial.readLive(); + }) + .start(); + } + + return "Live data toggled to " + Serial.exit; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java b/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java new file mode 100644 index 00000000..fda9e16b --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/FileDeleteResource.java @@ -0,0 +1,49 @@ +package com.mcmasterbaja; + +import com.mcmasterbaja.services.StorageService; +import jakarta.inject.Inject; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.PathParam; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.jboss.logging.Logger; + +@jakarta.ws.rs.Path("/delete") +public class FileDeleteResource { + + @Inject Logger logger; + @Inject StorageService storageService; + + @DELETE + @jakarta.ws.rs.Path("/file/{filekey}") + public String deleteFile(@PathParam("filekey") String filekey) { + logger.info("Deleting file: " + filekey); + + Path targetPath = Paths.get(filekey); + storageService.delete(targetPath); + + return "File deleted successfully"; + } + + @DELETE + @jakarta.ws.rs.Path("/folder/{folderkey}") + public String deleteFolder(@PathParam("folderkey") String folderkey) { + logger.info("Deleting folder: " + folderkey); + + Path targetPath = Paths.get(folderkey); + storageService.deleteAll(targetPath); + + return "All files deleted successfully"; + } + + @DELETE + @jakarta.ws.rs.Path("/all") + public String deleteAllFiles() { + logger.info("Deleting all files"); + + storageService.deleteAll(); + storageService.init(); + + return "All files deleted successfully"; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/FileFetchResource.java b/backend/src/main/java/com/mcmasterbaja/FileFetchResource.java new file mode 100644 index 00000000..af14d236 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/FileFetchResource.java @@ -0,0 +1,159 @@ +package com.mcmasterbaja; + +import com.mcmasterbaja.model.FileInformation; +import com.mcmasterbaja.model.FileTimespan; +import com.mcmasterbaja.services.FileMetadataService; +import com.mcmasterbaja.services.StorageService; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jboss.logging.Logger; + +@jakarta.ws.rs.Path("/files") // Use full package name to avoid conflict with java.nio.file.Path +public class FileFetchResource { + + @Inject Logger logger; + @Inject StorageService storageService; + @Inject FileMetadataService fileMetadataService; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List getAllFiles() { + logger.info("Getting all files"); + + List fileNames = + storageService.loadAll().map(Path::toString).collect(Collectors.toList()); + + return fileNames; + } + + // TODO: What exception is thrown when it can't find the file? + @GET + @jakarta.ws.rs.Path("/{filekey}") + public File getFile(@PathParam("filekey") String filekey) { + logger.info("Getting file: " + filekey); + + Path targetPath = addTypeFolder(filekey); + File file = storageService.load(targetPath).toFile(); + + return file; + } + + @GET + @jakarta.ws.rs.Path("/information") + public List getInformation() { + logger.info("Getting all file information"); + + List fileInformation = + storageService + .loadAll() + .map( + path -> + new FileInformation( + path, + fileMetadataService.readHeaders(path), + fileMetadataService.getSize(path))) + .collect(Collectors.toList()); + + return fileInformation; + } + + @GET + @jakarta.ws.rs.Path("/information/{filekey}") + public FileInformation getInformation(@PathParam("filekey") String filekey) { + logger.info("Getting file information for: " + filekey); + + Path targetPath = Paths.get(filekey); + + FileInformation fileInformation = + new FileInformation( + targetPath, + fileMetadataService.readHeaders(targetPath), + fileMetadataService.getSize(targetPath)); + + return fileInformation; + } + + @GET + @jakarta.ws.rs.Path("/information/folder/{folderkey}") + public List getInformationForFolder(@PathParam("folderkey") String folderkey) { + logger.info("Getting file information for folder: " + folderkey); + + Path folderPath = Paths.get(folderkey); + + List fileInformationList = + storageService + .loadAll(folderPath) + .map( + path -> + new FileInformation( + folderPath.relativize(path), + fileMetadataService.readHeaders(path), + fileMetadataService.getSize(path))) + .collect(Collectors.toList()); + + return fileInformationList; + } + + @GET + @jakarta.ws.rs.Path("/timespan/folder/{folderkey}") + public List getTimespan(@PathParam("folderkey") String folderkey) { + logger.info("Getting timespan for folder: " + folderkey); + + Path folderPath = Paths.get(folderkey); + List timespans = new ArrayList<>(); + Stream paths = storageService.loadAll(folderPath); + + switch (folderkey) { + case "csv": + // Map of parent folders to zero times to avoid recalculating the zero time + Map zeroTimeMap = new HashMap<>(); + paths.forEach( + path -> { + Path parent = path.getParent(); + if (fileMetadataService.canComputeTimespan(parent)) { + // Add the zero time to the map if the parent folder has not been analyzed + zeroTimeMap.putIfAbsent(parent, fileMetadataService.getZeroTime(parent)); + + // Comput the timespan of the file and add it to the list + LocalDateTime[] timespan = + fileMetadataService.getTimespan(path, zeroTimeMap.get(parent)); + timespans.add( + new FileTimespan(folderPath.relativize(path), timespan[0], timespan[1])); + } + }); + break; + + case "mp4": + paths.forEach( + path -> { + LocalDateTime[] timespan = fileMetadataService.getTimespan(path, null); + timespans.add( + new FileTimespan(folderPath.relativize(path), timespan[0], timespan[1])); + }); + break; + + default: + throw new IllegalArgumentException("Invalid folder name"); + } + + return timespans; + } + + private Path addTypeFolder(String fileKey) { + String typeFolder = fileMetadataService.getTypeFolder(Paths.get(fileKey)); + return storageService.load(Paths.get(typeFolder)).resolve(fileKey); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java b/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java new file mode 100644 index 00000000..3c7ed25e --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/FileUploadResource.java @@ -0,0 +1,77 @@ +package com.mcmasterbaja; + +import com.mcmasterbaja.binary_csv.BinaryToCSV; +import com.mcmasterbaja.exceptions.StorageException; +import com.mcmasterbaja.services.StorageService; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.PartType; +import org.jboss.resteasy.reactive.RestForm; + +@jakarta.ws.rs.Path("/upload") +public class FileUploadResource { + + @Inject Logger logger; + @Inject StorageService storageService; + + @POST + @jakarta.ws.rs.Path("/file") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String uploadFile( + @RestForm("fileName") String fileName, + @RestForm("fileData") @PartType(MediaType.APPLICATION_OCTET_STREAM) InputStream fileData) { + + logger.info("Uploading file: " + fileName); + + if (fileName.lastIndexOf('.') == -1) { + throw new IllegalArgumentException("Invalid file name: " + fileName); + } + + String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + + switch (fileExtension) { + case "csv": + case "mp4": + storageService.store(fileData, Paths.get(fileExtension + "/" + fileName)); + break; + + case "mov": + fileName = "mp4/" + fileName.substring(0, fileName.lastIndexOf('.') + 1) + "mp4"; + storageService.store(fileData, Paths.get(fileName)); + break; + + case "bin": + String outputDir = storageService.load(Paths.get("csv")).toString(); + + logger.info( + "Parsing bin to: " + + outputDir + + "/" + + fileName.substring(0, fileName.lastIndexOf('.')) + + "/"); + + try (InputStream input = fileData) { // try-with-resources, look it up if you don't know it + BinaryToCSV.bytesToCSV(fileData.readAllBytes(), outputDir, fileName, true); + } catch (IOException e) { // UnsatisfiedLinkError, IOException + throw new StorageException("Failed to read bytes from: " + fileName, e); + } + break; + + default: + try { + fileData.close(); + } catch (IOException e) { + throw new StorageException("Failed to close fileData", e); + } + throw new IllegalArgumentException("Invalid filetype: " + fileExtension); + } + + return "File uploaded successfully"; + } +} diff --git a/API/src/main/java/backend/API/analyzer/AccelCurveAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/AccelCurveAnalyzer.java similarity index 98% rename from API/src/main/java/backend/API/analyzer/AccelCurveAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/AccelCurveAnalyzer.java index 14f842fb..d7b6e908 100644 --- a/API/src/main/java/backend/API/analyzer/AccelCurveAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/AccelCurveAnalyzer.java @@ -1,8 +1,8 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; // Shouldn't need this -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.Reader; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.Reader; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; diff --git a/backend/src/main/java/com/mcmasterbaja/analyzer/Analyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/Analyzer.java new file mode 100644 index 00000000..18b1e7ca --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/Analyzer.java @@ -0,0 +1,84 @@ +package com.mcmasterbaja.analyzer; + +import com.opencsv.CSVReader; +import com.opencsv.CSVReaderBuilder; +import com.opencsv.CSVWriter; +import com.opencsv.CSVWriterBuilder; +import com.opencsv.ICSVWriter; +import com.opencsv.exceptions.CsvValidationException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +public abstract class Analyzer { + + // Input and output files are arrays because some analyzers may need multiple input files + protected String[] inputFiles; + protected String[] inputColumns; + protected String[] outputFiles; + + public Analyzer(String[] inputFiles, String[] inputColumns, String[] outputFiles) { + this.inputFiles = inputFiles; + // inputColumns is the names of the columns we are analyzing. index 0 is the independent + // variable (usually timestamp), 1+ are dependent variable(s) + this.inputColumns = inputColumns; + this.outputFiles = outputFiles; + } + + // Some analyzers work on entire rows and don't need to select columns (e.g. compression), they + // should use this constructor + public Analyzer(String[] inputFiles, String[] outputFiles) { + this.inputFiles = inputFiles; + this.inputColumns = new String[1]; + this.outputFiles = outputFiles; + } + + // Abstract method to be implemented by subclasses + public abstract void analyze() throws IOException, CsvValidationException; + + // I/O methods + // Streams as they avoid loading the entire file into memory at once + public CSVReader getReader(String filePath) throws IOException { + FileReader fileReader = new FileReader(filePath); + BufferedReader bufferedReader = new BufferedReader(fileReader); + return new CSVReaderBuilder(bufferedReader) + .withSkipLines(0) // Skip header if needed + .build(); + } + + public ICSVWriter getWriter(String filePath) throws IOException { + FileWriter fileWriter = new FileWriter(filePath); + BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); + return new CSVWriterBuilder(bufferedWriter) + .withSeparator(CSVWriter.DEFAULT_SEPARATOR) + .withQuoteChar(CSVWriter.NO_QUOTE_CHARACTER) + .withEscapeChar(CSVWriter.DEFAULT_ESCAPE_CHARACTER) + .withLineEnd(CSVWriter.DEFAULT_LINE_END) + .build(); + } + + // From this list of headers, which one are we actually doing analysis on + // fileIndex is basically the axis, 0=X, 1=Y, I made it a int to future-proof adding new columns + public int getAnalysisColumnIndex(int fileIndex, List fileHeaders) + throws RuntimeException { + for (int i = 0; i < fileHeaders.size(); i++) { + if (fileHeaders.get(i).trim().equals(this.inputColumns[fileIndex])) { + return i; + } + } + // The inputColum is wrong somehow, should never happen with working frontend + throw new RuntimeException("No column in file exists with analysis column name"); + } + + public int getColumnIndex(String columnName, String[] fileHeaders) throws RuntimeException { + for (int i = 0; i < fileHeaders.length; i++) { + if (fileHeaders[i].trim().equals(columnName)) { + return i; + } + } + throw new RuntimeException("No column in file exists with analysis column name"); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/analyzer/AnalyzerFactory.java b/backend/src/main/java/com/mcmasterbaja/analyzer/AnalyzerFactory.java new file mode 100644 index 00000000..df687a1d --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/AnalyzerFactory.java @@ -0,0 +1,83 @@ +package com.mcmasterbaja.analyzer; + +import com.mcmasterbaja.model.AnalyzerParams; + +public class AnalyzerFactory { + + public static Analyzer createAnalyzer(AnalyzerParams params) { + + String[] inputFiles = params.getInputFiles(); + String[] inputColumns = params.getInputColumns(); + String[] outputFiles = params.getOutputFiles(); + Object[] options = params.getOptions(); + + switch (params.getType()) { + case ACCEL_CURVE: + return new AccelCurveAnalyzer(inputFiles, inputColumns, outputFiles); + + case ROLL_AVG: + if (options.length == 0) { + return new RollingAvgAnalyzer(inputFiles, inputColumns, outputFiles); + } + int windowSize = Integer.parseInt((String) options[0]); + return new RollingAvgAnalyzer(inputFiles, inputColumns, outputFiles, windowSize); + + case SGOLAY: + // Check if passed a window size + if (options.length == 0) { + return new SGolayFilter(inputFiles, inputColumns, outputFiles); + } + windowSize = Integer.parseInt((String) options[0]); + int polynomialDegree = Integer.parseInt((String) options[1]); + return new SGolayFilter( + inputFiles, inputColumns, outputFiles, windowSize, polynomialDegree); + + case LINEAR_INTERPOLATE: + return new LinearInterpolaterAnalyzer( + inputFiles, new String[] {"Timestamp (ms)", inputColumns[1]}, outputFiles); + + case RDP_COMPRESSION: + if (options.length == 0) { + return new RDPCompressionAnalyzer(inputFiles, outputFiles, 15); + } + double epsilon = Double.parseDouble((String) options[0]); + return new RDPCompressionAnalyzer(inputFiles, outputFiles, epsilon); + + case SPLIT: + System.out.println("SplitAnalyzer"); + if (options[1] == "" || options[0] == "") { + return null; + } + int start = Integer.parseInt((String) options[0]); + int end = Integer.parseInt((String) options[1]); + return new SplitAnalyzer(inputFiles, inputColumns, outputFiles, start, end); + + case LINEAR_MULTIPLY: + if (options[1] == "" || options[0] == "") { + return null; + } + double m = Double.parseDouble((String) options[0]); + double b = Double.parseDouble((String) options[1]); + return new LinearMultiplyAnalyzer(inputFiles, inputColumns, outputFiles, m, b); + + case AVERAGE: + int[] range = new int[2]; + range[0] = Integer.parseInt((String) options[0]); + range[1] = Integer.parseInt((String) options[1]); + return new AverageAnalyzer(inputFiles, outputFiles, range); + + case INTERPOLATER_PRO: + return new InterpolaterProAnalyzer(inputFiles, inputColumns, outputFiles); + + case CUBIC: + double a = Double.parseDouble((String) options[0]); + double b1 = Double.parseDouble((String) options[1]); + double c = Double.parseDouble((String) options[2]); + double d = Double.parseDouble((String) options[3]); + return new CubicAnalyzer(inputFiles, inputColumns, outputFiles, a, b1, c, d); + + default: + return null; + } + } +} diff --git a/API/src/main/java/backend/API/analyzer/AverageAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/AverageAnalyzer.java similarity index 93% rename from API/src/main/java/backend/API/analyzer/AverageAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/AverageAnalyzer.java index 2e75897b..cb5cff69 100644 --- a/API/src/main/java/backend/API/analyzer/AverageAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/AverageAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/BullshitAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/BullshitAnalyzer.java similarity index 96% rename from API/src/main/java/backend/API/analyzer/BullshitAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/BullshitAnalyzer.java index dd86c7ab..f111af2c 100644 --- a/API/src/main/java/backend/API/analyzer/BullshitAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/BullshitAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/CubicAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/CubicAnalyzer.java similarity index 98% rename from API/src/main/java/backend/API/analyzer/CubicAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/CubicAnalyzer.java index 670082a9..51fb52a0 100644 --- a/API/src/main/java/backend/API/analyzer/CubicAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/CubicAnalyzer.java @@ -1,4 +1,4 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; import com.opencsv.CSVReader; import com.opencsv.ICSVWriter; diff --git a/API/src/main/java/backend/API/analyzer/DeleteRanom.java b/backend/src/main/java/com/mcmasterbaja/analyzer/DeleteRanom.java similarity index 88% rename from API/src/main/java/backend/API/analyzer/DeleteRanom.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/DeleteRanom.java index 63dd05a6..af6ba038 100644 --- a/API/src/main/java/backend/API/analyzer/DeleteRanom.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/DeleteRanom.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/InterpolaterProAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/InterpolaterProAnalyzer.java similarity index 99% rename from API/src/main/java/backend/API/analyzer/InterpolaterProAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/InterpolaterProAnalyzer.java index 1544f0d8..2cbe0038 100644 --- a/API/src/main/java/backend/API/analyzer/InterpolaterProAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/InterpolaterProAnalyzer.java @@ -1,4 +1,4 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; import com.opencsv.CSVReader; import com.opencsv.ICSVWriter; diff --git a/API/src/main/java/backend/API/analyzer/LinearInterpolaterAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/LinearInterpolaterAnalyzer.java similarity index 94% rename from API/src/main/java/backend/API/analyzer/LinearInterpolaterAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/LinearInterpolaterAnalyzer.java index 9cd6bc83..dce4b5af 100644 --- a/API/src/main/java/backend/API/analyzer/LinearInterpolaterAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/LinearInterpolaterAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/LinearMultiplyAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/LinearMultiplyAnalyzer.java similarity index 92% rename from API/src/main/java/backend/API/analyzer/LinearMultiplyAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/LinearMultiplyAnalyzer.java index fbc610b2..c49c4826 100644 --- a/API/src/main/java/backend/API/analyzer/LinearMultiplyAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/LinearMultiplyAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/RDPCompressionAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/RDPCompressionAnalyzer.java similarity index 94% rename from API/src/main/java/backend/API/analyzer/RDPCompressionAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/RDPCompressionAnalyzer.java index a0908612..66149a6b 100644 --- a/API/src/main/java/backend/API/analyzer/RDPCompressionAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/RDPCompressionAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/RollingAvgAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/RollingAvgAnalyzer.java similarity index 92% rename from API/src/main/java/backend/API/analyzer/RollingAvgAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/RollingAvgAnalyzer.java index 9e8b88a0..fc77ece4 100644 --- a/API/src/main/java/backend/API/analyzer/RollingAvgAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/RollingAvgAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/API/src/main/java/backend/API/analyzer/SGolayFilter.java b/backend/src/main/java/com/mcmasterbaja/analyzer/SGolayFilter.java similarity index 95% rename from API/src/main/java/backend/API/analyzer/SGolayFilter.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/SGolayFilter.java index 4a97251c..58dd4a35 100644 --- a/API/src/main/java/backend/API/analyzer/SGolayFilter.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/SGolayFilter.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; import org.apache.commons.math3.linear.MatrixUtils; diff --git a/API/src/main/java/backend/API/analyzer/SplitAnalyzer.java b/backend/src/main/java/com/mcmasterbaja/analyzer/SplitAnalyzer.java similarity index 92% rename from API/src/main/java/backend/API/analyzer/SplitAnalyzer.java rename to backend/src/main/java/com/mcmasterbaja/analyzer/SplitAnalyzer.java index 1c5d0a9e..137ffb6f 100644 --- a/API/src/main/java/backend/API/analyzer/SplitAnalyzer.java +++ b/backend/src/main/java/com/mcmasterbaja/analyzer/SplitAnalyzer.java @@ -1,9 +1,9 @@ -package backend.API.analyzer; +package com.mcmasterbaja.analyzer; -import backend.API.readwrite.CSVReader; -import backend.API.readwrite.CSVWriter; -import backend.API.readwrite.Reader; -import backend.API.readwrite.Writer; +import com.mcmasterbaja.readwrite.CSVReader; +import com.mcmasterbaja.readwrite.CSVWriter; +import com.mcmasterbaja.readwrite.Reader; +import com.mcmasterbaja.readwrite.Writer; import java.util.ArrayList; import java.util.List; diff --git a/backend/src/main/java/com/mcmasterbaja/binary_csv/BinaryToCSV.java b/backend/src/main/java/com/mcmasterbaja/binary_csv/BinaryToCSV.java new file mode 100644 index 00000000..c9c45a64 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/binary_csv/BinaryToCSV.java @@ -0,0 +1,47 @@ +package com.mcmasterbaja.binary_csv; + +import java.nio.file.Paths; + +public class BinaryToCSV { + + public static native void toCSV(String filename, String outputDir, boolean folder); + + public static native void bytesToCSV( + byte[] bytes, String outputDir, String fileName, boolean folder); + + private static final String relativePath = "/src/main/java/com/mcmasterbaja/binary_csv/"; + + static { + String path = System.getProperty("user.dir"); + + if (System.getProperty("os.name").equals("Mac OS X")) { + path += relativePath + "/libbinary_to_csv_lib.dylib"; + } else { + path += relativePath + "binary_to_csv_lib.dll"; + } + + System.load(path); + } + + public static void main(String[] args) { + System.out.println( + Paths.get("src/main/java/com/mcmasterbaja/binary_csv/040918.bin") + .toAbsolutePath() + .toString()); + System.out.println(Paths.get("uploads").toAbsolutePath() + "\\"); + try { + toCSV( + Paths.get("src/main/java/com/mcmasterbaja/binary_csv/040918.bin") + .toAbsolutePath() + .toString(), + Paths.get("uploads").toAbsolutePath() + "\\", + true); + } catch (UnsatisfiedLinkError e) { + // Print the error in full + e.printStackTrace(); + System.out.println(e); + } + + System.out.println("Done"); + } +} diff --git a/API/src/main/java/backend/API/binary_csv/Packet.java b/backend/src/main/java/com/mcmasterbaja/binary_csv/Packet.java similarity index 98% rename from API/src/main/java/backend/API/binary_csv/Packet.java rename to backend/src/main/java/com/mcmasterbaja/binary_csv/Packet.java index 6b0b3923..b6e0f34d 100644 --- a/API/src/main/java/backend/API/binary_csv/Packet.java +++ b/backend/src/main/java/com/mcmasterbaja/binary_csv/Packet.java @@ -1,4 +1,4 @@ -package backend.API.binary_csv; +package com.mcmasterbaja.binary_csv; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/backend/src/main/java/com/mcmasterbaja/binary_csv/binary_to_csv_lib.dll b/backend/src/main/java/com/mcmasterbaja/binary_csv/binary_to_csv_lib.dll new file mode 100644 index 00000000..87fb7b9c Binary files /dev/null and b/backend/src/main/java/com/mcmasterbaja/binary_csv/binary_to_csv_lib.dll differ diff --git a/backend/src/main/java/com/mcmasterbaja/binary_csv/com_mcmasterbaja_binary_csv_BinaryToCSV.h b/backend/src/main/java/com/mcmasterbaja/binary_csv/com_mcmasterbaja_binary_csv_BinaryToCSV.h new file mode 100644 index 00000000..f61600cc --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/binary_csv/com_mcmasterbaja_binary_csv_BinaryToCSV.h @@ -0,0 +1,29 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_mcmasterbaja_binary_csv_BinaryToCSV */ + +#ifndef _Included_com_mcmasterbaja_binary_csv_BinaryToCSV +#define _Included_com_mcmasterbaja_binary_csv_BinaryToCSV +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_mcmasterbaja_binary_csv_BinaryToCSV + * Method: toCSV + * Signature: (Ljava/lang/String;Ljava/lang/String;Z)V + */ +JNIEXPORT void JNICALL Java_com_mcmasterbaja_binary_1csv_BinaryToCSV_toCSV + (JNIEnv *, jclass, jstring, jstring, jboolean); + +/* + * Class: com_mcmasterbaja_binary_csv_BinaryToCSV + * Method: bytesToCSV + * Signature: ([BLjava/lang/String;Ljava/lang/String;Z)V + */ +JNIEXPORT void JNICALL Java_com_mcmasterbaja_binary_1csv_BinaryToCSV_bytesToCSV + (JNIEnv *, jclass, jbyteArray, jstring, jstring, jboolean); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/backend/src/main/java/com/mcmasterbaja/binary_csv/libbinary_to_csv_lib.dylib b/backend/src/main/java/com/mcmasterbaja/binary_csv/libbinary_to_csv_lib.dylib new file mode 100755 index 00000000..9db1670f Binary files /dev/null and b/backend/src/main/java/com/mcmasterbaja/binary_csv/libbinary_to_csv_lib.dylib differ diff --git a/backend/src/main/java/com/mcmasterbaja/exceptions/FileNotFoundException.java b/backend/src/main/java/com/mcmasterbaja/exceptions/FileNotFoundException.java new file mode 100644 index 00000000..a38d7778 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/exceptions/FileNotFoundException.java @@ -0,0 +1,12 @@ +package com.mcmasterbaja.exceptions; + +public class FileNotFoundException extends StorageException { + + public FileNotFoundException(String message) { + super(message); + } + + public FileNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/exceptions/InvalidArgumentException.java b/backend/src/main/java/com/mcmasterbaja/exceptions/InvalidArgumentException.java new file mode 100644 index 00000000..ab6b1871 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/exceptions/InvalidArgumentException.java @@ -0,0 +1,22 @@ +package com.mcmasterbaja.exceptions; + +import java.util.List; +import lombok.Getter; + +@Getter +public class InvalidArgumentException extends RuntimeException { + private final List errors; + + public InvalidArgumentException(List errors) { + this.errors = errors; + } + + public InvalidArgumentException(String error) { + this.errors = List.of(error); + } + + public InvalidArgumentException(List errors, Throwable cause) { + super(cause); + this.errors = errors; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/exceptions/MalformedCsvException.java b/backend/src/main/java/com/mcmasterbaja/exceptions/MalformedCsvException.java new file mode 100644 index 00000000..d6c4c87f --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/exceptions/MalformedCsvException.java @@ -0,0 +1,21 @@ +package com.mcmasterbaja.exceptions; + +import lombok.Getter; + +@Getter +public class MalformedCsvException extends StorageException { + private String file; + + public MalformedCsvException(String message) { + super(message); + } + + public MalformedCsvException(String message, Throwable cause) { + super(message, cause); + } + + public MalformedCsvException(String message, String file, Throwable cause) { + super(message, cause); + this.file = file; + } +} diff --git a/API/src/main/java/backend/API/storage/StorageException.java b/backend/src/main/java/com/mcmasterbaja/exceptions/StorageException.java similarity index 60% rename from API/src/main/java/backend/API/storage/StorageException.java rename to backend/src/main/java/com/mcmasterbaja/exceptions/StorageException.java index a6168e71..e499c90b 100644 --- a/API/src/main/java/backend/API/storage/StorageException.java +++ b/backend/src/main/java/com/mcmasterbaja/exceptions/StorageException.java @@ -1,14 +1,12 @@ -package backend.API.storage; +package com.mcmasterbaja.exceptions; public class StorageException extends RuntimeException { public StorageException(String message) { super(message); - System.out.println("StorageException - " + message); } public StorageException(String message, Throwable cause) { super(message, cause); - System.out.println("StorageException - " + message); } } diff --git a/API/src/main/java/backend/API/live/Serial.java b/backend/src/main/java/com/mcmasterbaja/live/Serial.java similarity index 92% rename from API/src/main/java/backend/API/live/Serial.java rename to backend/src/main/java/com/mcmasterbaja/live/Serial.java index 25726187..cf38c081 100644 --- a/API/src/main/java/backend/API/live/Serial.java +++ b/backend/src/main/java/com/mcmasterbaja/live/Serial.java @@ -1,14 +1,15 @@ // Written by Gavin, history of pain on this one -package backend.API.live; +package com.mcmasterbaja.live; -import backend.API.binary_csv.Packet; import com.fazecast.jSerialComm.*; +import com.mcmasterbaja.binary_csv.Packet; import java.io.FileWriter; public class Serial { public static volatile boolean exit = true; public static void readLive() { + String rootLocation = "./uploads"; // To be replaced with a path exit = false; String port = "COM4"; SerialPort comPort = SerialPort.getCommPort(port); @@ -47,16 +48,18 @@ public static void readLive() { String[] strainNames = {"Force X", "Force Z", "Force Y", "Moment X", "Moment Z", "Moment Y"}; try { // print the current path - fw = new FileWriter("./upload-dir/live_F_RPM_PRIM.csv"); - fw2 = new FileWriter("./upload-dir/live_F_RPM_SEC.csv"); - fw42 = new FileWriter("./upload-dir/live_F_BELT_SPEED.csv"); + // TODO: Use the correct path from the application.properties file + fw = new FileWriter(rootLocation.toString() + "/live_F_RPM_PRIM.csv"); + fw2 = new FileWriter(rootLocation.toString() + "/live_F_RPM_SEC.csv"); + fw42 = new FileWriter(rootLocation.toString() + "/live_F_BELT_SPEED.csv"); fw.write("Timestamp (ms),F_RPM_PRIM\n"); fw2.write("Timestamp (ms),F_RPM_SEC\n"); fw42.write("Timestamp (ms),F_BELT_SPEED\n"); for (int i = 1; i <= 6; i++) { // create a new file writer for each file - strains[i - 1] = new FileWriter("./upload-dir/Live " + strainNames[i - 1] + ".csv"); + strains[i - 1] = + new FileWriter(rootLocation.toString() + "/Live " + strainNames[i - 1] + ".csv"); // write the header to the file strains[i - 1].write("Timestamp (ms)" + "," + strainNames[i - 1] + "\n"); } diff --git a/backend/src/main/java/com/mcmasterbaja/model/AnalyzerParams.java b/backend/src/main/java/com/mcmasterbaja/model/AnalyzerParams.java new file mode 100644 index 00000000..063fa5ac --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/AnalyzerParams.java @@ -0,0 +1,78 @@ +package com.mcmasterbaja.model; + +import jakarta.ws.rs.QueryParam; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +public class AnalyzerParams { + // Input and output files defined as strings in order for serialization in quarkus + @QueryParam("inputFiles") + private String[] inputFiles; + + @QueryParam("outputFiles") + private String[] outputFiles; + + @QueryParam("inputColumns") + private String[] inputColumns; + + @QueryParam("type") + private AnalyzerType type; + + @QueryParam("analyzerOptions") + private String[] options; + + @QueryParam("live") + private Boolean live; + + public List getErrors() { + ArrayList errors = new ArrayList(); + + if (inputFiles == null || inputFiles.length == 0) { + errors.add("inputFiles cannot be empty"); + } + if (inputColumns == null || inputColumns.length == 0) { + errors.add("inputColumns cannot be empty"); + } + + return errors; + } + + /** + * Converts to absolute path within the rootLocation/csv/ directory + * + * @param rootLocation the root location of the storage service + */ + public void updateInputFiles(Path rootLocation) { + if (inputFiles != null) { + inputFiles = + Arrays.stream(inputFiles) + .map(Paths::get) + .map(rootLocation.resolve("csv")::resolve) + .map(Path::toString) + .toArray(String[]::new); + } + } + + /** If output files are empty, auto-populates them with the format: inputFile_type.csv */ + public void generateOutputFileNames() { + if (outputFiles == null || outputFiles.length == 0) { + outputFiles = new String[inputFiles.length]; + for (int i = 0; i < inputFiles.length; i++) { + if (type == null) { + outputFiles[i] = inputFiles[i]; + } else { + outputFiles[i] = inputFiles[i].replace(".csv", "_" + type.toString() + ".csv"); + } + } + } + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/model/AnalyzerType.java b/backend/src/main/java/com/mcmasterbaja/model/AnalyzerType.java new file mode 100644 index 00000000..81f5b60c --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/AnalyzerType.java @@ -0,0 +1,18 @@ +package com.mcmasterbaja.model; + +public enum AnalyzerType { + ACCEL_CURVE, + AVERAGE, + CUBIC, + LINEAR_INTERPOLATE, + LINEAR_MULTIPLY, + INTERPOLATER_PRO, + RDP_COMPRESSION, + ROLL_AVG, + SGOLAY, + SPLIT; + + public String toString() { + return this.name(); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/model/ErrorResponse.java b/backend/src/main/java/com/mcmasterbaja/model/ErrorResponse.java new file mode 100644 index 00000000..ee713464 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/ErrorResponse.java @@ -0,0 +1,34 @@ +package com.mcmasterbaja.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import java.time.Instant; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@EqualsAndHashCode +@ToString +public class ErrorResponse { + + @JsonInclude( + JsonInclude.Include.NON_NULL) // Removes fields that are null from appearing in the response + private String errorId; + + private String path; + private String message; + private String errorType; + private Instant timestamp; + private Object details; + + public ErrorResponse( + String errorId, String path, String message, String errorType, Object details) { + this.errorId = errorId; + this.path = path; + this.message = message; + this.errorType = errorType; + this.timestamp = Instant.now(); + this.path = path; + this.details = details; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/model/FileInformation.java b/backend/src/main/java/com/mcmasterbaja/model/FileInformation.java new file mode 100644 index 00000000..3cd89123 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/FileInformation.java @@ -0,0 +1,27 @@ +package com.mcmasterbaja.model; + +import java.nio.file.Path; +import lombok.ToString; + +// This class represents the data structure that is used to send information about the file through +// to the front end +// In order to send information, there must be either public getters or public variables +@ToString +public class FileInformation { + + public String key; + public String[] fileHeaders; + public long size; + + public FileInformation(String key, String[] fileHeaders, long size) { + this.key = key; + this.fileHeaders = fileHeaders; + this.size = size; + } + + public FileInformation(Path key, String[] fileHeaders, long size) { + this.key = key.toString().replace("\\", "/"); + this.fileHeaders = fileHeaders; + this.size = size; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/model/FileTimespan.java b/backend/src/main/java/com/mcmasterbaja/model/FileTimespan.java new file mode 100644 index 00000000..df3d55ea --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/model/FileTimespan.java @@ -0,0 +1,25 @@ +package com.mcmasterbaja.model; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import lombok.ToString; + +@ToString +public class FileTimespan { + + public String key; + public LocalDateTime start; + public LocalDateTime end; + + public FileTimespan(String key, LocalDateTime start, LocalDateTime end) { + this.key = key; + this.start = start; + this.end = end; + } + + public FileTimespan(Path key, LocalDateTime start, LocalDateTime end) { + this.key = key.toString().replace("\\", "/"); + this.start = start; + this.end = end; + } +} diff --git a/API/src/main/java/backend/API/readwrite/CSVReader.java b/backend/src/main/java/com/mcmasterbaja/readwrite/CSVReader.java similarity index 97% rename from API/src/main/java/backend/API/readwrite/CSVReader.java rename to backend/src/main/java/com/mcmasterbaja/readwrite/CSVReader.java index f08fe0e7..a52d1743 100644 --- a/API/src/main/java/backend/API/readwrite/CSVReader.java +++ b/backend/src/main/java/com/mcmasterbaja/readwrite/CSVReader.java @@ -1,4 +1,4 @@ -package backend.API.readwrite; +package com.mcmasterbaja.readwrite; import java.io.BufferedReader; import java.io.File; diff --git a/API/src/main/java/backend/API/readwrite/CSVWriter.java b/backend/src/main/java/com/mcmasterbaja/readwrite/CSVWriter.java similarity index 96% rename from API/src/main/java/backend/API/readwrite/CSVWriter.java rename to backend/src/main/java/com/mcmasterbaja/readwrite/CSVWriter.java index 92eb42d0..0257bdde 100644 --- a/API/src/main/java/backend/API/readwrite/CSVWriter.java +++ b/backend/src/main/java/com/mcmasterbaja/readwrite/CSVWriter.java @@ -1,4 +1,4 @@ -package backend.API.readwrite; +package com.mcmasterbaja.readwrite; import java.io.BufferedWriter; import java.io.FileWriter; diff --git a/API/src/main/java/backend/API/readwrite/Reader.java b/backend/src/main/java/com/mcmasterbaja/readwrite/Reader.java similarity index 91% rename from API/src/main/java/backend/API/readwrite/Reader.java rename to backend/src/main/java/com/mcmasterbaja/readwrite/Reader.java index e36ef920..94a7a31b 100644 --- a/API/src/main/java/backend/API/readwrite/Reader.java +++ b/backend/src/main/java/com/mcmasterbaja/readwrite/Reader.java @@ -1,4 +1,4 @@ -package backend.API.readwrite; +package com.mcmasterbaja.readwrite; import java.util.List; diff --git a/API/src/main/java/backend/API/readwrite/Writer.java b/backend/src/main/java/com/mcmasterbaja/readwrite/Writer.java similarity index 90% rename from API/src/main/java/backend/API/readwrite/Writer.java rename to backend/src/main/java/com/mcmasterbaja/readwrite/Writer.java index 101b727f..f900944c 100644 --- a/API/src/main/java/backend/API/readwrite/Writer.java +++ b/backend/src/main/java/com/mcmasterbaja/readwrite/Writer.java @@ -1,4 +1,4 @@ -package backend.API.readwrite; +package com.mcmasterbaja.readwrite; import java.util.List; diff --git a/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java new file mode 100644 index 00000000..551f9f80 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/services/DefaultFileMetadataService.java @@ -0,0 +1,279 @@ +package com.mcmasterbaja.services; + +import com.drew.imaging.mp4.Mp4MetadataReader; +import com.drew.metadata.Tag; +import com.drew.metadata.mp4.Mp4Directory; +import com.mcmasterbaja.exceptions.FileNotFoundException; +import com.mcmasterbaja.exceptions.MalformedCsvException; +import com.mcmasterbaja.exceptions.StorageException; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Locale; +import java.util.NoSuchElementException; +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.jboss.logging.Logger; + +@ApplicationScoped +public class DefaultFileMetadataService implements FileMetadataService { + + @Inject Logger logger; + @Inject private StorageService storageService; + + public String[] readHeaders(Path targetPath) { + try { + return Files.lines(storageService.load(targetPath)).findFirst().get().split(","); + } catch (IOException e) { + throw new FileNotFoundException( + "Could not read headers of file: " + targetPath.toString(), e); + } catch (NoSuchElementException e) { + throw new MalformedCsvException( + "Could not read headers of file: " + targetPath.toString(), targetPath.toString(), e); + } + } + + public long getSize(Path targetPath) { + try { + return Files.size(storageService.load(targetPath)); + } catch (IOException e) { + throw new StorageException("Failed to get size of file: " + targetPath.toString(), e); + } + } + + public Double[] getMinMax(Path targetPath, String column) { + int columnIndex = -1; + Double min = Double.MAX_VALUE; + Double max = Double.MIN_VALUE; + + try { + BufferedReader reader = + new BufferedReader(Files.newBufferedReader(storageService.load(targetPath))); + + // First get the column index + String[] headers = reader.readLine().split(","); + + for (int i = 0; i < headers.length; i++) { + if (headers[i].equals(column)) { + columnIndex = i; + break; + } + } + + if (columnIndex == -1) { + throw new IllegalArgumentException("Column not found in file: " + targetPath.toString()); + } + + // Then get the minimum and maximum values + String line; + while ((line = reader.readLine()) != null) { + String[] values = line.split(","); + Double value = Double.parseDouble(values[columnIndex]); + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + } + + } catch (IOException e) { + throw new FileNotFoundException("Failed to get min max of file: " + targetPath.toString(), e); + } catch (NumberFormatException e) { + throw new MalformedCsvException( + "Failed to get min max of file: " + targetPath.toString(), targetPath.toString(), e); + } + + return new Double[] {min, max}; + } + + public String getLast(Path targetPath, int columnIndex) { + + String timestamp; + + try { + ReversedLinesFileReader reverseReader = + ReversedLinesFileReader.builder() + .setPath(storageService.load(targetPath)) + .setCharset(StandardCharsets.UTF_8) + .get(); + + timestamp = reverseReader.readLine().split(",")[columnIndex]; + reverseReader.close(); + } catch (IOException e) { + throw new FileNotFoundException("Failed to get last of file: " + targetPath.toString(), e); + } + + return timestamp; + } + + public boolean canComputeTimespan(Path folderPath) { + if (folderPath.toString().equals("csv")) return false; + Path smhPath = + storageService.getRootLocation().resolve(folderPath.resolve("GPS SECOND MINUTE HOUR.csv")); + Path dmyPath = storageService.load(folderPath.resolve("GPS DAY MONTH YEAR.csv")); + return Files.exists(smhPath) && Files.exists(dmyPath); + } + + public LocalDateTime[] getTimespan(Path targetPath, LocalDateTime zeroTime) { + switch (getTypeFolder(targetPath)) { + case "csv": + return getTimespanCSV(targetPath, zeroTime); + case "mp4": + return getTimespanMP4(targetPath); + default: + return null; + } + } + + public LocalDateTime getZeroTime(Path folderPath) { + try { + // Get the values of the first line from the gps files ingoring the header + String[] smhArray = + Files.lines( + storageService + .getRootLocation() + .resolve(folderPath.resolve("GPS SECOND MINUTE HOUR.csv"))) + .skip(1) + .findFirst() + .orElseThrow() + .split(","); + + String[] dmyArray = + Files.lines( + storageService + .getRootLocation() + .resolve(folderPath.resolve("GPS DAY MONTH YEAR.csv"))) + .skip(1) + .findFirst() + .orElseThrow() + .split(","); + + // Convert the values to a LocalDateTime and subtract the timestamp + long timestamp = Long.parseLong(smhArray[0]); + LocalDateTime zeroTime = + LocalDateTime.of( + 2000 + Integer.parseInt(dmyArray[3]), + Integer.parseInt(dmyArray[2]), + Integer.parseInt(dmyArray[1]), + Integer.parseInt(smhArray[3]), + Integer.parseInt(smhArray[2]), + Integer.parseInt(smhArray[1])) + .minusNanos(timestamp * 1_000_000); + + return zeroTime; + } catch (IOException e) { + throw new FileNotFoundException( + "Failed to get zeroTime of file: " + folderPath.toString(), e); + } + } + + public String getTypeFolder(Path targetPath) { + String pathString = targetPath.toString(); + int dotIndex = pathString.lastIndexOf("."); + if (pathString == "" || pathString == null || dotIndex == -1) return ""; // No file extension + + String extension = pathString.substring(dotIndex + 1).toLowerCase(); + // Returns csv for bin and mp4 for mov for file conversion + switch (extension) { + case "bin": + return "csv"; + case "mov": + return "mp4"; + default: + return extension; + } + } + + // Returns all the metadata in the file as string with commas between each value + // Each value will be in the format "key - value" + private String extractMetadata(Path targetPath) { + try { + // Gets all the metadata from the file in the form of a directory + Mp4Directory metadata = + Mp4MetadataReader.readMetadata(targetPath.toFile()) + .getFirstDirectoryOfType(Mp4Directory.class); + + // Extracts all the key value pairs + String metadataString = ""; + for (Tag tag : metadata.getTags()) { + metadataString += tag.toString() + ","; + } + return metadataString; + + } catch (IOException e) { + throw new FileNotFoundException( + "Failed to extract metadata of file: " + targetPath.toString(), e); + } + } + + // Gets the value of a tag from the metadata of a file + private String getTagValue(String metadata, String tag) { + // Finds the tag in the metadata + String[] metadataArray = metadata.split(","); + for (String tagString : metadataArray) { + if (tagString.contains(tag)) { + return tagString.split(" - ")[1]; + } + } + + return null; + } + + private LocalDateTime[] getTimespanCSV(Path targetPath, LocalDateTime zeroTime) { + String firstTimestamp = null; + String lastTimestamp = null; + try { + BufferedReader reader = + new BufferedReader(Files.newBufferedReader(storageService.load(targetPath))); + int timestampIndex = Arrays.asList(reader.readLine().split(",")).indexOf("Timestamp (ms)"); + firstTimestamp = reader.readLine().split(",")[timestampIndex]; + reader.close(); + lastTimestamp = getLast(targetPath, timestampIndex); + } catch (IOException e) { + throw new FileNotFoundException( + "Failed to get timespan of file: " + targetPath.toString(), e); + } catch (NoSuchElementException e) { + throw new MalformedCsvException( + "Failed to get timespan of file: " + targetPath.toString(), targetPath.toString(), e); + } + + LocalDateTime startTime = + zeroTime.plusNanos((long) Double.parseDouble(firstTimestamp) * 1_000_000); + LocalDateTime endTime = + zeroTime.plusNanos((long) Double.parseDouble(lastTimestamp) * 1_000_000); + + return new LocalDateTime[] {startTime, endTime}; + } + + private LocalDateTime[] getTimespanMP4(Path targetPath) { + // Gets the metadata of the file to find the creation time and duration + String metadata = extractMetadata(storageService.load(targetPath)); + + // Parses with timezeone, converts to GMT, and then to LocalDateTime + assert metadata != null; + LocalDateTime creationTime = + ZonedDateTime.parse( + getTagValue(metadata, "Creation Time"), + DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ENGLISH)) + .withZoneSameInstant(ZoneId.of("GMT")) + .toLocalDateTime(); + + // Below calculation gives a better estimate than Duration in Seconds tag + // Each is converted to nanoseconds and then divided to preserve precision + long duration = + (Long.parseLong(getTagValue(metadata, "Duration")) * 1_000_000_000) + / (Long.parseLong(getTagValue(metadata, "Media Time Scale")) * 1_000_000_000); + + // Returns the start and end times as strings in GMT with milliseconds + return new LocalDateTime[] {creationTime, creationTime.plusSeconds(duration)}; + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java new file mode 100644 index 00000000..5772119e --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/services/FileMetadataService.java @@ -0,0 +1,73 @@ +package com.mcmasterbaja.services; + +import java.nio.file.Path; +import java.time.LocalDateTime; + +public interface FileMetadataService { + + /** + * Reads the headers of a csv file. + * + * @param targetPath The Path of the file to read. + * @return A String[] of the headers. + */ + String[] readHeaders(Path targetPath); + + /** + * Gets the size of the file. + * + * @param targetPath The path of the file to read. + * @return the size of the file + */ + long getSize(Path targetPath); + + /** + * Gets the minimum and maximum values of a column in a csv file. + * + * @param targetPath The Path of the file to read. + * @param column The column to analyze. + * @return A double[] containing the minimum and maximum values. + */ + Double[] getMinMax(Path targetPath, String column); + + /** + * Gets the last value of the column in the file. + * + * @param targetPath The Path of the file to read. + * @return The last value of the column + */ + String getLast(Path targetPath, int columnIndex); + + /** + * Checks if the timespan of a folder can be computed. + * + * @param folderPath The Path of the folder to analyze. + * @return A boolean indicating if the timespan can be computed. + */ + boolean canComputeTimespan(Path folderPath); + + /** + * Gets the start and end times of a file in GMT + * + * @param targetPath The Path of the file to analyze. + * @param zeroTime The datetime when timestamp is zero milliseconds + * @return A LocalDateTime[] containing the start and end times of the file. + */ + LocalDateTime[] getTimespan(Path targetPath, LocalDateTime zeroTime); + + /** + * Gets the datetime in GMT when then timestamp is zero milliseconds. + * + * @param folderPath The Path of the folder to analyze. + * @return The datetime of the folder when timestamp (ms) is zero. + */ + LocalDateTime getZeroTime(Path folderPath); + + /** + * Gets the desired folder for a file. + * + * @param pathString The Path of the file to analyze. + * @return The desired type folder. + */ + String getTypeFolder(Path targetPath); +} diff --git a/backend/src/main/java/com/mcmasterbaja/services/FileSystemStorageService.java b/backend/src/main/java/com/mcmasterbaja/services/FileSystemStorageService.java new file mode 100644 index 00000000..03f3b218 --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/services/FileSystemStorageService.java @@ -0,0 +1,113 @@ +package com.mcmasterbaja.services; + +import com.mcmasterbaja.exceptions.FileNotFoundException; +import com.mcmasterbaja.exceptions.StorageException; +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +@ApplicationScoped // Singleton I think +public class FileSystemStorageService implements StorageService { + + @Inject Logger logger; + + @ConfigProperty(name = "quarkus.http.body.uploads-directory") + private Path rootLocation; + + @PostConstruct + public void init() { + try { + logger.info("Initializing storage service"); + Path[] directories = { + rootLocation, rootLocation.resolve("csv/"), rootLocation.resolve("mp4/") + }; + + for (Path directory : directories) { + if (!Files.exists(directory)) { + Files.createDirectories(directory); + } else { + logger.info("Directory already exists: " + directory.toString()); + } + } + } catch (IOException e) { + throw new StorageException("Failed to initialize the storage service.", e); + } + } + + public Path getRootLocation() { + return rootLocation; + } + + public void store(InputStream fileData, Path targetPath) { + try { + Path destinationFile = rootLocation.resolve(targetPath).normalize().toAbsolutePath(); + + if (!destinationFile.startsWith(this.rootLocation.toAbsolutePath())) { + throw new StorageException("Cannot store file outside current directory!"); + } + + Files.createDirectories(destinationFile.getParent()); + Files.copy(fileData, destinationFile); + fileData.close(); + } catch (IOException e) { + throw new StorageException("Could not store file: " + targetPath.toFile(), e); + } + } + + public Path load(Path targetPath) { + return rootLocation.resolve(targetPath); + } + + public Stream loadAll(Path dir) { + try { + return Files.walk(rootLocation.resolve(dir)) + .filter(path -> !Files.isDirectory(path)) + .map(rootLocation::relativize); + } catch (IOException e) { + throw new FileNotFoundException( + "Could not list files inside directory: " + dir.toString(), e); + } + } + + public Stream loadAll() { + return loadAll(rootLocation); + } + + public void delete(Path targetPath) { + try { + Files.delete(rootLocation.resolve(targetPath)); + } catch (IOException e) { + throw new FileNotFoundException("Could not delete file: " + targetPath.toString(), e); + } + } + + // TODO: Does not regenerate csv/ or mp4/ + public void deleteAll(Path dir) { + try { + Files.walk(rootLocation.resolve(dir)) + .sorted(Comparator.reverseOrder()) + .forEach( + file -> { + try { + Files.delete(file); + } catch (IOException e) { + throw new FileNotFoundException("Could not delete file: " + file.toString(), e); + } + }); + } catch (IOException e) { + throw new FileNotFoundException("Could not delete directory: " + dir.toString(), e); + } + } + + public void deleteAll() { + deleteAll(rootLocation); + } +} diff --git a/backend/src/main/java/com/mcmasterbaja/services/StorageService.java b/backend/src/main/java/com/mcmasterbaja/services/StorageService.java new file mode 100644 index 00000000..3b72c96f --- /dev/null +++ b/backend/src/main/java/com/mcmasterbaja/services/StorageService.java @@ -0,0 +1,66 @@ +package com.mcmasterbaja.services; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.stream.Stream; + +public interface StorageService { + + /** Initializes the storage service, setting up required directories. */ + void init(); + + /** + * Returns the root location where the files are stored. + * + * @return The root Path. + */ + Path getRootLocation(); + + /** + * Stores a file. + * + * @param fileData The InputStream of the file data to be stored. + * @param targetPath The Path under which the file is to be stored. + */ + void store(InputStream fileData, Path targetPath); + + /** + * Loads a file as a Path. + * + * @param targetPath The Path of the file to load. + * @return The Path to the file, which can be used to read or process the file. + */ + Path load(Path targetPath); + + /** + * Lists all files stored in the root location. + * + * @return A Stream of Paths representing the files. + */ + Stream loadAll(); + + /** + * Loads all files in a directory. + * + * @param dir The directory to load files from. + * @return A Stream of Paths representing the files + */ + Stream loadAll(Path dir); + + /** + * Deletes a file. + * + * @param targetPath The Path of the file to delete. + */ + void delete(Path targetPath); + + /** + * Deletes all files stored in a directory. + * + * @param targetPath The Path of the file to delete. + */ + void deleteAll(Path dir); + + /** Deletes all files in the root location. */ + void deleteAll(); +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 00000000..35804a17 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,15 @@ + +# File upload configuration +quarkus.http.body.handle-file-uploads=true +quarkus.http.body.uploads-directory=${user.dir}/uploads/ +quarkus.http.body.delete-uploaded-files-on-end=true +quarkus.http.limits.max-body-size=512M + +# Logger configuration +quarkus.log.level=INFO +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss} %-5p [%c] %s%e%n +quarkus.log.console.color=true + +# CORS configuration +quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000 \ No newline at end of file diff --git a/backend/src/test/java/com/mcmasterbaja/GreetingResourceIT.java b/backend/src/test/java/com/mcmasterbaja/GreetingResourceIT.java new file mode 100644 index 00000000..ec52648d --- /dev/null +++ b/backend/src/test/java/com/mcmasterbaja/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package com.mcmasterbaja; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/backend/src/test/java/com/mcmasterbaja/GreetingResourceTest.java b/backend/src/test/java/com/mcmasterbaja/GreetingResourceTest.java new file mode 100644 index 00000000..ed6fad8c --- /dev/null +++ b/backend/src/test/java/com/mcmasterbaja/GreetingResourceTest.java @@ -0,0 +1,17 @@ +package com.mcmasterbaja; + +import static io.restassured.RestAssured.given; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +// import static org.hamcrest.CoreMatchers.is; + +@QuarkusTest +class GreetingResourceTest { + @Test + void testHelloEndpoint() { + given().when().get("/files").then().statusCode(200); + // .body(is("Hello from Quarkus REST")); + } +} diff --git a/binary-to-csv-lib/Cargo.toml b/binary-to-csv-lib/Cargo.toml index c3ca89f7..33ed4e20 100644 --- a/binary-to-csv-lib/Cargo.toml +++ b/binary-to-csv-lib/Cargo.toml @@ -9,5 +9,5 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -jni = "0.20.0" +jni = "0.21.1" num_enum = "0.6.1" \ No newline at end of file diff --git a/binary-to-csv-lib/src/lib.rs b/binary-to-csv-lib/src/lib.rs index 23780426..0e300283 100644 --- a/binary-to-csv-lib/src/lib.rs +++ b/binary-to-csv-lib/src/lib.rs @@ -1,4 +1,4 @@ -use jni::objects::{JClass, JString}; +use jni::objects::{JByteArray, JClass, JString}; use jni::sys::jboolean; use jni::JNIEnv; @@ -11,6 +11,8 @@ use std::fs::File; use std::io::{BufWriter, Write}; use std::time::Instant; +// Adding a temporary comment to lib.rs + //create an enum for each data string #[allow(dead_code)] #[allow(non_camel_case_types)] @@ -219,8 +221,8 @@ pub extern "system" fn get_writer( #[no_mangle] #[allow(non_snake_case)] -pub extern "system" fn Java_backend_API_binary_1csv_BinaryTOCSV_toCSV( - env: JNIEnv, +pub extern "system" fn Java_com_mcmasterbaja_binary_1csv_BinaryToCSV_toCSV<'local>( + mut env: JNIEnv<'local>, _class: JClass, file_name: JString, destination: JString, @@ -229,11 +231,11 @@ pub extern "system" fn Java_backend_API_binary_1csv_BinaryTOCSV_toCSV( let now = Instant::now(); let file_name: String = env - .get_string(file_name) + .get_string(&file_name) .expect("Java string broken") .into(); let destination: String = env - .get_string(destination) + .get_string(&destination) .expect("Java string broken") .into(); @@ -259,7 +261,8 @@ pub extern "system" fn Java_backend_API_binary_1csv_BinaryTOCSV_toCSV( f32::from_bits(x[1]) % 100.0 / 60.0 + (f32::from_bits(x[1]) / 100.0).floor(), )), DataType::F_GPS_LONGITUDE => Some(Data::FloatData( - (f32::from_bits(x[1]) % 100.0 / 60.0 + (f32::from_bits(x[1]) / 100.0).floor()) * -1.0, + (f32::from_bits(x[1]) % 100.0 / 60.0 + (f32::from_bits(x[1]) / 100.0).floor()) + * -1.0, )), DataType::INT_GPS_TIME | DataType::INT_GPS_DAYMONTHYEAR @@ -361,3 +364,151 @@ pub extern "system" fn Java_backend_API_binary_1csv_BinaryTOCSV_toCSV( } println!("×All Done×\nCompleted in {}ms", now.elapsed().as_millis()); } + +#[no_mangle] +#[allow(non_snake_case)] +pub extern "system" fn Java_com_mcmasterbaja_binary_1csv_BinaryToCSV_bytesToCSV<'local>( + mut env: JNIEnv<'local>, + _class: JClass, + jbytes: JByteArray<'local>, + destination: JString, + file_name: JString, + folder: jboolean, +) { + let now = Instant::now(); + + let file_name: String = env + .get_string(&file_name) + .expect("Java string broken") + .into(); + let destination: String = env + .get_string(&destination) + .expect("Java string broken") + .into(); + + let folder = !matches!(folder, 0); + let bytes = env.convert_byte_array(jbytes).unwrap(); + + let packets = convert_to_32bit(&bytes); + //let packets = convert_to_32bit(&read_data(file)); + + //filter_map + let parse = |x: &[u32]| { + let timestamp: u32 = x[0] >> 6; + let datatype: u8 = (x[0] & 0x3F) as u8; + + if datatype >= 41 { + println!("Invalid datatype: {}", datatype); + return None; + } + let datatype: DataType = TryFrom::try_from(datatype).unwrap(); + + let data = match datatype { + DataType::INT_GPS_LAT | DataType::INT_GPS_LON => None, + DataType::F_GPS_LATITUDE => Some(Data::FloatData( + f32::from_bits(x[1]) % 100.0 / 60.0 + (f32::from_bits(x[1]) / 100.0).floor(), + )), + DataType::F_GPS_LONGITUDE => Some(Data::FloatData( + (f32::from_bits(x[1]) % 100.0 / 60.0 + (f32::from_bits(x[1]) / 100.0).floor()) + * -1.0, + )), + DataType::INT_GPS_TIME + | DataType::INT_GPS_DAYMONTHYEAR + | DataType::INT_GPS_SECONDMINUTEHOUR + | DataType::INT_PRIM_TEMP + | DataType::INT_STRAIN3 + | DataType::INT_STRAIN4 + | DataType::INT_STRAIN5 + | DataType::INT_STRAIN6 + | DataType::INT_BATT_PERC + | DataType::INT_SUS_TRAV_FL + | DataType::INT_SUS_TRAV_FR + | DataType::INT_SUS_TRAV_RL + | DataType::INT_SUS_TRAV_RR => Some(Data::IntData(x[1])), + + DataType::INT_STRAIN1 => Some(Data::FloatData( + 4078.4 * (f32::from_bits(x[1]) / 1024.0) * 3.3 - 7009.2, + )), + DataType::INT_STRAIN2 => Some(Data::FloatData( + 5288.0 * (f32::from_bits(x[1]) / 1024.0) * 3.3 - 5000.0, + )), + DataType::F_RPM_PRIM | DataType::F_RPM_SEC => { + let raw = f32::from_bits(x[1]); + (raw < 15000.0).then_some(Data::FloatData(raw)) + } + _ => { + let raw = f32::from_bits(x[1]); + Some(Data::FloatData(raw)) + } + }; + + data.map(|data| Packet { + timestamp, + datatype, + data, + }) + }; + let parsed_packets = packets.chunks(2).filter_map(parse); + let mut utilised_types: HashMap> = + HashMap::with_capacity(DATA_TYPE_LEN); + let extension_index = file_name.find('.').unwrap(); + let path_no_extension = destination.to_string() + "/" + &file_name[0..extension_index]; + if folder { + match std::fs::create_dir(&path_no_extension) { + Ok(_) => println!("Created folder: {}", path_no_extension), + Err(e) => { + if e.kind() == std::io::ErrorKind::AlreadyExists { + println!( + "Folder already exists: {}\nAttempting to delete and reparse", + path_no_extension + ); + std::fs::remove_dir_all(&path_no_extension).unwrap(); + std::fs::create_dir(&path_no_extension).unwrap(); + } else { + panic!("{}", e); + } + } + } + } + + for packet in parsed_packets { + utilised_types.entry(packet.datatype).or_insert_with(|| { + get_writer(&packet.datatype, &destination, &path_no_extension, folder) + }); + + match packet.datatype { + DataType::F_GPS_LATITUDE | DataType::F_GPS_LONGITUDE => { + utilised_types + .get_mut(&packet.datatype) + .unwrap() + .write_all(format!("{},{:.7}\n", packet.timestamp, packet.data).as_bytes()) + .unwrap(); + } + + DataType::INT_GPS_DAYMONTHYEAR | DataType::INT_GPS_SECONDMINUTEHOUR => { + utilised_types + .get_mut(&packet.datatype) + .unwrap() + .write_all( + format!( + "{},{},{},{}\n", + packet.timestamp, + ((&packet.data & (0b11111111 << 16)).unwrap() >> 16), + ((&packet.data & (0b11111111 << 8)).unwrap() >> 8), + (&packet.data & (0b11111111)).unwrap() + ) + .as_bytes(), + ) + .unwrap(); + } + _ => { + utilised_types + .get_mut(&packet.datatype) + .unwrap() + .write_all(format!("{},{:.2}\n", packet.timestamp, packet.data).as_bytes()) + .unwrap(); + } + }; + } + println!("×All Done×\nCompleted in {}ms", now.elapsed().as_millis()); +} diff --git a/front-end/src/components/analyzerData.js b/front-end/src/components/analyzerData.js index edd9966a..ebbe5a60 100644 --- a/front-end/src/components/analyzerData.js +++ b/front-end/src/components/analyzerData.js @@ -29,7 +29,7 @@ const analyzerData = [ }, { title: 'Acceleration Curve Tool', - code: 'accelCurve', + code: 'ACCEL_CURVE', parameters: [], description: 'Given both primary (on y-axis) and secondary (x-axis) RPM values, this tool will first apply a noise reduction algorithm, and then interpolate between them to achieve a graph that displays the shift curve. ', image: { @@ -46,7 +46,7 @@ const analyzerData = [ }, { title: 'Ultimate Smoothener', - code: 'sGolay', + code: 'SGOLAY', parameters: [{ name: 'Window Size', default: '100' }, { name: 'Polynomial Order', default: '3' }], description: 'Implements the Savitzky-Golay algorithm in order to smooth out a curve. This is a very powerful tool that can help capture many trends not visible. Input variables are the window and polynomial order.', image: { @@ -63,7 +63,7 @@ const analyzerData = [ }, { title: 'Interpolation', - code: 'interpolaterPro', + code: 'INTERPOLATER_PRO', parameters: [], // One day should take in column description: 'Interpolation is the act of adding new data points between existing data points. This is useful for making data more readable, or for making it easier to compare data sets. This is implemented linearly.', image: { @@ -80,7 +80,7 @@ const analyzerData = [ }, { title: 'Moving Average', - code: 'rollAvg', + code: 'ROLL_AVG', parameters: [{ name: 'WindowSize', default: '100' }], description: 'Given noisy data, this will take the average over a window of points. This should help reduce noise, but can add other imperfections.', image: { @@ -97,7 +97,7 @@ const analyzerData = [ }, { title: 'Compression', - code: 'RDPCompression', + code: 'RDP_COMPRESSION', parameters: [{ name: 'Epsilon', default: '0.1' }], description: 'The Ramer-Douglas-Peucker algorithm helps simplify a curve by removing some of its points while keeping its overall shape intact. It\'s a handy tool for looking at large files!', image: { @@ -114,7 +114,7 @@ const analyzerData = [ }, { title: 'Split', - code: 'split', + code: 'SPLIT', parameters: [{ name: 'Start', default: '0' }, { name: 'End', default: null }], description: 'Splits the data into two parts, given a start and end point (timestamp)', image: { @@ -131,7 +131,7 @@ const analyzerData = [ }, { title: 'Linear Multiply.', - code: 'linearMultiply', + code: 'LINEAR_MULTIPLY', parameters: [{ name: 'Multiplier', default: '1' }, { name: 'Offset', default: '0' }], description: 'Given a multiplier and offset, this will multiply the data by the multiplier and add the offset.', image: { @@ -152,7 +152,7 @@ const analyzerData = [ }, { title: 'Cubic Multiply.', - code: 'cubic', + code: 'CUBIC', parameters: [{ name: 'A', default: '1' }, { name: 'B', default: '1' }, { name: 'C', default: '1' }, { name: 'D', default: '1' }], description: 'Given the coefficients of a cubic function, this will pass the data into the cubic function.', image: { diff --git a/front-end/src/components/map/MapDisplay.js b/front-end/src/components/map/MapDisplay.js index 52bb8f89..0e63b42e 100644 --- a/front-end/src/components/map/MapDisplay.js +++ b/front-end/src/components/map/MapDisplay.js @@ -122,7 +122,8 @@ const MapDisplay = ({ setLapsCallback, gotoTime }) => { [`${chosen}/${LAT_COLUMNNAME}.csv`, `${chosen}/${LNG_COLUMNNAME}.csv`], [LAT_COLUMNNAME, LNG_COLUMNNAME], [], - ['interpolaterPro'], + 'INTERPOLATER_PRO', + [], false ) .then((response) => response.text()) diff --git a/front-end/src/components/modal/FileStorage.js b/front-end/src/components/modal/FileStorage.js index fc4f4a55..68ea92c3 100644 --- a/front-end/src/components/modal/FileStorage.js +++ b/front-end/src/components/modal/FileStorage.js @@ -5,6 +5,7 @@ import RawFileBrowser, { Icons } from 'react-keyed-file-browser'; import React from 'react'; const formatSize = (size) => { + if (size === 0) return '0 B'; // Finds the order of magnitude of the size in base 1024 (e.g. how many digits it would have) const magnitude = Math.floor(Math.log(size) / Math.log(1024)); const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'ZB', 'YB', 'RB', 'QB']; diff --git a/front-end/src/components/modal/create/CreateGraphModal.js b/front-end/src/components/modal/create/CreateGraphModal.js index c4c0491e..6e431087 100644 --- a/front-end/src/components/modal/create/CreateGraphModal.js +++ b/front-end/src/components/modal/create/CreateGraphModal.js @@ -85,6 +85,7 @@ export const CreateGraphModal = ({ * For none datetime format: * - x-axis will be linear with no shifting of x-values. */ + let dtformat = 'full'; if (chartInformationFiles.some(file => file.columns[0].timespan.start === '')) dtformat = 'partial'; if (chartInformationFiles.some(file => file.columns[0].header !== 'Timestamp (ms)')) dtformat = 'none'; diff --git a/front-end/src/components/views/Chart.js b/front-end/src/components/views/Chart.js index f41604fb..5f8c4138 100644 --- a/front-end/src/components/views/Chart.js +++ b/front-end/src/components/views/Chart.js @@ -44,8 +44,9 @@ const Chart = ({ chartInformation, video, videoTimestamp }) => { files, inputColumns.map(col => col.header), [], - [chartInformation.files[i].analyze.analysis, chartInformation.files[i].analyze.analyzerValues].filter(e => e), - [chartInformation.live] + chartInformation.files[i].analyze.analysis, + chartInformation.files[i].analyze.analyzerValues.filter(e => e), + chartInformation.live ); const filename = response.headers.get('content-disposition').split('filename=')[1].slice(1, -1); diff --git a/front-end/src/lib/apiUtils.js b/front-end/src/lib/apiUtils.js index 9dac97ea..8914004e 100644 --- a/front-end/src/lib/apiUtils.js +++ b/front-end/src/lib/apiUtils.js @@ -7,6 +7,7 @@ export const ApiUtil = { * @returns {Promise} A promise that resolves to the server's response. */ getFile: async (fileKey) => { + fileKey = encodeURIComponent(fileKey); const response = await fetch(`http://${window.location.hostname}:8080/files/${fileKey}`); if (!response.ok) throw Error(response.statusText); return response; @@ -28,12 +29,17 @@ export const ApiUtil = { }, /** - * @description Sends a GET request to the server to fetch a specific folder. + * @description Sends a GET request to the server to fetch fileInformation about a specific folder. + * Each file in the list is represented as an object with the following properties: + * - key: A that represents the unique identifier of the file. This will be relative to the folder provided. + * - fileHeaders: An array of strings that represents the headers of the file. + * - size: A long that represents the size of the file. + * * @param {string} folderKey - The unique identifier of the folder. * @returns {Promise} A promise that resolves to the server's response. */ getFolder: async (folderKey) => { - const response = await fetch(`http://${window.location.hostname}:8080/files/folder/${folderKey}`); + const response = await fetch(`http://${window.location.hostname}:8080/files/information/folder/${folderKey}`); if (!response.ok) throw Error(response.statusText); return response; }, @@ -44,7 +50,7 @@ export const ApiUtil = { * @returns {Promise} A promise that resolves to the server's response. */ getTimespans: async (folderKey) => { - const response = await fetch(`http://${window.location.hostname}:8080/timespan/folder/${folderKey}`); + const response = await fetch(`http://${window.location.hostname}:8080//files/timespan/folder/${folderKey}`); if (!response.ok) throw Error(response.statusText); return response; }, @@ -54,19 +60,32 @@ export const ApiUtil = { * @param {string} inputFiles - The input files. * @param {string} inputColumns - The input columns. * @param {string} outputFiles - The output files. + * @param {Enum} type - The analyzer type. * @param {string} analyzerOptions - The analyzer options. - * @param {string} liveOptions - The live options. + * @param {Boolean} live - The live options. * @returns {Promise} A promise that resolves to the server's response. */ - analyzeFiles: async (inputFiles, inputColumns, outputFiles, analyzerOptions, liveOptions) => { + analyzeFiles: async (inputFiles, inputColumns, outputFiles, type, analyzerOptions, live) => { try { - const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + new URLSearchParams({ - inputFiles: inputFiles, - inputColumns: inputColumns, - outputFiles: outputFiles, - analyzer: analyzerOptions, - liveOptions: liveOptions - })); + const params = new URLSearchParams(); + const parameters = { inputFiles, inputColumns, outputFiles, type, analyzerOptions, live }; + + Object.entries(parameters).forEach(([key, value]) => { + if (value && value.length !== 0) { + if (Array.isArray(value)) { + value.forEach((val) => { + params.append(key, val); + }); + } else { + params.append(key, value); + } + } + }); + + const response = await fetch(`http://${window.location.hostname}:8080/analyze?` + params.toString(), { + method: 'POST' + }); + if (!response.ok) { alert(`An error has occured!\nCode: ${response.status}\n${await response.text()}`); throw Error(response.statusText); @@ -84,7 +103,7 @@ export const ApiUtil = { * @returns {Promise} A promise that resolves to the server's response. */ getMinMax: async (filename, header) => { - const url = `http://${window.location.hostname}:8080/files/maxmin/${filename}?headerName=${header}`; + const url = `http://${window.location.hostname}:8080/minMax/${encodeURIComponent(filename)}?column=${header}`; const response = await fetch(url); if (!response.ok) { @@ -99,7 +118,7 @@ export const ApiUtil = { * @returns {Promise} A promise that resolves to the server's response. */ deleteAllFiles: async () => { - const response = await fetch(`http://${window.location.hostname}:8080/deleteAll`, { + const response = await fetch(`http://${window.location.hostname}:8080/delete/all`, { // method: "DELETE" }); @@ -116,9 +135,9 @@ export const ApiUtil = { const formData = new FormData(); formData.append('port', port); - const response = await fetch(`http://${window.location.hostname}:8080/live`, { - method: 'POST', - body: formData, + const response = await fetch(`http://${window.location.hostname}:8080/togglelive`, { + method: 'PATCH', + //body: formData, }); if (!response.ok) throw Error(response.statusText); @@ -132,9 +151,10 @@ export const ApiUtil = { */ uploadFile: async (file) => { const formData = new FormData(); - formData.set('file', file); + formData.set('fileName', file.name); + formData.set('fileData', file); - const response = await fetch(`http://${window.location.hostname}:8080/upload`, { + const response = await fetch(`http://${window.location.hostname}:8080/upload/file`, { method: 'POST', body: formData, }); diff --git a/front-end/src/lib/chartUtils.js b/front-end/src/lib/chartUtils.js index 927d6c62..e9b7ce4d 100644 --- a/front-end/src/lib/chartUtils.js +++ b/front-end/src/lib/chartUtils.js @@ -37,8 +37,8 @@ export const getSeriesData = async (text, filename, columns, minMax, chartType, // Make a request to get the maximum and minimum values of the colour value // TODO: Seems to break when giving it a file with 3+ colomns, worth looking into const minMaxResponse = await ApiUtil.getMinMax(filename, columns[columns.length -1].header); - - let [minval, maxval] = (await minMaxResponse.text()).split(',').map(parseFloat); + + let [minval, maxval] = JSON.parse(await minMaxResponse.text()); minMax.current = [minval, maxval]; return lines.map((line) => { diff --git a/front-end/src/lib/mapUtils.js b/front-end/src/lib/mapUtils.js index 0cc66c78..f479d1b2 100644 --- a/front-end/src/lib/mapUtils.js +++ b/front-end/src/lib/mapUtils.js @@ -81,7 +81,7 @@ export function findLapTimes(coords, rects) { currLap.checkpoints.push(event.time); visitedCheckpoints.push(event.rect); // console.log(event.time + ": Checkpoint"); - console.log(visitedCheckpoints, event.rect); + // console.log(visitedCheckpoints, event.rect); } } // console.log(event.time, ": " +event.event + " " + rects[event.rect].type + " " + event.rect); diff --git a/start.ps1 b/start.ps1 index 27bb2215..c2014808 100644 --- a/start.ps1 +++ b/start.ps1 @@ -1,20 +1,20 @@ #If the script does not run add your java home locations to the array below -$javaHomeLocations = @('C:\Program Files\Java\jdk-17.03\','C:\Program Files\Java\jdk-19\','C:\Program Files\Java\jdk-17.0.5\','C:\Program Files\Java\jdk-17.0.2\', 'C:\Program Files\Java\jdk-19', 'C:\Program Files\Java\jdk-20\','C:\Program Files\Java\jdk-17.03\', 'C:\Program Files\Java\jdk-21\') +$javaHomeLocations = @('C:\Program Files\Java\jdk-21\', '') $counter = 0 $env:JAVA_HOME = $javaHomeLocations[0] try{ Set-Location front-end Start-Process powershell {npm start} - Set-Location ../API - ./mvnw spring-boot:run + Set-Location ../backend + ./mvnw quarkus:dev while ($counter -lt $javaHomeLocations.Length) { if ($LastExitCode -ne 0) { "Add your Java Home locations to the array in the start.ps1 file" $env:JAVA_HOME = $javaHomeLocations[$counter] - ./mvnw spring-boot:run + ./mvnw quarkus:dev $counter++ } else